PluginProbe
ezCache / 2.5
ezCache v2.5
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 2.5, at includes/Cache.php

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