PluginProbe
SpinupWP / trunk
SpinupWP vtrunk
trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2 1.3 1.4 1.4.1 1.4.2 1.5 1.5.1 1.6 1.7 1.7.1 1.8.0 1.9.0 1.9.1
spinupwp / src / Cache.php

Cache.php in SpinupWP trunk, at src/Cache.php

488 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SpinupWp;
4
5 use SpinupWp\Cli\CacheCommands;
6 use WP_Post;
7
8 class Cache {
9
10 /**
11 * @var AdminBar
12 */
13 private $admin_bar;
14
15 /**
16 * @var Cli
17 */
18 private $cli;
19
20 /**
21 * @var string
22 */
23 private $cache_path;
24
25 /**
26 * Cache constructor.
27 *
28 * @param AdminBar $admin_bar
29 */
30 public function __construct( AdminBar $admin_bar, Cli $cli ) {
31 $this->admin_bar = $admin_bar;
32 $this->cli = $cli;
33 }
34
35 /**
36 * Init
37 */
38 public function init() {
39 $this->set_cache_path();
40
41 $this->cli->register_command( 'spinupwp cache', CacheCommands::class );
42
43 add_action( 'init', array( $this, 'register_admin_bar_menu_items' ) );
44 add_action( 'spinupwp_purge_object_cache', array( $this, 'purge_object_cache' ) );
45 add_action( 'spinupwp_purge_page_cache', array( $this, 'purge_page_cache' ) );
46 add_action( 'spinupwp_purge_url', array( $this, 'purge_url' ) );
47 add_action( 'admin_init', array( $this, 'handle_manual_purge_action' ) );
48 add_action( 'transition_post_status', array( $this, 'purge_post_on_update' ), 10, 3 );
49 add_action( 'delete_post', array( $this, 'purge_post_on_delete' ), 10, 1 );
50 add_action( 'switch_theme', array( $this, 'purge_page_cache' ) );
51 add_action( 'comment_post', array( $this, 'purge_post_on_comment' ), 10, 2 );
52 add_action( 'wp_set_comment_status', array( $this, 'purge_post_by_comment' ) );
53 add_action( 'upgrader_process_complete', array( $this, 'purge_page_cache_on_shutdown' ) );
54 }
55
56 /**
57 * Register cache purge menu items in the admin bar.
58 *
59 * @return void
60 */
61 public function register_admin_bar_menu_items() {
62 if ( $this->is_object_cache_enabled() && $this->is_page_cache_enabled() ) {
63 $this->admin_bar->add_item( __( 'Purge All Caches', 'spinupwp' ), 'purge-all' );
64 }
65
66 if ( $this->is_object_cache_enabled() ) {
67 $this->admin_bar->add_item( __( 'Purge Object Cache', 'spinupwp' ), 'purge-object' );
68 }
69
70 if ( $this->is_page_cache_enabled() ) {
71 $this->admin_bar->add_item( __( 'Purge Page Cache', 'spinupwp' ), 'purge-page' );
72 }
73
74 if ( $this->is_page_cache_enabled() && ! is_admin() ) {
75 $this->admin_bar->add_item( __( 'Purge this URL', 'spinupwp' ), 'purge-url' );
76 }
77 }
78
79
80 /**
81 * Handle manual purge actions.
82 */
83 public function handle_manual_purge_action() {
84 if ( ! current_user_can( apply_filters( 'spinupwp_purge_cache_capability', 'manage_options' ) ) ) {
85 return;
86 }
87
88 $action = filter_input( INPUT_GET, 'spinupwp_action' );
89
90 if ( ! $action || ! in_array( $action, array( 'purge-all', 'purge-object', 'purge-page', 'purge-url' ) ) ) {
91 return;
92 }
93
94 if ( ! wp_verify_nonce( filter_input( INPUT_GET, '_wpnonce' ), $action ) ) {
95 return;
96 }
97
98 if ( 'purge-all' === $action ) {
99 $purge_object_cache = $this->purge_object_cache();
100 $purge_page_cache = $this->purge_page_cache();
101
102 $purge = $purge_object_cache && $purge_page_cache;
103 $type = 'all';
104 }
105
106 if ( 'purge-object' === $action ) {
107 $purge = $this->purge_object_cache();
108 $type = 'object';
109 }
110
111 if ( 'purge-page' === $action ) {
112 $purge = $this->purge_page_cache();
113 $type = 'page';
114 }
115
116 if ( 'purge-url' === $action ) {
117 $url = $_SERVER['HTTP_REFERER'];
118 $purge = $this->purge_url( $url );
119 $type = 'url';
120 }
121
122 $redirect_url = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : admin_url();
123
124 wp_safe_redirect( add_query_arg( array(
125 'purge_success' => (int) $purge,
126 'cache_type' => $type,
127 ), $redirect_url ) );
128 }
129
130 /**
131 * When a post is transitioned to 'publish' for the first time purge the
132 * entire site cache. This ensures blog pages, category archives, author archives,
133 * search results and the 'Latest Posts' footer section is accurate. Otherwise,
134 * only update the current post URL.
135 *
136 * @param string $new_status
137 * @param string $old_status
138 * @param WP_Post $post
139 *
140 * @return bool
141 */
142 public function purge_post_on_update( $new_status, $old_status, $post ) {
143 $post_type = get_post_type( $post );
144
145 if ( ! in_array( $post_type, $this->get_public_post_types() ) ) {
146 return false;
147 }
148
149 if ( in_array( $post_type, $this->get_post_types_excluded_from_purge() ) ) {
150 return false;
151 }
152
153 if ( ! $this->should_purge_post_status( $new_status, $old_status ) ) {
154 return false;
155 }
156
157 if ( $post_type === 'customize_changeset' && $new_status === 'trash' ) {
158 return false;
159 }
160
161 if ( in_array( $post_type, $this->get_post_types_needing_single_purge() ) ) {
162 return $this->purge_post( $post );
163 }
164
165 return $this->purge_page_cache();
166 }
167
168 /**
169 * Purge the entire cache when a post type is deleted.
170 *
171 * @param int $post_id
172 *
173 * @return bool
174 */
175 public function purge_post_on_delete( $post_id ) {
176 $post_type = get_post_type( $post_id );
177
178 if ( ! in_array( $post_type, $this->get_public_post_types() ) ) {
179 return false;
180 }
181
182 if ( in_array( $post_type, $this->get_post_types_excluded_from_purge() ) ) {
183 return false;
184 }
185
186 $post_status = get_post_status( $post_id );
187
188 if ( in_array( $post_status, array( 'auto-draft', 'draft', 'trash' ) ) ) {
189 return false;
190 }
191
192 return $this->purge_page_cache();
193 }
194
195 /**
196 * Purge a post on new comment (if approved).
197 *
198 * @param int $comment_id
199 * @param bool $comment_approved
200 *
201 * @return bool
202 */
203 public function purge_post_on_comment( $comment_id, $comment_approved ) {
204 if ( ! $comment_approved ) {
205 return false;
206 }
207
208 return $this->purge_post_by_comment( $comment_id );
209 }
210
211 /**
212 * Purge a post by comment ID.
213 *
214 * @param int $comment_id
215 *
216 * @return bool
217 */
218 public function purge_post_by_comment( $comment_id ) {
219 $comment = get_comment( $comment_id );
220
221 if ( $comment && $comment->comment_post_ID ) {
222 $post = get_post( $comment->comment_post_ID );
223
224 return $this->purge_post( $post );
225 }
226
227 return false;
228 }
229
230 /**
231 * Purge the page cache on shutdown.
232 */
233 public function purge_page_cache_on_shutdown() {
234 add_action( 'shutdown', array( $this, 'purge_page_cache' ) );
235 }
236
237 /**
238 * Set the base cache path.
239 */
240 private function set_cache_path() {
241 if ( getenv( 'SPINUPWP_CACHE_PATH' ) ) {
242 $this->cache_path = getenv( 'SPINUPWP_CACHE_PATH' );
243 }
244
245 if ( defined( 'SPINUPWP_CACHE_PATH' ) ) {
246 $this->cache_path = SPINUPWP_CACHE_PATH;
247 }
248 }
249
250 /**
251 * Is the object cache enabled?
252 *
253 * @return bool
254 */
255 public function is_object_cache_enabled() {
256 return wp_using_ext_object_cache();
257 }
258
259 /**
260 * Is the page cache enabled?
261 *
262 * @return bool
263 */
264 public function is_page_cache_enabled() {
265 return defined( 'SPINUPWP_CACHE_PATH' ) || getenv( 'SPINUPWP_CACHE_PATH' );
266 }
267
268 /**
269 * Purge entire object cache.
270 *
271 * @return bool
272 */
273 public function purge_object_cache() {
274 return wp_cache_flush();
275 }
276
277 /**
278 * Purge entire cache.
279 *
280 * @return bool
281 */
282 public function purge_page_cache() {
283 $result = $this->delete( $this->cache_path, true );
284
285 do_action( 'spinupwp_site_purged', $result );
286
287 return $result;
288 }
289
290 /**
291 * Purge the current post URL.
292 *
293 * @param WP_Post $post
294 *
295 * @return bool
296 */
297 public function purge_post( $post ) {
298 $result = $this->purge_url( get_permalink( $post ) );
299
300 do_action( 'spinupwp_post_purged', $post, $result );
301
302 return $result;
303 }
304
305 /**
306 * Purge a single URL from the cache.
307 *
308 * @param string $url
309 *
310 * @return bool
311 */
312 public function purge_url( $url ) {
313 $cache_paths = $this->get_cache_paths_for_url( $url );
314
315 $all_deleted = true;
316 foreach ( $cache_paths as $path ) {
317 $deleted = $this->delete( $path );
318 do_action( 'spinupwp_url_purged', $url, $deleted );
319 $all_deleted &= $deleted;
320 }
321
322 return $all_deleted;
323 }
324
325 /**
326 * Gets the cache file paths for a given URL.
327 *
328 * Must be using the default Nginx cache options (levels=1:2)
329 * https://www.digitalocean.com/community/tutorials/how-to-setup-fastcgi-caching-with-nginx-on-your-vps#purging-the-cache
330 *
331 * @param string $url
332 *
333 * @return array
334 */
335 private function get_cache_paths_for_url( $url ) {
336 $cache_keys = $this->get_cache_keys_for_url( $url );
337
338 $cache_paths = array();
339 foreach ( $cache_keys as $key ) {
340 $hashed_key = md5( $key );
341 $path = substr( $hashed_key, - 1 ) . '/' . substr( $hashed_key, - 3, 2 ) . '/' . $hashed_key;
342 $cache_paths[] = trailingslashit( $this->cache_path ) . $path;
343 }
344
345 return $cache_paths;
346 }
347
348 /**
349 * Gets the cache keys for a given URL.
350 *
351 * It defaults to a single key: (fastcgi_cache_key "$scheme$request_method$host$request_uri"),
352 * but it can be modified with spinupwp_cache_key_components filter.
353 * This must match the Nginx configuration.
354 *
355 * @param string $url
356 *
357 * @return array
358 */
359 private function get_cache_keys_for_url( $url ) {
360 $parsed_url = parse_url( $url );
361
362 $cache_keys = array(
363 $parsed_url['scheme'] . 'GET' . $parsed_url['host'] . trailingslashit( $parsed_url['path'] ),
364 $parsed_url['scheme'] . 'GET' . $parsed_url['host'] . untrailingslashit( $parsed_url['path'] ),
365 );
366
367 // Allow the cache keys to be modified
368 $cache_keys = apply_filters( 'spinupwp_cache_keys_for_url', $cache_keys, $url );
369
370 return $cache_keys;
371 }
372
373 /**
374 * Delete a file/dir from the local filesystem.
375 *
376 * @param string $path Absolute path to file
377 * @param bool $recursive
378 *
379 * @return bool
380 */
381 private function delete( $path, $recursive = false ) {
382 if ( null !== $path && file_exists( $path ) && is_writable( $path ) ) {
383 require_once( ABSPATH . '/wp-admin/includes/file.php' );
384
385 $context = $path;
386 if ( is_file( $path ) ) {
387 $context = dirname( $path );
388 }
389
390 if ( ! WP_Filesystem( false, $context, true ) ) {
391 return false;
392 }
393
394 global $wp_filesystem;
395 $wp_filesystem->delete( $path, $recursive );
396
397 return true;
398 }
399
400 return $this->delete_via_cache_daemon( $path );
401 }
402
403 /**
404 * Use SpinupWP daemon to purge cache.
405 *
406 * @param string $path
407 *
408 * @return bool
409 */
410 private function delete_via_cache_daemon( $path ) {
411 $fp = @fsockopen( '127.0.0.1', '7836' );
412
413 if ( $fp ) {
414 fwrite( $fp, $path . "\n" );
415
416 $result = fread( $fp, 512 );
417 fclose( $fp );
418
419 return (bool) preg_match( '/^Purge:\sSuccess/', $result );
420 }
421
422 return false;
423 }
424
425 /**
426 * Should a post be purged based on the new/old status.
427 *
428 * @param string $new_status
429 * @param string $old_status
430 *
431 * @return bool
432 */
433 private function should_purge_post_status( $new_status, $old_status ) {
434 // A newly created post with no content
435 if ( $new_status === 'auto-draft' ) {
436 return false;
437 }
438
439 // A post in draft status
440 if ( $new_status === 'draft' && in_array( $old_status, array( 'auto-draft', 'draft', 'trash' ) ) ) {
441 return false;
442 }
443
444 // A post in trash status
445 if ( $new_status === 'trash' && in_array( $old_status, array( 'auto-draft', 'draft', 'trash' ) ) ) {
446 return false;
447 }
448
449 return apply_filters( 'spinupwp_should_purge_post_status', true );
450 }
451
452 /**
453 * Get public post types that should trigger a cache purge.
454 *
455 * @return array
456 */
457 private function get_public_post_types() {
458 $post_types = get_post_types( [
459 'public' => true,
460 ] );
461
462 return apply_filters( 'spinupwp_public_post_types', $post_types );
463 }
464
465 /**
466 * Get post types that should only purge their own public facing URL.
467 *
468 * @return array
469 */
470 private function get_post_types_needing_single_purge() {
471 return apply_filters( 'spinupwp_post_types_needing_single_purge', array() );
472 }
473
474 /**
475 * Get post types that should never trigger a cache purge.
476 *
477 * @return array
478 */
479 private function get_post_types_excluded_from_purge() {
480 return apply_filters( 'spinupwp_post_types_excluded_from_purge', array(
481 'attachment',
482 'custom_css',
483 'revision',
484 'user_request'
485 ) );
486 }
487 }
488