Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CachePurgingService
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 11
2162
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 can_purge
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 manual_purge_request
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 purge_page_caches
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 purge_all
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 purge_url
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 on_save_post
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
110
 on_edit_term
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 on_update_comment
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 on_update_option
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
90
 is_public_taxonomy
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace NewfoldLabs\WP\Module\Performance\Cache;
4
5use NewfoldLabs\WP\Module\Performance\Performance;
6use NewfoldLabs\WP\Module\Performance\Cache\Types\CacheBase;
7use NewfoldLabs\WP\Module\Performance\Cache\Types\ObjectCache;
8use wpscholar\Url;
9
10use function NewfoldLabs\WP\Module\Performance\to_studly_case;
11use function NewfoldLabs\WP\Module\Performance\to_snake_case;
12
13/**
14 * Cache purging service.
15 */
16class CachePurgingService {
17
18    /**
19     * Define cache types.
20     *
21     * @var array|CacheBase[] $cache_types Cache types.
22     */
23    public $cache_types = array();
24
25    /**
26     * Constructor.
27     *
28     * @param CacheBase[] $cache_types Cache types.
29     */
30    public function __construct( array $cache_types ) {
31
32        $this->cache_types = $cache_types;
33
34        if ( $this->can_purge() ) {
35
36            // Handle manual purge requests
37            add_action( 'init', array( $this, 'manual_purge_request' ) );
38
39            // Handle automatic purging
40            add_action( 'transition_post_status', array( $this, 'on_save_post' ), 10, 3 );
41            add_action( 'edit_terms', array( $this, 'on_edit_term' ) );
42            add_action( 'comment_post', array( $this, 'on_update_comment' ) );
43            add_action( 'updated_option', array( $this, 'on_update_option' ), 10, 3 );
44            add_action( 'wp_update_nav_menu', array( $this, 'purge_all' ) );
45
46        }
47    }
48
49    /**
50     * Check if the cache can be purged.
51     *
52     * @return bool
53     */
54    public function can_purge() {
55        foreach ( $this->cache_types as $instance ) {
56            if ( array_key_exists( Purgeable::class, class_implements( $instance ) ) ) {
57                return true;
58            }
59        }
60
61        return false;
62    }
63
64    /**
65     * Listens for purge actions and handles based on type.
66     */
67    public function manual_purge_request() {
68
69        $purge_all = Performance::PURGE_ALL;
70        $purge_url = Performance::PURGE_URL;
71
72        if ( ( isset( $_GET[ $purge_all ] ) || isset( $_GET[ $purge_url ] ) ) && is_user_logged_in() && current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
73
74            $url = new Url();
75            $url->removeQueryVar( $purge_all );
76            $url->removeQueryVar( $purge_url );
77
78            if ( isset( $_GET[ $purge_all ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
79                $this->purge_all();
80            } else {
81                $this->purge_url( Url::stripQueryString( $url ) );
82            }
83            wp_safe_redirect(
84                $url,
85                302,
86                'Newfold File Caching'
87            );
88            exit;
89        }
90    }
91
92    /**
93     * Purge page caches only (File, Browser). Does not flush object cache.
94     * Use when enabling object cache so session/auth data is not flushed.
95     */
96    public function purge_page_caches() {
97        foreach ( $this->cache_types as $instance ) {
98            if ( array_key_exists( Purgeable::class, class_implements( $instance ) ) ) {
99                /**
100                 * Purgeable instance.
101                 *
102                 * @var Purgeable $instance
103                 */
104                $instance->purge_all();
105            }
106        }
107    }
108
109    /**
110     * Purge everything (page caches and object cache).
111     */
112    public function purge_all() {
113        $this->purge_page_caches();
114        ObjectCache::flush_object_cache();
115    }
116
117    /**
118     * Purge a specific URL.
119     *
120     * @param  string $url  The URL to be purged.
121     */
122    public function purge_url( $url ) {
123        foreach ( $this->cache_types as $instance ) {
124            if ( array_key_exists( Purgeable::class, class_implements( $instance ) ) ) {
125                /**
126                 * Purgeable instance.
127                 *
128                 * @var Purgeable $instance
129                 */
130                $instance->purge_url( $url );
131            }
132        }
133    }
134
135    /**
136     * Purge appropriate caches when a post is updated.
137     *
138     * @param  string   $oldStatus  The previous post status
139     * @param  string   $newStatus  The new post status
140     * @param  \WP_Post $post  The post object of the edited or created post
141     */
142    public function on_save_post( $oldStatus, $newStatus, \WP_Post $post ) {
143
144        // Skip purging for non-public post types
145        if ( ! get_post_type_object( $post->post_type )->public ) {
146            return;
147        }
148
149        // Skip purging if the post wasn't public before and isn't now
150        if ( 'publish' !== $oldStatus && 'publish' !== $newStatus ) {
151            return;
152        }
153
154        // Purge post URL when post is updated.
155        $permalink = get_permalink( $post );
156        if ( $permalink ) {
157            $this->purge_url( $permalink );
158        }
159
160        // Purge taxonomy term URLs for related terms.
161        $taxonomies = get_post_taxonomies( $post );
162        foreach ( $taxonomies as $taxonomy ) {
163            if ( $this->is_public_taxonomy( $taxonomy ) ) {
164                $terms = get_the_terms( $post, $taxonomy );
165                if ( is_array( $terms ) ) {
166                    foreach ( $terms as $term ) {
167                        $term_link = get_term_link( $term );
168                        $this->purge_url( $term_link );
169                    }
170                }
171            }
172        }
173
174        // Purge post type archive URL when post is updated.
175        $post_type_archive = get_post_type_archive_link( $post->post_type );
176        if ( $post_type_archive ) {
177            $this->purge_url( $post_type_archive );
178        }
179
180        // Purge date archive URL when post is updated.
181        $year_archive = get_year_link( (int) get_the_date( 'y', $post ) );
182        $this->purge_url( $year_archive );
183    }
184
185    /**
186     * Purge taxonomy term URL when a term is updated.
187     *
188     * @param  int $termId  Term ID
189     */
190    public function on_edit_term( $termId ) {
191        $url = get_term_link( $termId );
192        if ( ! is_wp_error( $url ) ) {
193            $this->purge_url( $url );
194        }
195    }
196
197    /**
198     * Purge a single post when a comment is updated.
199     *
200     * @param  int $commentId  ID of the comment.
201     */
202    public function on_update_comment( $commentId ) {
203        $comment = get_comment( $commentId );
204        if ( $comment && property_exists( $comment, 'comment_post_ID' ) ) {
205            $postUrl = get_permalink( $comment->comment_post_ID );
206            if ( $postUrl ) {
207                $this->purge_url( $postUrl );
208            }
209        }
210    }
211
212    /**
213     * Purge page caches when an option is updated. Does not flush object cache (Redis).
214     *
215     * @param  string $option    Option name.
216     * @param  mixed  $oldValue  Old option value.
217     * @param  mixed  $newValue  New option value.
218     *
219     * @return bool
220     */
221    public function on_update_option( $option, $oldValue, $newValue ) {
222        // No need to process if nothing was updated
223        if ( $oldValue === $newValue ) {
224            return false;
225        }
226
227        $exemptIfEquals = array(
228            'active_plugins'    => true,
229            'html_type'         => true,
230            'fs_accounts'       => true,
231            'rewrite_rules'     => true,
232            'uninstall_plugins' => true,
233            'wp_user_roles'     => true,
234        );
235
236        // If we have an exact match, we can just stop here.
237        if ( array_key_exists( $option, $exemptIfEquals ) ) {
238            return false;
239        }
240
241        $forceIfContains = array(
242            'html',
243            'css',
244            'style',
245            'query',
246            'queries',
247        );
248
249        $exemptIfContains = array(
250            '_active',
251            '_activated',
252            '_activation',
253            '_attempts',
254            '_available',
255            '_blacklist',
256            '_cache_validator',
257            '_check_',
258            '_checksum',
259            '_config',
260            '_count',
261            '_dectivated',
262            '_disable',
263            '_enable',
264            '_errors',
265            '_hash',
266            '_inactive',
267            '_installed',
268            '_key',
269            '_last_',
270            '_license',
271            '_log_',
272            '_mode',
273            '_options',
274            '_pageviews',
275            '_redirects',
276            '_rules',
277            '_schedule',
278            '_session',
279            '_settings',
280            '_shown',
281            '_stats',
282            '_status',
283            '_statistics',
284            '_supports',
285            '_sync',
286            '_task',
287            '_time',
288            '_token',
289            '_traffic',
290            '_transient',
291            '_url_',
292            '_version',
293            '_views',
294            '_visits',
295            '_whitelist',
296            '404s',
297            'cron',
298            'limit_login_',
299            'nonce',
300            'user_roles',
301        );
302
303        $force_purge = false;
304
305        if ( ctype_upper( str_replace( array( '-', '_' ), '', $option ) ) ) {
306            $option = strtolower( $option );
307        }
308        $option_name = '_' . to_snake_case( to_studly_case( $option ) ) . '_';
309
310        foreach ( $forceIfContains as $slug ) {
311            if ( false !== strpos( $option_name, $slug ) ) {
312                $force_purge = true;
313                break;
314            }
315        }
316
317        if ( ! $force_purge ) {
318            foreach ( $exemptIfContains as $slug ) {
319                if ( false !== strpos( $option_name, $slug ) ) {
320                    return false;
321                }
322            }
323        }
324
325        $this->purge_page_caches();
326
327        return true;
328    }
329
330    /**
331     * Checks if a taxonomy is public.
332     *
333     * @param  string $taxonomy  Taxonomy name.
334     *
335     * @return boolean
336     */
337    protected function is_public_taxonomy( $taxonomy ) {
338        $public          = false;
339        $taxonomy_object = get_taxonomy( $taxonomy );
340        if ( $taxonomy_object && isset( $taxonomy_object->public ) ) {
341            $public = $taxonomy_object->public;
342        }
343
344        return $public;
345    }
346}