PluginProbe
Autoptimize / 2.5.0
Autoptimize v2.5.0
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeCache.php

autoptimizeCache.php in Autoptimize 2.5.0, at classes/autoptimizeCache.php

690 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles disk-cache-related operations.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 class autoptimizeCache
11 {
12 /**
13 * Cache filename.
14 *
15 * @var string
16 */
17 private $filename;
18
19 /**
20 * Cache directory path (with a trailing slash).
21 *
22 * @var string
23 */
24 private $cachedir;
25
26 /**
27 * Whether gzipping is done by the web server or us.
28 * True => we don't gzip, the web server does it.
29 * False => we do it ourselves.
30 *
31 * @var bool
32 */
33 private $nogzip;
34
35 /**
36 * Ctor.
37 *
38 * @param string $md5 Hash.
39 * @param string $ext Extension.
40 */
41 public function __construct( $md5, $ext = 'php' )
42 {
43 $this->cachedir = AUTOPTIMIZE_CACHE_DIR;
44 $this->nogzip = AUTOPTIMIZE_CACHE_NOGZIP;
45 if ( ! $this->nogzip ) {
46 $this->filename = AUTOPTIMIZE_CACHEFILE_PREFIX . $md5 . '.php';
47 } else {
48 if ( in_array( $ext, array( 'js', 'css' ) ) ) {
49 $this->filename = $ext . '/' . AUTOPTIMIZE_CACHEFILE_PREFIX . $md5 . '.' . $ext;
50 } else {
51 $this->filename = AUTOPTIMIZE_CACHEFILE_PREFIX . $md5 . '.' . $ext;
52 }
53 }
54 }
55
56 /**
57 * Returns true if the cached file exists on disk.
58 *
59 * @return bool
60 */
61 public function check()
62 {
63 return file_exists( $this->cachedir . $this->filename );
64 }
65
66 /**
67 * Returns cache contents if they exist, false otherwise.
68 *
69 * @return string|false
70 */
71 public function retrieve()
72 {
73 if ( $this->check() ) {
74 if ( false == $this->nogzip ) {
75 return file_get_contents( $this->cachedir . $this->filename . '.none' );
76 } else {
77 return file_get_contents( $this->cachedir . $this->filename );
78 }
79 }
80 return false;
81 }
82
83 /**
84 * Stores given $data in cache.
85 *
86 * @param string $data Data to cache.
87 * @param string $mime Mimetype.
88 *
89 * @return void
90 */
91 public function cache( $data, $mime )
92 {
93 // off by default; check if cachedirs exist every time before caching
94 //
95 // to be activated for users that experience these ugly errors;
96 // PHP Warning: file_put_contents failed to open stream: No such file or directory.
97 if ( apply_filters( 'autoptimize_filter_cache_checkdirs_on_write', false ) ) {
98 $this->check_and_create_dirs();
99 }
100
101 if ( false === $this->nogzip ) {
102 // We handle gzipping ourselves.
103 $file = 'default.php';
104 $phpcode = file_get_contents( AUTOPTIMIZE_PLUGIN_DIR . 'config/' . $file );
105 $phpcode = str_replace( array( '%%CONTENT%%', 'exit;' ), array( $mime, '' ), $phpcode );
106
107 file_put_contents( $this->cachedir . $this->filename, $phpcode );
108 file_put_contents( $this->cachedir . $this->filename . '.none', $data );
109 } else {
110 // Write code to cache without doing anything else.
111 file_put_contents( $this->cachedir . $this->filename, $data );
112 if ( apply_filters( 'autoptimize_filter_cache_create_static_gzip', false ) ) {
113 // Create an additional cached gzip file.
114 file_put_contents( $this->cachedir . $this->filename . '.gz', gzencode( $data, 9, FORCE_GZIP ) );
115 // If PHP Brotli extension is installed, create an additional cached Brotli file.
116 if ( function_exists( 'brotli_compress' ) ) {
117 file_put_contents( $this->cachedir . $this->filename . '.br', brotli_compress( $data, 11, BROTLI_GENERIC ) );
118 }
119 }
120 }
121 }
122
123 /**
124 * Get cache filename.
125 *
126 * @return string
127 */
128 public function getname()
129 {
130 // NOTE: This could've maybe been a do_action() instead, however,
131 // that ship has sailed.
132 // The original idea here was to provide 3rd party code a hook so that
133 // it can "listen" to all the complete autoptimized-urls that the page
134 // will emit... Or something to that effect I think?
135 apply_filters( 'autoptimize_filter_cache_getname', AUTOPTIMIZE_CACHE_URL . $this->filename );
136
137 return $this->filename;
138 }
139
140 /**
141 * Returns true if given `$file` is considered a valid Autoptimize cache file,
142 * false otherwise.
143 *
144 * @param string $dir Directory name (with a trailing slash).
145 * @param string $file Filename.
146 * @return bool
147 */
148 protected static function is_valid_cache_file( $dir, $file )
149 {
150 if ( '.' !== $file && '..' !== $file &&
151 false !== strpos( $file, AUTOPTIMIZE_CACHEFILE_PREFIX ) &&
152 is_file( $dir . $file ) ) {
153
154 // It's a valid file!
155 return true;
156 }
157
158 // Everything else is considered invalid!
159 return false;
160 }
161
162 /**
163 * Clears contents of AUTOPTIMIZE_CACHE_DIR.
164 *
165 * @return void
166 */
167 protected static function clear_cache_classic()
168 {
169 $contents = self::get_cache_contents();
170 foreach ( $contents as $name => $files ) {
171 $dir = rtrim( AUTOPTIMIZE_CACHE_DIR . $name, '/' ) . '/';
172 foreach ( $files as $file ) {
173 if ( self::is_valid_cache_file( $dir, $file ) ) {
174 @unlink( $dir . $file ); // @codingStandardsIgnoreLine
175 }
176 }
177 }
178
179 @unlink( AUTOPTIMIZE_CACHE_DIR . '/.htaccess' ); // @codingStandardsIgnoreLine
180 }
181
182 /**
183 * Recursively deletes the specified pathname (file/directory) if possible.
184 * Returns true on success, false otherwise.
185 *
186 * @param string $pathname Pathname to remove.
187 *
188 * @return bool
189 */
190 protected static function rmdir( $pathname )
191 {
192 $files = self::get_dir_contents( $pathname );
193 foreach ( $files as $file ) {
194 $path = $pathname . '/' . $file;
195 if ( is_dir( $path ) ) {
196 self::rmdir( $path );
197 } else {
198 unlink( $path );
199 }
200 }
201
202 return rmdir( $pathname );
203 }
204
205 /**
206 * Clears contents of AUTOPTIMIZE_CACHE_DIR by renaming the current
207 * cache directory into a new one with a unique name and then
208 * re-creating the default (empty) cache directory.
209 *
210 * @return bool Returns true when everything is done successfully, false otherwise.
211 */
212 protected static function clear_cache_via_rename()
213 {
214 $ok = false;
215 $dir = self::get_pathname_base();
216 $new_name = self::get_unique_name();
217
218 // Makes sure the new pathname is on the same level...
219 $new_pathname = dirname( $dir ) . '/' . $new_name;
220 $renamed = @rename( $dir, $new_pathname ); // @codingStandardsIgnoreLine
221
222 // When renamed, re-create the default cache directory back so it's
223 // available again...
224 if ( $renamed ) {
225 $ok = self::cacheavail();
226 }
227
228 return $ok;
229 }
230
231 /**
232 * Returns true when advanced cache clearing is enabled.
233 *
234 * @return bool
235 */
236 public static function advanced_cache_clear_enabled()
237 {
238 return apply_filters( 'autoptimize_filter_cache_clear_advanced', false );
239 }
240
241 /**
242 * Returns a (hopefully) unique new cache folder name for renaming purposes.
243 *
244 * @return string
245 */
246 protected static function get_unique_name()
247 {
248 $prefix = self::get_advanced_cache_clear_prefix();
249 $new_name = uniqid( $prefix, true );
250
251 return $new_name;
252 }
253
254 /**
255 * Get cache prefix name used in advanced cache clearing mode.
256 *
257 * @return string
258 */
259 protected static function get_advanced_cache_clear_prefix()
260 {
261 $pathname = self::get_pathname_base();
262 $basename = basename( $pathname );
263 $prefix = $basename . '-';
264
265 return $prefix;
266 }
267
268 /**
269 * Returns an array of file and directory names found within
270 * the given $pathname without '.' and '..' elements.
271 *
272 * @param string $pathname Pathname.
273 *
274 * @return array
275 */
276 protected static function get_dir_contents( $pathname )
277 {
278 return array_slice( scandir( $pathname ), 2 );
279 }
280
281 /**
282 * Wipes directories which were created as part of the fast cache clearing
283 * routine (which renames the current cache directory into a new one with
284 * a custom-prefixed unique name).
285 *
286 * @return bool
287 */
288 public static function delete_advanced_cache_clear_artifacts()
289 {
290 $dir = self::get_pathname_base();
291 $prefix = self::get_advanced_cache_clear_prefix();
292 $parent = dirname( $dir );
293 $ok = false;
294
295 // Returns the list of files without '.' and '..' elements.
296 $files = self::get_dir_contents( $parent );
297 if ( is_array( $files ) && ! empty( $files ) ) {
298 foreach ( $files as $file ) {
299 $path = $parent . '/' . $file;
300 $prefixed = ( false !== strpos( $path, $prefix ) );
301 // Removing only our own (prefixed) directories...
302 if ( is_dir( $path ) && $prefixed ) {
303 $ok = self::rmdir( $path );
304 }
305 }
306 }
307
308 return $ok;
309 }
310
311 /**
312 * Returns the cache directory pathname used.
313 * Done as a function so we canSlightly different
314 * if multisite is used and `autoptimize_separate_blog_caches` filter
315 * is used.
316 *
317 * @return string
318 */
319 public static function get_pathname()
320 {
321 $pathname = self::get_pathname_base();
322
323 if ( is_multisite() && apply_filters( 'autoptimize_separate_blog_caches', true ) ) {
324 $blog_id = get_current_blog_id();
325 $pathname .= $blog_id . '/';
326 }
327
328 return $pathname;
329 }
330
331 /**
332 * Returns the base path of our cache directory.
333 *
334 * @return string
335 */
336 protected static function get_pathname_base()
337 {
338 $pathname = WP_CONTENT_DIR . AUTOPTIMIZE_CACHE_CHILD_DIR;
339
340 return $pathname;
341 }
342
343 /**
344 * Deletes everything from the cache directories.
345 *
346 * @param bool $propagate Whether to trigger additional actions when cache is purged.
347 *
348 * @return bool
349 */
350 public static function clearall( $propagate = true )
351 {
352 if ( ! self::cacheavail() ) {
353 return false;
354 }
355
356 // TODO/FIXME: If cache is big, switch to advanced/new cache clearing automatically?
357 if ( self::advanced_cache_clear_enabled() ) {
358 self::clear_cache_via_rename();
359 } else {
360 self::clear_cache_classic();
361 }
362
363 // Remove the transient so it gets regenerated...
364 delete_transient( 'autoptimize_stats' );
365
366 // Cache was just purged, clear page cache and allow others to hook into our purging...
367 if ( true === $propagate ) {
368 if ( ! function_exists( 'autoptimize_do_cachepurged_action' ) ) {
369 function autoptimize_do_cachepurged_action() {
370 do_action( 'autoptimize_action_cachepurged' );
371 }
372 }
373 add_action( 'shutdown', 'autoptimize_do_cachepurged_action', 11 );
374 add_action( 'autoptimize_action_cachepurged', array( 'autoptimizeCache', 'flushPageCache' ), 10, 0 );
375 }
376
377 // Warm cache (part of speedupper)!
378 if ( apply_filters( 'autoptimize_filter_speedupper', true ) && false == get_transient( 'autoptimize_cache_warmer_protector' ) ) {
379 set_transient( 'autoptimize_cache_warmer_protector', 'I shall not warm cache for another 10 minutes.', 60 * 10 );
380 $url = site_url() . '/?ao_speedup_cachebuster=' . rand( 1, 100000 );
381 $cache = @wp_remote_get( $url ); // @codingStandardsIgnoreLine
382 unset( $cache );
383 }
384
385 return true;
386 }
387
388 /**
389 * Wrapper for clearall but with false param
390 * to ensure the event is not propagated to others
391 * through our own hooks (to avoid infinite loops).
392 *
393 * @return bool
394 */
395 public static function clearall_actionless()
396 {
397 return self::clearall( false );
398 }
399
400 /**
401 * Returns the contents of our cache dirs.
402 *
403 * @return array
404 */
405 protected static function get_cache_contents()
406 {
407 $contents = array();
408
409 foreach ( array( '', 'js', 'css' ) as $dir ) {
410 $contents[ $dir ] = scandir( AUTOPTIMIZE_CACHE_DIR . $dir );
411 }
412
413 return $contents;
414 }
415
416 /**
417 * Returns stats about cached contents.
418 *
419 * @return array
420 */
421 public static function stats()
422 {
423 $stats = get_transient( 'autoptimize_stats' );
424
425 // If no transient, do the actual scan!
426 if ( ! is_array( $stats ) ) {
427 if ( ! self::cacheavail() ) {
428 return 0;
429 }
430 $stats = self::stats_scan();
431 $count = $stats[0];
432 if ( $count > 100 ) {
433 // Store results in transient.
434 set_transient(
435 'autoptimize_stats',
436 $stats,
437 apply_filters( 'autoptimize_filter_cache_statsexpiry', HOUR_IN_SECONDS )
438 );
439 }
440 }
441
442 return $stats;
443 }
444
445 /**
446 * Performs a scan of cache directory contents and returns an array
447 * with 3 values: count, size, timestamp.
448 * count = total number of found files
449 * size = total filesize (in bytes) of found files
450 * timestamp = unix timestamp when the scan was last performed/finished.
451 *
452 * @return array
453 */
454 protected static function stats_scan()
455 {
456 $count = 0;
457 $size = 0;
458
459 // Scan everything in our cache directories.
460 foreach ( self::get_cache_contents() as $name => $files ) {
461 $dir = rtrim( AUTOPTIMIZE_CACHE_DIR . $name, '/' ) . '/';
462 foreach ( $files as $file ) {
463 if ( self::is_valid_cache_file( $dir, $file ) ) {
464 if ( AUTOPTIMIZE_CACHE_NOGZIP &&
465 (
466 false !== strpos( $file, '.js' ) ||
467 false !== strpos( $file, '.css' ) ||
468 false !== strpos( $file, '.img' ) ||
469 false !== strpos( $file, '.txt' )
470 )
471 ) {
472 // Web server is gzipping, we count .js|.css|.img|.txt files.
473 $count++;
474 } elseif ( ! AUTOPTIMIZE_CACHE_NOGZIP && false !== strpos( $file, '.none' ) ) {
475 // We are gzipping ourselves via php, counting only .none files.
476 $count++;
477 }
478 $size += filesize( $dir . $file );
479 }
480 }
481 }
482
483 $stats = array( $count, $size, time() );
484
485 return $stats;
486 }
487
488 /**
489 * Ensures the cache directory exists, is writeable and contains the
490 * required .htaccess files.
491 * Returns false in case it fails to ensure any of those things.
492 *
493 * @return bool
494 */
495 public static function cacheavail()
496 {
497 if ( false === autoptimizeCache::check_and_create_dirs() ) {
498 return false;
499 }
500
501 // Using .htaccess inside our cache folder to overrule wp-super-cache.
502 $htaccess = AUTOPTIMIZE_CACHE_DIR . '/.htaccess';
503 if ( ! is_file( $htaccess ) ) {
504 /**
505 * Create `wp-content/AO_htaccess_tmpl` file with
506 * whatever htaccess rules you might need
507 * if you want to override default AO htaccess
508 */
509 $htaccess_tmpl = WP_CONTENT_DIR . '/AO_htaccess_tmpl';
510 if ( is_file( $htaccess_tmpl ) ) {
511 $content = file_get_contents( $htaccess_tmpl );
512 } elseif ( is_multisite() || ! AUTOPTIMIZE_CACHE_NOGZIP ) {
513 $content = '<IfModule mod_expires.c>
514 ExpiresActive On
515 ExpiresByType text/css A30672000
516 ExpiresByType text/javascript A30672000
517 ExpiresByType application/javascript A30672000
518 </IfModule>
519 <IfModule mod_headers.c>
520 Header append Cache-Control "public, immutable"
521 </IfModule>
522 <IfModule mod_deflate.c>
523 <FilesMatch "\.(js|css)$">
524 SetOutputFilter DEFLATE
525 </FilesMatch>
526 </IfModule>
527 <IfModule mod_authz_core.c>
528 <Files *.php>
529 Require all granted
530 </Files>
531 </IfModule>
532 <IfModule !mod_authz_core.c>
533 <Files *.php>
534 Order allow,deny
535 Allow from all
536 </Files>
537 </IfModule>';
538 } else {
539 $content = '<IfModule mod_expires.c>
540 ExpiresActive On
541 ExpiresByType text/css A30672000
542 ExpiresByType text/javascript A30672000
543 ExpiresByType application/javascript A30672000
544 </IfModule>
545 <IfModule mod_headers.c>
546 Header append Cache-Control "public, immutable"
547 </IfModule>
548 <IfModule mod_deflate.c>
549 <FilesMatch "\.(js|css)$">
550 SetOutputFilter DEFLATE
551 </FilesMatch>
552 </IfModule>
553 <IfModule mod_authz_core.c>
554 <Files *.php>
555 Require all denied
556 </Files>
557 </IfModule>
558 <IfModule !mod_authz_core.c>
559 <Files *.php>
560 Order deny,allow
561 Deny from all
562 </Files>
563 </IfModule>';
564 }
565 @file_put_contents( $htaccess, $content ); // @codingStandardsIgnoreLine
566 }
567
568 // All OK!
569 return true;
570 }
571
572 /**
573 * Checks if cache dirs exist and create if not.
574 * Returns false if not succesful.
575 *
576 * @return bool
577 */
578 public static function check_and_create_dirs() {
579 if ( ! defined( 'AUTOPTIMIZE_CACHE_DIR' ) ) {
580 // We didn't set a cache.
581 return false;
582 }
583
584 foreach ( array( '', 'js', 'css' ) as $dir ) {
585 if ( ! self::check_cache_dir( AUTOPTIMIZE_CACHE_DIR . $dir ) ) {
586 return false;
587 }
588 }
589 return true;
590 }
591
592 /**
593 * Ensures the specified `$dir` exists and is writeable.
594 * Returns false if that's not the case.
595 *
596 * @param string $dir Directory to check/create.
597 *
598 * @return bool
599 */
600 protected static function check_cache_dir( $dir )
601 {
602 // Try creating the dir if it doesn't exist.
603 if ( ! file_exists( $dir ) ) {
604 @mkdir( $dir, 0775, true ); // @codingStandardsIgnoreLine
605 if ( ! file_exists( $dir ) ) {
606 return false;
607 }
608 }
609
610 // If we still cannot write, bail.
611 if ( ! is_writable( $dir ) ) {
612 return false;
613 }
614
615 // Create an index.html in there to avoid prying eyes!
616 $idx_file = rtrim( $dir, '/\\' ) . '/index.html';
617 if ( ! is_file( $idx_file ) ) {
618 @file_put_contents( $idx_file, '<html><head><meta name="robots" content="noindex, nofollow"></head><body>Generated by <a href="http://wordpress.org/extend/plugins/autoptimize/" rel="nofollow">Autoptimize</a></body></html>' ); // @codingStandardsIgnoreLine
619 }
620
621 return true;
622 }
623
624 /**
625 * Flushes as many page cache plugin's caches as possible.
626 *
627 * @return void
628 */
629 // @codingStandardsIgnoreStart
630 public static function flushPageCache()
631 {
632 if ( function_exists( 'wp_cache_clear_cache' ) ) {
633 if ( is_multisite() ) {
634 $blog_id = get_current_blog_id();
635 wp_cache_clear_cache( $blog_id );
636 } else {
637 wp_cache_clear_cache();
638 }
639 } elseif ( has_action( 'cachify_flush_cache' ) ) {
640 do_action( 'cachify_flush_cache' );
641 } elseif ( function_exists( 'w3tc_pgcache_flush' ) ) {
642 w3tc_pgcache_flush();
643 } elseif ( function_exists( 'wp_fast_cache_bulk_delete_all' ) ) {
644 wp_fast_cache_bulk_delete_all();
645 } elseif ( class_exists( 'WpFastestCache' ) ) {
646 $wpfc = new WpFastestCache();
647 $wpfc->deleteCache();
648 } elseif ( class_exists( 'c_ws_plugin__qcache_purging_routines' ) ) {
649 c_ws_plugin__qcache_purging_routines::purge_cache_dir(); // quick cache
650 } elseif ( class_exists( 'zencache' ) ) {
651 zencache::clear();
652 } elseif ( class_exists( 'comet_cache' ) ) {
653 comet_cache::clear();
654 } elseif ( class_exists( 'WpeCommon' ) ) {
655 // WPEngine cache purge/flush methods to call by default
656 $wpe_methods = array(
657 'purge_varnish_cache',
658 );
659
660 // More agressive clear/flush/purge behind a filter
661 if ( apply_filters( 'autoptimize_flush_wpengine_aggressive', false ) ) {
662 $wpe_methods = array_merge( $wpe_methods, array( 'purge_memcached', 'clear_maxcdn_cache' ) );
663 }
664
665 // Filtering the entire list of WpeCommon methods to be called (for advanced usage + easier testing)
666 $wpe_methods = apply_filters( 'autoptimize_flush_wpengine_methods', $wpe_methods );
667
668 foreach ( $wpe_methods as $wpe_method ) {
669 if ( method_exists( 'WpeCommon', $wpe_method ) ) {
670 WpeCommon::$wpe_method();
671 }
672 }
673 } elseif ( function_exists( 'sg_cachepress_purge_cache' ) ) {
674 sg_cachepress_purge_cache();
675 } elseif ( file_exists( WP_CONTENT_DIR . '/wp-cache-config.php' ) && function_exists( 'prune_super_cache' ) ) {
676 // fallback for WP-Super-Cache
677 global $cache_path;
678 if ( is_multisite() ) {
679 $blog_id = get_current_blog_id();
680 prune_super_cache( get_supercache_dir( $blog_id ), true );
681 prune_super_cache( $cache_path . 'blogs/', true );
682 } else {
683 prune_super_cache( $cache_path . 'supercache/', true );
684 prune_super_cache( $cache_path, true );
685 }
686 }
687 }
688 // @codingStandardsIgnoreEnd
689 }
690