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