PluginProbe
ezCache / 1.3.10
ezCache v1.3.10
2.6.5 2.6.4 2.6.2 2.6.3 2.6.1 2.6.0 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5 2.2.1 2.2.2 trunk 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.3 1.3.1 1.3.10 1.3.11 All 49 releases
ezcache / includes / Cache.php

Cache.php in ezCache 1.3.10, at includes/Cache.php

1,198 lines 31.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Upress\EzCache;
3
4 use MatthiasMullie\Minify\CSS;
5 use MatthiasMullie\Minify\JS;
6 use RecursiveDirectoryIterator;
7 use RecursiveIteratorIterator;
8 use RegexIterator;
9 use UnexpectedValueException;
10 use Upress\EzCache\BackgroundProcesses\ConvertWebpProcess;
11 use Upress\EzCache\FileOptimizer\CombineGoogleFonts;
12 use Upress\EzCache\FileOptimizer\CssMinifier;
13 use Upress\EzCache\FileOptimizer\CssCombiner;
14 use Upress\EzCache\FileOptimizer\JsMinifier;
15 use Upress\EzCache\FileOptimizer\JsCombiner;
16 use Upress\EzCache\FileOptimizer\WebpConverter;
17 use Upress\EzCache\ThirdParty\Minify_HTML;
18
19 class Cache {
20 protected static $instance;
21 protected $settings;
22 protected $cache_start_time;
23 protected $webp_processor;
24 protected $root_cache_dir = WP_CONTENT_DIR . '/cache/ezcache/';
25
26
27 public static function instance() {
28 if ( ! self::$instance ) {
29 self::$instance = new self();
30 }
31
32 return self::$instance;
33 }
34
35 private function __construct() {
36 $this->settings = Settings::get_settings();
37 $this->webp_processor = new ConvertWebpProcess();
38 $this->cache_start_time = microtime( true );
39 }
40
41 /**
42 * @return string
43 */
44 public function get_default_cache_path() {
45 $hostname = preg_replace( '/:.*$/', '', $this->get_http_host() );
46 return $this->root_cache_dir . $hostname . '/';
47 }
48
49 /**
50 * Get the HTTP host
51 *
52 * @return string
53 */
54 public function get_http_host() {
55 if ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
56 $host = function_exists( 'mb_strtolower' ) ? mb_strtolower( $_SERVER['HTTP_HOST'] ) : strtolower( $_SERVER['HTTP_HOST'] );
57
58 return htmlentities( $host );
59 } elseif ( function_exists( 'get_option' ) ) {
60 return (string) parse_url( get_option( 'home' ), PHP_URL_HOST );
61 }
62
63 return '';
64 }
65
66 /**
67 * Get the logged in cookie value
68 * @return string
69 */
70 public function get_cookies_values() {
71 static $string = '';
72
73 if ( $string != '' ) {
74 return $string;
75 }
76
77 if ( defined( 'COOKIEHASH' ) ) {
78 $cookiehash = preg_quote( constant( 'COOKIEHASH' ) );
79 } else {
80 $cookiehash = '';
81 }
82
83 $regex = "/^wp-postpass_{$cookiehash}|^comment_author_{$cookiehash}";
84 if ( defined( 'LOGGED_IN_COOKIE' ) ) {
85 $regex .= "|^" . preg_quote( constant( 'LOGGED_IN_COOKIE' ) );
86 } else {
87 $regex .= "|^wordpress_logged_in_{$cookiehash}";
88 }
89 $regex .= "/";
90 while ( $key = key( $_COOKIE ) ) {
91 if ( preg_match( $regex, $key ) ) {
92 $string .= $_COOKIE[ $key ] . ",";
93 }
94 next( $_COOKIE );
95 }
96 reset( $_COOKIE );
97
98 if ( $string != '' ) {
99 $string = md5( $string );
100 }
101
102 return $string;
103 }
104
105 /**
106 * Check if the request supports gzip compression
107 *
108 * @return bool
109 */
110 public function gzip_accepted() {
111 if( defined( 'EZCACHE_DISABLE_GZIP' ) && EZCACHE_DISABLE_GZIP ) {
112 return false;
113 }
114
115 return isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' );
116 }
117
118 /**
119 * Check if the request supports webp images
120 *
121 * @return bool
122 */
123 public function webp_accepted() {
124 return isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' );
125 }
126
127 /**
128 * Check if the request comes from the backend
129 *
130 * @return bool
131 */
132 public function is_backend() {
133 if ( is_admin() ) {
134 return true;
135 }
136
137 $script = isset( $_SERVER['PHP_SELF'] ) ? basename( $_SERVER['PHP_SELF'] ) : '';
138 if ( $script !== 'index.php' ) {
139 if ( in_array( $script, array( 'wp-login.php', 'xmlrpc.php', 'wp-cron.php' ) ) ) {
140 return true;
141 } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
142 return true;
143 } elseif ( PHP_SAPI == 'cli' || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
144 return true;
145 }
146 }
147
148 return false;
149 }
150
151 /**
152 * Should we serve the cached file
153 *
154 * @return bool
155 */
156 public function should_serve_cached_data() {
157 if ( defined( 'WP_CLI' ) && WP_CLI ) {
158 return false;
159 }
160 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
161 return false;
162 }
163 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
164 return false;
165 }
166 if ( defined( 'JSON_REQUEST' ) && JSON_REQUEST ) {
167 return false;
168 }
169 if ( defined( 'WC_API_REQUEST' ) && WC_API_REQUEST ) {
170 return false;
171 }
172 if ( defined( 'WP_ADMIN' ) && WP_ADMIN ) {
173 return false;
174 }
175 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
176 return false;
177 }
178 if ( defined( 'WP_USE_THEMES' ) && false === WP_USE_THEMES ) {
179 return false;
180 }
181
182 if ( !isset( $_SERVER['REQUEST_METHOD'] ) || ( isset( $_SERVER['REQUEST_METHOD'] ) && in_array( $_SERVER['REQUEST_METHOD'], [
183 'POST',
184 'PUT',
185 'PATCH',
186 'DELETE'
187 ] ) ) || isset( $_GET['customize_changeset_uuid'] ) || isset( $_POST['wp_customize'] ) ) {
188 return false;
189 }
190
191 $settings = $this->settings;
192
193 if ( $settings->no_cache_known_users && $this->get_cookies_values() ) {
194 return false;
195 }
196
197 if ( $this->is_backend() ) {
198 return false;
199 }
200
201 // Don't cache with variables but the cache is enabled if the visitor comes from an RSS feed, a Facebook action or Google Adsense tracking
202 if ( ( $settings->no_cache_query_params && ! empty( $_GET ) ) || (
203 isset( $_GET['utm_source'], $_GET['utm_medium'], $_GET['utm_campaign'] ) ||
204 isset( $_GET['utm_expid'] ) ||
205 isset( $_GET['fb_action_ids'], $_GET['fb_action_types'], $_GET['fb_source'] ) ||
206 isset( $_GET['gclid'] ) ||
207 isset( $_GET['permalink_name'] ) ||
208 isset( $_GET['lp-variation-id'] ) ||
209 isset( $_GET['lang'] ) ||
210 isset( $_GET['s'] ) ||
211 isset( $_GET['age-verified'] ) ||
212 isset( $_GET['ao_noptimize'] ) ||
213 isset( $_GET['usqp'] ) ||
214 isset( $_GET['woo_ajax'] )
215 ) ) {
216 return false;
217 }
218
219 // Don't cache pages where the rejected cookies are defined
220 if ( ! empty( $settings->rejected_cookies ) ) {
221 $rejected_cookies = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_cookies ), -1, PREG_SPLIT_NO_EMPTY );
222 $rejected_cookies = array_filter( $rejected_cookies );
223
224 if ( preg_match( '#(' . implode( '|', $rejected_cookies ) . ')#', var_export( $_COOKIE, true ) ) ) {
225 return false;
226 }
227 }
228
229 return true;
230 }
231
232 /**
233 * Should we save the cache files.
234 * Most of the checks here have to be run after the page was rendered as they require WordPress.
235 *
236 * @return bool
237 */
238 public function should_save_cache() {
239 if ( ! $this->should_serve_cached_data() ) {
240 return false;
241 }
242
243 $settings = $this->settings;
244
245 // check if we have any errors or otherwise settings preventing caching
246 $error = error_get_last();
247 if ( null !== $error && ( $error['type'] & ( E_ERROR | E_CORE_ERROR | E_PARSE | E_COMPILE_ERROR | E_USER_ERROR ) ) ) {
248 return false;
249 }
250
251 if ( function_exists( 'http_response_code' ) && http_response_code() > 300 ) {
252 return false;
253 }
254
255 if ( is_404() ) {
256 return false;
257 }
258
259 if ( $settings->bypass_cache->single && is_single() ) {
260 return false;
261 }
262 if ( $settings->bypass_cache->pages && is_page() ) {
263 return false;
264 }
265 if ( $settings->bypass_cache->frontpage && is_front_page() ) {
266 return false;
267 }
268 if ( $settings->bypass_cache->home && is_home() ) {
269 return false;
270 }
271 if ( $settings->bypass_cache->archives && is_archive() ) {
272 return false;
273 }
274 if ( $settings->bypass_cache->tag && is_tag() ) {
275 return false;
276 }
277 if ( $settings->bypass_cache->category && is_category() ) {
278 return false;
279 }
280 if ( $settings->bypass_cache->feed && is_feed() ) {
281 return false;
282 }
283 if ( $settings->bypass_cache->search && is_search() ) {
284 return false;
285 }
286 if ( $settings->bypass_cache->author && is_author() ) {
287 return false;
288 }
289 if ( function_exists( 'is_checkout' ) && is_checkout() ) {
290 return false;
291 }
292 if ( function_exists( 'is_cart' ) && is_cart() ) {
293 return false;
294 }
295
296 if ( is_robots() || get_query_var( 'sitemap' ) || get_query_var( 'xsl' ) || get_query_var( 'xml_sitemap' ) ) {
297 return false;
298 }
299
300 if ( isset( $_GET['preview'] ) || isset( $_POST['wp_customize'] ) ) {
301 return false;
302 }
303
304 if ( get_post_meta( get_the_ID(), '_ezcache_do_not_cache_post', true ) ) {
305 return false;
306 }
307
308 // check useragent
309 $rejected_useragents = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_user_agent ), -1, PREG_SPLIT_NO_EMPTY );
310 $rejected_useragents = array_filter( $rejected_useragents );
311 if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
312 foreach( $rejected_useragents as $ua ) {
313 if ( empty( $ua ) ) continue;
314
315 if ( false !== strpos( $_SERVER['HTTP_USER_AGENT'], trim( $ua ) ) ) {
316 return false;
317 }
318 }
319 }
320
321 // check URL
322 $rejected_uris = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_uri ), -1, PREG_SPLIT_NO_EMPTY );
323 $rejected_uris = array_filter( $rejected_uris );
324 $domain = untrailingslashit( home_url() );
325 if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
326 foreach( $rejected_uris as $url ) {
327 $url = str_replace( $domain, '', $url );
328 $url = '/' . trim( $url, '/' );
329 $url = str_replace( ['\/*', '*'], ['\/?.*?', '.*?'], preg_quote( $url, '/' ) );
330 $url = str_replace( '\/\.*?', '\/.*?', $url );
331 if ( @preg_match( "/^{$url}\/?$/u", urldecode( $_SERVER['REQUEST_URI'] ) ) ) {
332 return false;
333 }
334 }
335 }
336
337 return true;
338 }
339
340 /**
341 * Get the mobile browser name
342 *
343 * @return string
344 */
345 public function detect_mobile() {
346 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
347 return '';
348 }
349
350 $mobile_browsers = apply_filters( 'ezcache_mobile_browsers', [
351 '2.0 MMP',
352 '240x320',
353 '400X240',
354 'AvantGo',
355 'BlackBerry',
356 'Blazer',
357 'Cellphone',
358 'Danger',
359 'DoCoMo',
360 'Elaine/3.0',
361 'EudoraWeb',
362 'Googlebot-Mobile',
363 'hiptop',
364 'IEMobile',
365 'KYOCERA/WX310K',
366 'LG/U990',
367 'MIDP-2.',
368 'MMEF20',
369 'MOT-V',
370 'NetFront',
371 'Newt',
372 'Nintendo Wii',
373 'Nitro',
374 'Nokia',
375 'Opera Mini',
376 'Palm',
377 'PlayStation Portable',
378 'portalmmm',
379 'Proxinet',
380 'ProxiNet',
381 'SHARP-TQ-GX10',
382 'SHG-i900',
383 'Small',
384 'SonyEricsson',
385 'Symbian OS',
386 'SymbianOS',
387 'TS21i-10',
388 'UP.Browser',
389 'UP.Link',
390 'webOS',
391 'Windows CE',
392 'WinWAP',
393 'YahooSeeker/M1A1-R2D2',
394 'iPhone',
395 'iPod',
396 'iPad',
397 'Android',
398 'BlackBerry9530',
399 'LG-TU915 Obigo',
400 'LGE VX',
401 'webOS',
402 'Nokia5800'
403 ] );
404 $user_agent = strtolower( $_SERVER['HTTP_USER_AGENT'] );
405 foreach ( $mobile_browsers as $browser ) {
406 if ( strstr( $user_agent, trim( strtolower( $browser ) ) ) ) {
407 return $user_agent;
408 }
409 }
410
411 if ( isset( $_SERVER['HTTP_X_WAP_PROFILE'] ) ) {
412 return $_SERVER['HTTP_X_WAP_PROFILE'];
413 }
414
415 if ( isset( $_SERVER['HTTP_PROFILE'] ) ) {
416 return $_SERVER['HTTP_PROFILE'];
417 }
418
419 $browser_prefixes = apply_filters( 'ezcache_mobile_browser_prefixes', [
420 'w3c',
421 'w3c-',
422 'acs-',
423 'alav',
424 'alca',
425 'amoi',
426 'audi',
427 'avan',
428 'benq',
429 'bird',
430 'blac',
431 'blaz',
432 'brew',
433 'cell',
434 'cldc',
435 'cmd-',
436 'dang',
437 'doco',
438 'eric',
439 'hipt',
440 'htc_',
441 'inno',
442 'ipaq',
443 'ipod',
444 'jigs',
445 'kddi',
446 'keji',
447 'leno',
448 'lg-c',
449 'lg-d',
450 'lg-g',
451 'lge-',
452 'lg/u',
453 'maui',
454 'maxo',
455 'midp',
456 'mits',
457 'mmef',
458 'mobi',
459 'mot-',
460 'moto',
461 'mwbp',
462 'nec-',
463 'newt',
464 'noki',
465 'palm',
466 'pana',
467 'pant',
468 'phil',
469 'play',
470 'port',
471 'prox',
472 'qwap',
473 'sage',
474 'sams',
475 'sany',
476 'sch-',
477 'sec-',
478 'send',
479 'seri',
480 'sgh-',
481 'shar',
482 'sie-',
483 'siem',
484 'smal',
485 'smar',
486 'sony',
487 'sph-',
488 'symb',
489 't-mo',
490 'teli',
491 'tim-',
492 'tosh',
493 'tsm-',
494 'upg1',
495 'upsi',
496 'vk-v',
497 'voda',
498 'wap-',
499 'wapa',
500 'wapi',
501 'wapp',
502 'wapr',
503 'webc',
504 'winw',
505 'winw',
506 'xda',
507 'xda-'
508 ] );
509 foreach ( $browser_prefixes as $prefix ) {
510 if ( substr( $user_agent, 0, 4 ) == $prefix ) {
511 return $prefix;
512 }
513 }
514
515 $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( $_SERVER['HTTP_ACCEPT'] ) : '';
516 if ( strpos( $accept, 'wap' ) !== false ) {
517 return 'wap';
518 }
519
520 if ( isset( $_SERVER['ALL_HTTP'] ) && false !== strpos( strtolower( $_SERVER['ALL_HTTP'] ), 'operamini' ) ) {
521 return 'operamini';
522 }
523
524 return '';
525 }
526
527 /**
528 * Search & replace in a string
529 *
530 * @param string|string[] $search
531 * @param string $subject
532 *
533 * @return string
534 */
535 public function deep_replace( $search, $subject ) {
536 $subject = (string) $subject;
537
538 $count = 1;
539 while ( $count ) {
540 $subject = str_replace( $search, '', $subject, $count );
541 }
542
543 return $subject;
544 }
545
546 /**
547 * Get the cache directory URL for the current post
548 *
549 * @param int $post_id
550 *
551 * @return mixed|string
552 */
553 public function get_current_url_cache_dir( $post_id = 0 ) {
554 static $url_cache_dir = array();
555
556 if ( isset( $url_cache_dir[ $post_id ] ) ) {
557 return $url_cache_dir[ $post_id ];
558 }
559
560 $uri = strtolower( $_SERVER['REQUEST_URI'] );
561
562 $DONOTREMEMBER = 0;
563 if ( 0 !== $post_id ) {
564 $site_url = site_url();
565 $permalink = get_permalink( $post_id );
566 if ( false === strpos( $permalink, $site_url ) ) {
567 $DONOTREMEMBER = 1;
568 if ( preg_match( '`^(https?:)?//([^/]+)(/.*)?$`i', $permalink, $matches ) ) {
569 $uri = isset( $matches[3] ) ? $matches[3] : '';
570 } elseif ( preg_match( '`^/([^/]+)(/.*)?$`i', $permalink, $matches ) ) {
571 $uri = $permalink;
572 } else {
573 $uri = '';
574 }
575 } else {
576 $uri = str_replace( $site_url, '', $permalink );
577 if ( 0 !== strpos( $uri, '/' ) ) {
578 $uri = '/' . $uri;
579 }
580 }
581 }
582
583 $uri = $this->deep_replace(
584 [
585 '..',
586 '\\',
587 'index.php'
588 ],
589 preg_replace(
590 '/[ <>\'\"\r\n\t\(\)]/',
591 '',
592 preg_replace( "/(\?.*)?(#.*)?$/", '', $uri )
593 )
594 );
595
596 $uri = md5( $uri );
597 $dir = str_replace( '..', '', str_replace( '//', '/', $uri . '/' ) );
598
599 if ( $DONOTREMEMBER == 0 ) {
600 $url_cache_dir[ $post_id ] = $dir;
601 }
602
603 return $dir;
604 }
605
606 /**
607 * Get the cache directory path
608 *
609 * @param int $postid
610 *
611 * @return string
612 */
613 public function get_real_cache_dir( $postid = 0 ) {
614 return $this->get_default_cache_path() . $this->get_current_url_cache_dir( $postid );
615 }
616
617 /**
618 * Get the full cache file path
619 *
620 * @param int $postid
621 *
622 * @return string
623 */
624 public function get_cache_file_path( $postid = 0 ) {
625 return $this->get_real_cache_dir( $postid ) . $this->get_cache_filename();
626 }
627
628 /**
629 * Get the filename for the cached file
630 *
631 * @return string
632 */
633 public function get_cache_filename() {
634 $settings = $this->settings;
635
636 // Add support for https and http caching
637 // also supports https requests coming from an nginx reverse proxy
638 $is_https = ( ( isset( $_SERVER['HTTPS'] ) && 'on' == strtolower( $_SERVER['HTTPS'] ) ) || ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == strtolower( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) );
639 $extra_str = $is_https ? '-https' : '';
640
641 if ( $settings->separate_mobile_cache ) {
642 $mobile_ua = $this->detect_mobile();
643 if ( ! empty( $mobile_ua ) ) {
644 $extra_str .= '-mobile';
645 }
646 }
647
648 if ( $settings->enable_webp_support && $this->webp_accepted()) {
649 $extra_str .= '-webp';
650 }
651
652 $filename = 'index';
653 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
654 $filename = md5( $_SERVER['QUERY_STRING'] );
655 }
656
657 return $filename . $extra_str . '.html';
658 }
659
660 /**
661 * Check if we have a cached file and serve it
662 */
663 public function maybe_serve_cached_data() {
664 if ( ! $this->should_serve_cached_data() ) {
665 return;
666 }
667
668 $cache_file = $this->get_cache_file_path();
669 $gzip_accepted = $this->gzip_accepted();
670
671 $cache_file = $cache_file . '.gz';
672 $filesize = file_exists( $cache_file ) ? @filesize( $cache_file ) : false;
673
674 if ( ! $filesize ) {
675 // the file is empty, we have nothing to serve
676 return;
677 }
678
679 header( "X-Cached-With: ezCache" );
680 header( "Vary: Accept-Encoding, Cookie" );
681 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_file ) ) . ' GMT' );
682
683 // Getting If-Modified-Since headers sent by the client.
684 if ( function_exists( 'apache_request_headers' ) ) {
685 $headers = apache_request_headers();
686 $http_if_modified_since = ( isset( $headers['If-Modified-Since'] ) ) ? $headers['If-Modified-Since'] : '';
687 } else {
688 $http_if_modified_since = ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : '';
689 }
690
691 // Checking if the client is validating his cache and if it is current.
692 if ( $http_if_modified_since && ( strtotime( $http_if_modified_since ) === @filemtime( $cache_file ) ) ) {
693 // Client's cache is current, so we just respond '304 Not Modified'.
694 header( $_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified', true, 304 );
695 exit;
696 }
697
698 // Serve the cache if file isn't store in the client browser cache.
699 // if the browser does not support gzip read the file and output it without gzip encoding
700 if ( ! $gzip_accepted ) {
701 readgzfile( $cache_file );
702 exit;
703 }
704
705 // otherwise output the gzipped file as-is
706 header( "Content-Length: {$filesize}" );
707 header( "Content-Encoding: gzip" );
708 readfile( $cache_file );
709 exit;
710 }
711
712 public function do_frontend_optimizations() {
713 if ( ! $this->should_serve_cached_data() ) {
714 return false;
715 }
716
717 $settings = $this->settings;
718
719 if ( isset( $settings->disable_wp_emoji ) && $settings->disable_wp_emoji ) {
720 add_action( 'init', function () {
721 remove_action( 'admin_print_styles', 'print_emoji_styles' );
722 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
723 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
724 remove_action( 'wp_print_styles', 'print_emoji_styles' );
725 remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
726 remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
727 remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
728 add_filter( 'emoji_svg_url', '__return_false' );
729 }, 999 );
730 }
731 }
732
733 /**
734 * Write cache file if we need to
735 */
736 public function maybe_write_cache_file() {
737 if ( ! $this->should_serve_cached_data() ) {
738 return;
739 }
740
741 ob_start( [ $this, 'optimize_and_write_cache_file' ] );
742 }
743
744 /**
745 * Optimize output and write the buffer to the cache file
746 *
747 * @param string $buffer
748 *
749 * @return string
750 */
751 public function optimize_and_write_cache_file( $buffer ) {
752 global $wpdb;
753
754 // we need these check to run after WordPress is finished preparing the page
755 if ( ! $this->should_save_cache() ) {
756 return $buffer;
757 }
758
759 $real_cache_dir = $this->get_real_cache_dir();
760 $cache_file = $this->get_cache_file_path() . '.gz';
761 $asset_cache_dir = $this->get_default_cache_path() . 'min/';
762 $asset_cache_url = trailingslashit( trailingslashit( get_home_url() ) . trim( str_replace( dirname( WP_CONTENT_DIR ), '', $asset_cache_dir ), '/' ) );
763 $settings = $this->settings;
764
765 if ( $settings->optimize_google_fonts ) {
766 $optimizer = new CombineGoogleFonts();
767 $buffer = $optimizer->optimize( $buffer );
768 }
769
770 if ( $settings->minify_css ) {
771 if ( $settings->combine_css ) {
772 $optimizer = new CssCombiner( $asset_cache_dir, $asset_cache_url );
773 } else {
774 $optimizer = new CssMinifier( $asset_cache_dir, $asset_cache_url );
775 }
776
777 $buffer = $optimizer->optimize( $buffer );
778 }
779
780 if ( $settings->minify_js ) {
781 if ( $settings->combine_head_js ) {
782 $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'head' );
783 $buffer = $optimizer->optimize( $buffer );
784 }
785
786 if ( $settings->combine_body_js ) {
787 $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'body' );
788 $buffer = $optimizer->optimize( $buffer );
789 }
790
791 if( ! $settings->combine_head_js && ! $settings->combine_body_js ) {
792 $optimizer = new JsMinifier( $asset_cache_dir, $asset_cache_url );
793 $buffer = $optimizer->optimize( $buffer );
794 }
795 }
796
797 if ( $settings->minify_html ) {
798 $buffer = Minify_HTML::minify( $buffer, [
799 'htmlCleanComments' => $settings->minify_html_comments,
800
801 'cssMinifier' => function( $css ) use ($settings) {
802 if ( ! $settings->minify_inline_css ) {
803 return $css;
804 }
805
806 $minifier = new CSS( $css );
807 return $minifier->minify();
808 },
809
810 'jsMinifier' => function( $js ) use ($settings) {
811 if ( ! $settings->minify_inline_js ) {
812 return $js;
813 }
814
815 $minifier = new JS( $js );
816 return $minifier->minify();
817 },
818 ] );
819 }
820
821 if ( $settings->enable_webp_support && $this->webp_accepted() ) {
822 $optimizer = new WebpConverter( $real_cache_dir, $cache_file, $this->webp_processor, $wpdb );
823 $buffer = $optimizer->optimize( $buffer );
824 }
825
826 $buffer = trim( $buffer );
827 if ( empty( $buffer ) ) {
828 error_log( 'ezCache will not save cache file for a blank page' );
829 return $buffer;
830 }
831
832 if ( ! apply_filters( 'wp_bost_hide_cache_time_comment', false ) ) {
833 $total_time = number_format( microtime( true ) - $this->cache_start_time, 2 );
834 $buffer .= "\n<!-- Cached by ezCache -->\n<!-- Cache created in {$total_time}s -->";
835 }
836
837 $buffer = apply_filters( 'ezcache_before_save_cache', $buffer );
838
839 if ( ! file_exists( $real_cache_dir ) ) {
840 if ( ! @wp_mkdir_p( $real_cache_dir ) ) {
841 error_log( 'ezCache could not create directory ' . $real_cache_dir );
842 return $buffer;
843 }
844 }
845
846 // write gzipped file
847 $handle = @fopen( $cache_file, 'w' );
848
849 if ( $handle && @flock( $handle, LOCK_EX ) ) {
850 fwrite( $handle, gzencode( $buffer, 6, FORCE_GZIP ) );
851 flock( $handle, LOCK_UN );
852 } else {
853 error_log( 'ezCache could not write to ' . str_replace( ABSPATH, '', $cache_file ) );
854 }
855
856 if ( $handle ) {
857 fclose( $handle );
858 }
859
860 $this->webp_processor->dispatch();
861
862 return $buffer;
863 }
864
865 /**
866 * Delete a path recursively
867 *
868 * @param string $path
869 */
870 public function rmdir_recursive( $path ) {
871 if ( ! file_exists( $path ) ) {
872 return;
873 }
874
875 $files = glob( $path . '/*' );
876 foreach ( $files as $file ) {
877 if ( file_exists( $file) && is_dir( $file ) ) {
878 $this->rmdir_recursive( $file );
879 } elseif ( file_exists( $file) ) {
880 unlink( $file );
881 }
882 }
883
884 rmdir( $path );
885 }
886
887 /**
888 * Preload the homepage and immediately create cache for it
889 */
890 public function preload_homepage() {
891 $desktop_ua = apply_filters(
892 'ezcache_desktop_useragent',
893 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36 (ezCache Preload)'
894 );
895 $mobile_ua = apply_filters(
896 'ezcache_mobile_useragent',
897 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/ 604.1.21 (KHTML, like Gecko) Version/ 12.0 Mobile/17A6278a Safari/602.1.26 (ezCache Preload)'
898 );
899
900 wp_safe_remote_get( home_url(), [
901 'user-agent' => $desktop_ua,
902 'timeout' => 0.1,
903 ] );
904
905 wp_safe_remote_get( home_url(), [
906 'user-agent' => $mobile_ua,
907 'timeout' => 0.1,
908 ] );
909 }
910
911 function delete_missing_webp_images( $delete_all = false ) {
912 global $wpdb;
913
914 // delete the actual files
915 $ids = [0];
916 $where = $delete_all ? '' : "WHERE `status` = 'completed'";
917 $images = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}ezcache_webp_images` {$where}" );
918 foreach( $images as $image ) {
919 if ( ! file_exists( $image->webp_path ) ) {
920 $ids[] = $image->id;
921 } elseif ( ( $delete_all || ! file_exists( $image->path ) ) && file_exists( $image->webp_path ) ) {
922 unlink( $image->webp_path );
923 $ids[] = $image->id;
924 }
925 }
926
927 // clean the database
928 $wpdb->query(
929 $wpdb->prepare(
930 "DELETE FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'failed' OR `id` IN ( " . substr( str_repeat( "%d, ", count( $ids ) ), 0, -2 ) . " )",
931 $ids
932 )
933 );
934
935 $wpdb->query( "OPTIMIZE TABLE `{$wpdb->prefix}ezcache_webp_images`" );
936 }
937
938 function delete_all_webp_images() {
939 $this->delete_missing_webp_images( true );
940 }
941
942 /**
943 * Clear all caches
944 */
945 public function clear_cache() {
946 $cache_dir = $this->get_default_cache_path();
947
948 $this->rmdir_recursive( $cache_dir );
949 @wp_mkdir_p( $cache_dir );
950
951 if ( $this->settings->cache_clear_delete_webp ) {
952 $this->delete_all_webp_images();
953 } else {
954 $this->delete_missing_webp_images();
955 }
956
957 $this->preload_homepage();
958 }
959
960 /**
961 * Clear cache for a single post
962 *
963 * @param int $post_id
964 */
965 public function clear_cache_single( $post_id ) {
966 $real_cache_dir = $this->get_real_cache_dir( $post_id );
967
968 $this->rmdir_recursive( $real_cache_dir );
969 }
970
971 /**
972 * Delete expired cache
973 */
974 public function clear_expired_cache() {
975 $settings = $this->settings;
976
977 try {
978 $dir = new RecursiveDirectoryIterator( $this->root_cache_dir );
979 $ite = new RecursiveIteratorIterator( $dir );
980 $files = new RegexIterator( $ite, '/^.+\.(?:gz|html|js|css)$/i', RegexIterator::GET_MATCH );
981 } catch( UnexpectedValueException $ex) {
982 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
983 $files = [];
984 } else {
985 throw $ex;
986 }
987 }
988
989 foreach($files as $file) {
990 if ( is_array( $file ) ) {
991 $file = array_shift( $file );
992 }
993
994 $stats = stat( $file );
995 if ( $stats['mtime'] > ( time() - $settings->cache_lifetime ) ) {
996 // skip not expired files
997 continue;
998 }
999
1000 @unlink( $file );
1001 }
1002 }
1003
1004 /**
1005 * Get caching statistics and file sizes
1006 * @return array
1007 */
1008 public function get_cache_stats() {
1009 $settings = $this->settings;
1010 $cache_dir = $this->get_default_cache_path();
1011
1012 try {
1013 $dir = new RecursiveDirectoryIterator( $cache_dir );
1014 $ite = new RecursiveIteratorIterator( $dir );
1015 $files = new RegexIterator( $ite, '/^.+\.(?:gz|html|css|js)$/i', RegexIterator::GET_MATCH );
1016 } catch( UnexpectedValueException $ex) {
1017 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
1018 $files = [];
1019 } else {
1020 throw $ex;
1021 }
1022 }
1023
1024 $raw_data = [];
1025
1026 $mobile_count = 0;
1027 $mobile_size = 0;
1028 $mobile_expired_count = 0;
1029 $mobile_expired_size = 0;
1030 $desktop_count = 0;
1031 $desktop_size = 0;
1032 $desktop_expired_count = 0;
1033 $desktop_expired_size = 0;
1034 $js_count = 0;
1035 $js_size = 0;
1036 $js_expired_count = 0;
1037 $js_expired_size = 0;
1038 $css_count = 0;
1039 $css_size = 0;
1040 $css_expired_count = 0;
1041 $css_expired_size = 0;
1042
1043 foreach($files as $file) {
1044 if ( is_array( $file ) ) {
1045 $file = array_shift( $file );
1046 }
1047
1048 $stats = stat( $file );
1049 $expired = $stats['mtime'] <= (time() - $settings->cache_lifetime);
1050
1051 $raw_data[] = [
1052 'path' => $file,
1053 'stats' => $stats,
1054 'expired' => $expired,
1055 ];
1056
1057 if ( preg_match( '/^.+?-mobile\.html(\.gz)?$/i', $file ) ) {
1058 if ( ! $expired ) {
1059 $mobile_count++;
1060 $mobile_size += $stats['size'];
1061 } else {
1062 $mobile_expired_count++;
1063 $mobile_expired_size += $stats['size'];
1064 }
1065 } elseif( preg_match( '/\.css$/i', $file ) ) {
1066 if ( $expired ) {
1067 $css_expired_count++;
1068 $css_expired_size += $stats['size'];
1069 } else {
1070 $css_count ++;
1071 $css_size += $stats['size'];
1072 }
1073 } elseif( preg_match( '/\.js$/i', $file ) ) {
1074 if ( $expired ) {
1075 $js_expired_count++;
1076 $js_expired_size += $stats['size'];
1077 } else {
1078 $js_count ++;
1079 $js_size += $stats['size'];
1080 }
1081 } else {
1082 if ( ! $expired ) {
1083 $desktop_count++;
1084 $desktop_size += $stats['size'];
1085 } else {
1086 $desktop_expired_count++;
1087 $desktop_expired_size += $stats['size'];
1088 }
1089 }
1090 }
1091
1092 // we want to count only the number of pages which have cache, but we have 2 files for each page
1093 $mobile_count = $mobile_count / 2;
1094 $desktop_count = $desktop_count / 2;
1095
1096
1097 global $wpdb;
1098 $webp_images = 0;
1099 $webp_images_size = 0;
1100 $webp_images_original_size = 0;
1101
1102 $results = $wpdb->get_row( "SELECT COUNT(*) AS total, SUM(`original_size`) AS total_original_size, SUM(`webp_size`) AS total_webp_size FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'completed'" );
1103 if ( $results ) {
1104 $webp_images = intval( $results->total );
1105 $webp_images_size = intval( $results->total_webp_size );
1106 $webp_images_original_size = intval( $results->total_original_size );
1107 }
1108
1109 return compact( 'webp_images', 'webp_images_original_size', 'webp_images_size', 'mobile_count', 'desktop_count', 'mobile_expired_count', 'desktop_expired_count', 'mobile_size', 'desktop_size', 'mobile_expired_size', 'desktop_expired_size' ,'css_size', 'css_count', 'js_count', 'js_size', 'css_expired_count', 'css_expired_size', 'js_expired_count', 'js_expired_size' );
1110 }
1111
1112 /**
1113 * Get the status of the cache
1114 *
1115 * @return array
1116 */
1117 public function get_status() {
1118 global $wpdb;
1119
1120 $wp_cache_enabled = defined( 'WP_CACHE' ) && WP_CACHE;
1121 $adv_cache_exists = file_exists( WP_CONTENT_DIR . '/advanced-cache.php' );
1122 $correct_advanced_cache = $adv_cache_exists && strpos( file_get_contents( WP_CONTENT_DIR . '/advanced-cache.php' ), 'ezCache Advanced Cache' ) !== false;
1123 $webp_table_exists = ! is_null( $wpdb->get_row( "SHOW TABLES LIKE '{$wpdb->prefix}ezcache_webp_images'" ) );
1124
1125 return [
1126 'cache_enabled' => $wp_cache_enabled,
1127 'adv_cache_exists' => $adv_cache_exists,
1128 'correct_cache_exists' => $correct_advanced_cache,
1129 'webp_table_exists' => $webp_table_exists,
1130 ];
1131 }
1132
1133 /**
1134 * Get a path by the URL
1135 *
1136 * @param string $url
1137 *
1138 * @return bool|string
1139 */
1140 public static function url_to_path( $url ) {
1141 if ( preg_match( '/^\/\//i',$url ) ) {
1142 $url = 'http' . (is_ssl() ? 's' : '') . ':' . $url;
1143 }
1144
1145 $root_dir = trailingslashit( dirname( WP_CONTENT_DIR ) );
1146 $root_url = str_replace( wp_basename( WP_CONTENT_DIR ), '', WP_CONTENT_URL );
1147 $url_host = wp_parse_url( $url, PHP_URL_HOST );
1148
1149 if ( null === $url_host ) {
1150 $subdir_levels = substr_count( preg_replace( '/https?:\/\//','', site_url() ), '/' );
1151 $url = site_url() . str_repeat( '/..', $subdir_levels ) . $url;
1152 }
1153
1154 $root_url = preg_replace( '/^https?:/', '', $root_url );
1155 $url = preg_replace( '/^https?:/', '', $url );
1156 $file = str_replace( $root_url, $root_dir, $url );
1157 $file = self::realpath( $file );
1158
1159 if ( ! file_exists( $file ) ) {
1160 return false;
1161 }
1162
1163 return $file;
1164 }
1165
1166 /**
1167 * Returns canonicalized absolute pathname.
1168 * The resulting path will have no symbolic link, '/./' or '/../' components.
1169 * Same as the defautl PHP realpath() function but works even when the files does not exist.
1170 *
1171 * @see \realpath()
1172 *
1173 * @param string $file The path being checked.
1174 *
1175 * @return string
1176 */
1177 public static function realpath( $file ) {
1178 $path = array();
1179
1180 foreach ( explode( '/', $file ) as $part ) {
1181 if ( '' === $part || '.' === $part ) {
1182 continue;
1183 }
1184
1185 if ( '..' !== $part ) {
1186 array_push( $path, $part );
1187 }
1188 elseif ( count( $path ) > 0 ) {
1189 array_pop( $path );
1190 }
1191 }
1192
1193 $prefix = 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ? '' : '/';
1194
1195 return $prefix . join( '/', $path );
1196 }
1197 }
1198