Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 153
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CachePurgingService
0.00% covered (danger)
0.00%
0 / 153
0.00% covered (danger)
0.00%
0 / 10
2070
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_all
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 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 everything.
94     */
95    public function purge_all() {
96        foreach ( $this->cache_types as $instance ) {
97            if ( array_key_exists( Purgeable::class, class_implements( $instance ) ) ) {
98                /**
99                 * Purgeable instance.
100                 *
101                 * @var Purgeable $instance
102                 */
103                $instance->purge_all();
104            }
105        }
106        ObjectCache::flush_object_cache();
107    }
108
109    /**
110     * Purge a specific URL.
111     *
112     * @param  string $url  The URL to be purged.
113     */
114    public function purge_url( $url ) {
115        foreach ( $this->cache_types as $instance ) {
116            if ( array_key_exists( Purgeable::class, class_implements( $instance ) ) ) {
117                /**
118                 * Purgeable instance.
119                 *
120                 * @var Purgeable $instance
121                 */
122                $instance->purge_url( $url );
123            }
124        }
125    }
126
127    /**
128     * Purge appropriate caches when a post is updated.
129     *
130     * @param  string   $oldStatus  The previous post status
131     * @param  string   $newStatus  The new post status
132     * @param  \WP_Post $post  The post object of the edited or created post
133     */
134    public function on_save_post( $oldStatus, $newStatus, \WP_Post $post ) {
135
136        // Skip purging for non-public post types
137        if ( ! get_post_type_object( $post->post_type )->public ) {
138            return;
139        }
140
141        // Skip purging if the post wasn't public before and isn't now
142        if ( 'publish' !== $oldStatus && 'publish' !== $newStatus ) {
143            return;
144        }
145
146        // Purge post URL when post is updated.
147        $permalink = get_permalink( $post );
148        if ( $permalink ) {
149            $this->purge_url( $permalink );
150        }
151
152        // Purge taxonomy term URLs for related terms.
153        $taxonomies = get_post_taxonomies( $post );
154        foreach ( $taxonomies as $taxonomy ) {
155            if ( $this->is_public_taxonomy( $taxonomy ) ) {
156                $terms = get_the_terms( $post, $taxonomy );
157                if ( is_array( $terms ) ) {
158                    foreach ( $terms as $term ) {
159                        $term_link = get_term_link( $term );
160                        $this->purge_url( $term_link );
161                    }
162                }
163            }
164        }
165
166        // Purge post type archive URL when post is updated.
167        $post_type_archive = get_post_type_archive_link( $post->post_type );
168        if ( $post_type_archive ) {
169            $this->purge_url( $post_type_archive );
170        }
171
172        // Purge date archive URL when post is updated.
173        $year_archive = get_year_link( (int) get_the_date( 'y', $post ) );
174        $this->purge_url( $year_archive );
175    }
176
177    /**
178     * Purge taxonomy term URL when a term is updated.
179     *
180     * @param  int $termId  Term ID
181     */
182    public function on_edit_term( $termId ) {
183        $url = get_term_link( $termId );
184        if ( ! is_wp_error( $url ) ) {
185            $this->purge_url( $url );
186        }
187    }
188
189    /**
190     * Purge a single post when a comment is updated.
191     *
192     * @param  int $commentId  ID of the comment.
193     */
194    public function on_update_comment( $commentId ) {
195        $comment = get_comment( $commentId );
196        if ( $comment && property_exists( $comment, 'comment_post_ID' ) ) {
197            $postUrl = get_permalink( $comment->comment_post_ID );
198            if ( $postUrl ) {
199                $this->purge_url( $postUrl );
200            }
201        }
202    }
203
204    /**
205     * Purge all caches when an option is updated.
206     *
207     * @param  string $option    Option name.
208     * @param  mixed  $oldValue  Old option value.
209     * @param  mixed  $newValue  New option value.
210     *
211     * @return bool
212     */
213    public function on_update_option( $option, $oldValue, $newValue ) {
214        // No need to process if nothing was updated
215        if ( $oldValue === $newValue ) {
216            return false;
217        }
218
219        $exemptIfEquals = array(
220            'active_plugins'    => true,
221            'html_type'         => true,
222            'fs_accounts'       => true,
223            'rewrite_rules'     => true,
224            'uninstall_plugins' => true,
225            'wp_user_roles'     => true,
226        );
227
228        // If we have an exact match, we can just stop here.
229        if ( array_key_exists( $option, $exemptIfEquals ) ) {
230            return false;
231        }
232
233        $forceIfContains = array(
234            'html',
235            'css',
236            'style',
237            'query',
238            'queries',
239        );
240
241        $exemptIfContains = array(
242            '_active',
243            '_activated',
244            '_activation',
245            '_attempts',
246            '_available',
247            '_blacklist',
248            '_cache_validator',
249            '_check_',
250            '_checksum',
251            '_config',
252            '_count',
253            '_dectivated',
254            '_disable',
255            '_enable',
256            '_errors',
257            '_hash',
258            '_inactive',
259            '_installed',
260            '_key',
261            '_last_',
262            '_license',
263            '_log_',
264            '_mode',
265            '_options',
266            '_pageviews',
267            '_redirects',
268            '_rules',
269            '_schedule',
270            '_session',
271            '_settings',
272            '_shown',
273            '_stats',
274            '_status',
275            '_statistics',
276            '_supports',
277            '_sync',
278            '_task',
279            '_time',
280            '_token',
281            '_traffic',
282            '_transient',
283            '_url_',
284            '_version',
285            '_views',
286            '_visits',
287            '_whitelist',
288            '404s',
289            'cron',
290            'limit_login_',
291            'nonce',
292            'user_roles',
293        );
294
295        $force_purge = false;
296
297        if ( ctype_upper( str_replace( array( '-', '_' ), '', $option ) ) ) {
298            $option = strtolower( $option );
299        }
300        $option_name = '_' . to_snake_case( to_studly_case( $option ) ) . '_';
301
302        foreach ( $forceIfContains as $slug ) {
303            if ( false !== strpos( $option_name, $slug ) ) {
304                $force_purge = true;
305                break;
306            }
307        }
308
309        if ( ! $force_purge ) {
310            foreach ( $exemptIfContains as $slug ) {
311                if ( false !== strpos( $option_name, $slug ) ) {
312                    return false;
313                }
314            }
315        }
316
317        $this->purge_all();
318
319        return true;
320    }
321
322    /**
323     * Checks if a taxonomy is public.
324     *
325     * @param  string $taxonomy  Taxonomy name.
326     *
327     * @return boolean
328     */
329    protected function is_public_taxonomy( $taxonomy ) {
330        $public          = false;
331        $taxonomy_object = get_taxonomy( $taxonomy );
332        if ( $taxonomy_object && isset( $taxonomy_object->public ) ) {
333            $public = $taxonomy_object->public;
334        }
335
336        return $public;
337    }
338}