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

1,576 lines 41.7 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 /**
696 * Lowercased list of query-string parameters to ignore when building the
697 * cache key. Only meaningful when the ignore_query_params setting is on.
698 *
699 * @return array
700 */
701 private function get_ignored_query_params() {
702 static $cached = null;
703 if ( null !== $cached ) {
704 return $cached;
705 }
706 $raw = isset( $this->settings->ignored_query_params_list ) ? (string) $this->settings->ignored_query_params_list : '';
707 $list = preg_split( '/[\s,]+/', strtolower( $raw ), -1, PREG_SPLIT_NO_EMPTY );
708
709 /**
710 * Filters the query-string parameters ignored when building the cache key.
711 *
712 * @param array $list Lowercased parameter names.
713 */
714 $list = apply_filters( 'ezcache_ignored_query_params', $list );
715 $cached = array_values( array_unique( array_map( 'strtolower', (array) $list ) ) );
716
717 return $cached;
718 }
719
720 /**
721 * Normalize a raw query string for cache-key purposes. When the
722 * ignore_query_params feature is on, drop the ignored (tracking) parameters
723 * and sort the rest so different orderings and tracking values map to the
724 * same cache entry. Returns '' when nothing meaningful remains.
725 *
726 * @param string $query_string
727 * @return string
728 */
729 private function normalize_query_string( $query_string ) {
730 if ( '' === (string) $query_string ) {
731 return '';
732 }
733 if ( empty( $this->settings->ignore_query_params ) ) {
734 return $query_string; // feature off — behaviour unchanged
735 }
736 parse_str( (string) $query_string, $params );
737 if ( empty( $params ) ) {
738 return '';
739 }
740 $ignored = $this->get_ignored_query_params();
741 foreach ( array_keys( $params ) as $key ) {
742 if ( $this->query_param_is_ignored( strtolower( $key ), $ignored ) ) {
743 unset( $params[ $key ] );
744 }
745 }
746 if ( empty( $params ) ) {
747 return '';
748 }
749 ksort( $params );
750
751 return http_build_query( $params );
752 }
753
754 /**
755 * Whether a (lowercased) query parameter name matches the ignore list.
756 * Supports exact names and trailing-"*" prefix patterns (e.g. "utm_*").
757 * A bare "*" is skipped to avoid accidentally dropping every parameter.
758 *
759 * @param string $key Lowercased parameter name.
760 * @param array $ignored Lowercased ignore patterns.
761 * @return bool
762 */
763 private function query_param_is_ignored( $key, $ignored ) {
764 foreach ( $ignored as $pattern ) {
765 if ( '' === $pattern || '*' === $pattern ) {
766 continue;
767 }
768 if ( '*' === substr( $pattern, -1 ) ) {
769 $prefix = substr( $pattern, 0, -1 );
770 if ( '' !== $prefix && 0 === strpos( $key, $prefix ) ) {
771 return true;
772 }
773 } elseif ( $key === $pattern ) {
774 return true;
775 }
776 }
777
778 return false;
779 }
780
781 /**
782 * Build the full-page (Redis) cache URL for the current request, applying
783 * the same query-string normalization used for the disk cache key.
784 *
785 * @return string
786 */
787 private function build_fullpage_url() {
788 $scheme = ( is_ssl() ? 'https://' : 'http://' );
789 $host = $_SERVER['HTTP_HOST'] ?? '';
790 $uri = $_SERVER['REQUEST_URI'] ?? '/';
791 $path = $uri;
792 $qs = '';
793 $pos = strpos( $uri, '?' );
794 if ( false !== $pos ) {
795 $path = substr( $uri, 0, $pos );
796 $qs = substr( $uri, $pos + 1 );
797 }
798 $norm = $this->normalize_query_string( $qs );
799
800 return $scheme . $host . $path . ( '' !== $norm ? '?' . $norm : '' );
801 }
802
803 public function get_cache_filename() {
804 $settings = $this->settings;
805
806 // Add support for https and http caching
807 // also supports https requests coming from an nginx reverse proxy
808 $is_https = ( ( isset( $_SERVER['HTTPS'] ) && 'on' == strtolower( $_SERVER['HTTPS'] ) ) || ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == strtolower( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) );
809 $extra_str = $is_https ? '-https' : '';
810
811 if ( $settings->separate_mobile_cache ) {
812 $mobile_ua = $this->detect_mobile();
813 if ( ! empty( $mobile_ua ) ) {
814 $extra_str .= '-mobile';
815 }
816 }
817
818 if ( $settings->enable_webp_support && $this->webp_accepted() ) {
819 $extra_str .= '-webp';
820 }
821
822 $filename = 'index';
823 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
824 $normalized = $this->normalize_query_string( $_SERVER['QUERY_STRING'] );
825 // When every parameter was ignored, fall back to 'index' so the
826 // request maps to the same cache entry as the clean URL.
827 if ( '' !== $normalized ) {
828 $filename = md5( $normalized );
829 }
830 }
831
832 return $filename . $extra_str . '.html';
833 }
834
835 /**
836 * Check if we have a cached file and serve it
837 */
838 public function maybe_serve_cached_data() {
839 if ( ! $this->should_serve_cached_data() ) {
840 return;
841 }
842
843 // ── Redis Full-Page Cache fast path ────────────────────
844 // When enabled, try Redis first. A hit is sub-millisecond and skips
845 // the disk read entirely. On miss we fall through to the disk path
846 // below (and the response handler in maybe_write_cache_file will
847 // populate Redis for next time).
848 if (
849 ! empty( $this->settings->enable_redis_fullpage )
850 && class_exists( '\\Upress\\EzCache\\RedisObjectCache' )
851 ) {
852 $current_url = $this->build_fullpage_url();
853 $cached_html = \Upress\EzCache\RedisObjectCache::get_page( $current_url );
854 if ( false !== $cached_html && '' !== $cached_html ) {
855 header( 'X-Cached-With: ezCache (Redis)' );
856 header( 'Vary: Accept-Encoding, Cookie' );
857 echo $cached_html;
858 exit;
859 }
860 }
861
862 $cache_file = $this->get_cache_file_path();
863 $gzip_accepted = $this->gzip_accepted();
864
865 $cache_file = $cache_file . '.gz';
866 $filesize = file_exists( $cache_file ) ? @filesize( $cache_file ) : false;
867
868 if ( ! $filesize ) {
869 // the file is empty, we have nothing to serve
870 return;
871 }
872
873 header( "X-Cached-With: ezCache" );
874 header( "Vary: Accept-Encoding, Cookie" );
875 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_file ) ) . ' GMT' );
876
877 // Getting If-Modified-Since headers sent by the client.
878 if ( function_exists( 'apache_request_headers' ) ) {
879 $headers = apache_request_headers();
880 $http_if_modified_since = ( isset( $headers['If-Modified-Since'] ) ) ? $headers['If-Modified-Since'] : '';
881 } else {
882 $http_if_modified_since = ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : '';
883 }
884
885 // Checking if the client is validating his cache and if it is current.
886 if ( $http_if_modified_since && ( strtotime( $http_if_modified_since ) === @filemtime( $cache_file ) ) ) {
887 // Client's cache is current, so we just respond '304 Not Modified'.
888 header( $_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified', true, 304 );
889 exit;
890 }
891
892 // Serve the cache if file isn't store in the client browser cache.
893 // if the browser does not support gzip read the file and output it without gzip encoding
894 if ( ! $gzip_accepted ) {
895 readgzfile( $cache_file );
896 exit;
897 }
898
899 // otherwise output the gzipped file as-is
900 header( "Content-Length: {$filesize}" );
901 header( "Content-Encoding: gzip" );
902 readfile( $cache_file );
903 exit;
904 }
905
906 public function do_frontend_optimizations() {
907 if ( ! $this->should_serve_cached_data() ) {
908 return;
909 }
910
911 $settings = $this->settings;
912
913 if ( isset( $settings->disable_wp_emoji ) && $settings->disable_wp_emoji ) {
914 add_action( 'init', function () {
915 remove_action( 'admin_print_styles', 'print_emoji_styles' );
916 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
917 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
918 remove_action( 'wp_print_styles', 'print_emoji_styles' );
919 remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
920 remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
921 remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
922 add_filter( 'emoji_svg_url', '__return_false' );
923 }, 999 );
924 }
925
926
927 if ( ! empty( $settings->critical_css ) ) {
928 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_critical_css' ], PHP_INT_MAX );
929 }
930 }
931
932 public function enqueue_critical_css() {
933 wp_register_style( 'ezcache-critical-css', false );
934 wp_enqueue_style( 'ezcache-critical-css' );
935 wp_add_inline_style( 'ezcache-critical-css', $this->settings->critical_css );
936 }
937
938 /**
939 * Write cache file if we need to
940 * @noinspection PhpUnused
941 */
942 public function maybe_write_cache_file() {
943 if ( ! $this->should_serve_cached_data() ) {
944 return;
945 }
946
947 ob_start( [ $this, 'optimize_and_write_cache_file' ] );
948 }
949
950 /**
951 * Optimize output and write the buffer to the cache file
952 *
953 * @param string $buffer
954 *
955 * @return string
956 */
957 public function optimize_and_write_cache_file( $buffer ) {
958 global $wpdb;
959
960 // we need these check to run after WordPress is finished preparing the page
961 if ( ! $this->should_save_cache() ) {
962 return $buffer;
963 }
964
965 $real_cache_dir = $this->get_real_cache_dir();
966 $cache_file = $this->get_cache_file_path() . '.gz';
967 $asset_cache_dir = $this->get_default_cache_path() . 'min/';
968 $asset_cache_url = trailingslashit( trailingslashit( get_site_url() ) . trim( str_replace( dirname( WP_CONTENT_DIR ), '', $asset_cache_dir ), '/' ) );
969 $settings = $this->settings;
970
971 if ( $settings->optimize_google_fonts ) {
972 $optimizer = new CombineGoogleFonts();
973 $buffer = $optimizer->optimize( $buffer );
974 }
975
976 if ( $settings->minify_css ) {
977 if ( $settings->combine_css ) {
978 $optimizer = new CssCombiner( $asset_cache_dir, $asset_cache_url, $settings->combine_css_footer );
979 } else {
980 $optimizer = new CssMinifier( $asset_cache_dir, $asset_cache_url );
981 }
982
983 $buffer = $optimizer->optimize( $buffer );
984 }
985
986 if ( $settings->minify_js ) {
987 if ( $settings->combine_head_js ) {
988 $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'head', $settings->combine_head_inline_js );
989 $buffer = $optimizer->optimize( $buffer );
990 }
991
992 if ( $settings->combine_body_js ) {
993 $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'body', $settings->combine_body_inline_js );
994 $buffer = $optimizer->optimize( $buffer );
995 }
996
997 if ( ! $settings->combine_head_js && ! $settings->combine_body_js ) {
998 $optimizer = new JsMinifier( $asset_cache_dir, $asset_cache_url );
999 $buffer = $optimizer->optimize( $buffer );
1000 }
1001 }
1002
1003 if ( $settings->minify_html ) {
1004 wp_raise_memory_limit( 'image' );
1005
1006 $buffer = Minify_HTML::minify( $buffer, [
1007 'htmlCleanComments' => $settings->minify_html_comments,
1008
1009 'cssMinifier' => function ( $css ) use ( $settings ) {
1010 if ( ! $settings->minify_inline_css ) {
1011 return $css;
1012 }
1013
1014 $minifier = new CSS( $css );
1015 $minifier->setMaxImportSize( 0 );
1016 $minifier->setImportExtensions( [] );
1017
1018 return $minifier->minify();
1019 },
1020
1021 'jsMinifier' => function ( $js ) use ( $settings ) {
1022 if ( ! $settings->minify_inline_js ) {
1023 return $js;
1024 }
1025
1026 $minifier = new JS( $js );
1027
1028 return $minifier->minify();
1029 },
1030 ] );
1031 }
1032
1033 if ( $settings->enable_webp_support && $this->webp_accepted() ) {
1034 $optimizer = new WebpConverter( $real_cache_dir, $cache_file, $this->webp_processor, $wpdb );
1035 $buffer = $optimizer->optimize( $buffer );
1036 }
1037
1038 $buffer = trim( $buffer );
1039 if ( empty( $buffer ) ) {
1040 Logger::log( 'ezCache will not save cache file for a blank page' );
1041
1042 return $buffer;
1043 }
1044
1045 if ( ! apply_filters( 'wp_bost_hide_cache_time_comment', false ) ) {
1046 $total_time = number_format( microtime( true ) - $this->cache_start_time, 2 );
1047 $cache_type = ( \Upress\EzCache\Settings::get_settings()->enable_redis_fullpage ?? false ) ? 'Redis' : 'Disk';
1048 $buffer .= "\n<!-- Cached by ezCache | Full-Page Cache: {$cache_type} | Generated: " . date('Y-m-d H:i:s') . " | Time: {$total_time}s -->";
1049 }
1050
1051 $buffer = apply_filters( 'ezcache_before_save_cache', $buffer );
1052
1053 // ── Redis Full-Page Cache write ───────────────────────
1054 // Mirror the cached HTML to Redis when the flag is on. TTL matches
1055 // the disk-cache lifetime so both backends expire in sync.
1056 if (
1057 ! empty( $settings->enable_redis_fullpage )
1058 && class_exists( '\\Upress\\EzCache\\RedisObjectCache' )
1059 ) {
1060 $current_url = $this->build_fullpage_url();
1061 $ttl = ! empty( $settings->cache_lifetime ) ? (int) $settings->cache_lifetime : 604800;
1062 \Upress\EzCache\RedisObjectCache::set_page( $current_url, $buffer, $ttl );
1063 }
1064
1065 if ( ! file_exists( $real_cache_dir ) ) {
1066 if ( ! @wp_mkdir_p( $real_cache_dir ) ) {
1067 Logger::log( 'ezCache could not create directory ' . $real_cache_dir );
1068
1069 return $buffer;
1070 }
1071 }
1072
1073 // write gzipped file
1074 $handle = @fopen( $cache_file, 'w' );
1075
1076 if ( $handle && @flock( $handle, LOCK_EX ) ) {
1077 fwrite( $handle, gzencode( $buffer, 6, FORCE_GZIP ) );
1078 flock( $handle, LOCK_UN );
1079 } else {
1080 Logger::log( 'ezCache could not write to ' . str_replace( ABSPATH, '', $cache_file ) );
1081 }
1082
1083 if ( $handle ) {
1084 fclose( $handle );
1085 }
1086
1087 return $buffer;
1088 }
1089
1090 /**
1091 * Delete a path recursively
1092 *
1093 * @param string $path
1094 */
1095 public function rmdir_recursive( $path ) {
1096 if ( ! file_exists( $path ) ) {
1097 return;
1098 }
1099
1100 $files = glob( $path . '/*' );
1101 foreach ( $files as $file ) {
1102 if ( file_exists( $file ) && is_dir( $file ) ) {
1103 $this->rmdir_recursive( $file );
1104 } elseif ( file_exists( $file ) ) {
1105 unlink( $file );
1106 }
1107 }
1108
1109 rmdir( $path );
1110 }
1111
1112 /**
1113 * Preload the homepage and immediately create cache for it
1114 */
1115 public function preload_homepage() {
1116 $desktop_ua = apply_filters(
1117 'ezcache_desktop_useragent',
1118 '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)'
1119 );
1120 $mobile_ua = apply_filters(
1121 'ezcache_mobile_useragent',
1122 '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)'
1123 );
1124
1125 wp_safe_remote_get( site_url(), [
1126 'user-agent' => $desktop_ua,
1127 'timeout' => 0.1,
1128 ] );
1129
1130 wp_safe_remote_get( site_url(), [
1131 'user-agent' => $mobile_ua,
1132 'timeout' => 0.1,
1133 ] );
1134 }
1135
1136 function delete_missing_webp_images( $delete_all = false ) {
1137 global $wpdb;
1138
1139 // delete the actual files
1140 $ids = [ 0 ];
1141 $where = $delete_all ? '' : "WHERE `status` = 'completed'";
1142 $images = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}ezcache_webp_images` {$where}" );
1143 foreach ( $images as $image ) {
1144 if ( ! file_exists( $image->webp_path ) ) {
1145 $ids[] = $image->id;
1146 } elseif ( ( $delete_all || ! file_exists( $image->path ) ) && file_exists( $image->webp_path ) ) {
1147 unlink( $image->webp_path );
1148 $ids[] = $image->id;
1149 }
1150 }
1151
1152 // clean the database
1153 $wpdb->query(
1154 $wpdb->prepare(
1155 "DELETE FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'failed' OR `id` IN ( " . substr( str_repeat( "%d, ", count( $ids ) ), 0, - 2 ) . " )",
1156 $ids
1157 )
1158 );
1159
1160 $wpdb->query( "OPTIMIZE TABLE `{$wpdb->prefix}ezcache_webp_images`" );
1161 }
1162
1163 function delete_all_webp_images() {
1164 $this->delete_missing_webp_images( true );
1165 }
1166
1167 /**
1168 * Clear all caches
1169 *
1170 * @param bool $clear_webp Should deleting cache clear the WebP images
1171 */
1172 public function clear_cache( $clear_webp = false ) {
1173 $this->rmdir_recursive( $this->root_cache_dir );
1174 @wp_mkdir_p( $this->root_cache_dir );
1175
1176 if ( $clear_webp ) {
1177 $this->delete_all_webp_images();
1178 } else {
1179 $this->delete_missing_webp_images();
1180 }
1181
1182 $this->purge_varnish_cache();
1183
1184 // Also flush Redis (both object cache and full-page keys live under ezcache:*).
1185 // If Redis is disabled or unavailable this is a no-op.
1186 if ( class_exists( '\\Upress\\EzCache\\RedisObjectCache' ) ) {
1187 \Upress\EzCache\RedisObjectCache::flush();
1188 }
1189
1190 $this->preload_homepage();
1191
1192 /**
1193 * Fires after the entire cache has been cleared.
1194 * Used by the Preload module to start a fresh preload run.
1195 */
1196 do_action( 'ezcache_after_clear_cache' );
1197 }
1198
1199 /**
1200 * Clear cache for a single post
1201 *
1202 * @param int $post_id
1203 */
1204 public function clear_cache_single( $post_id ) {
1205 $real_cache_dir = $this->get_real_cache_dir( $post_id );
1206
1207 $this->rmdir_recursive( $real_cache_dir );
1208
1209 $this->purge_varnish_cache();
1210
1211 // Remove the matching Redis full-page key so the next request rebuilds.
1212 if ( class_exists( '\\Upress\\EzCache\\RedisObjectCache' ) ) {
1213 $url = get_permalink( $post_id );
1214 if ( $url ) {
1215 \Upress\EzCache\RedisObjectCache::delete_page( $url );
1216 }
1217 }
1218
1219 /**
1220 * Fires after a single post's cache has been cleared.
1221 *
1222 * @param int $post_id
1223 */
1224 do_action( 'ezcache_after_clear_cache_single', $post_id );
1225 }
1226
1227 public function clear_cache_url( $url ) {
1228 $real_cache_dir = $this->get_real_cache_dir( 0, $url );
1229
1230 $this->rmdir_recursive( $real_cache_dir );
1231
1232 $this->purge_varnish_cache();
1233
1234 if ( class_exists( '\\Upress\\EzCache\\RedisObjectCache' ) ) {
1235 \Upress\EzCache\RedisObjectCache::delete_page( $url );
1236 }
1237
1238 /**
1239 * Fires after a URL's cache has been cleared.
1240 *
1241 * @param string $url
1242 */
1243 do_action( 'ezcache_after_clear_cache_url', $url );
1244 }
1245
1246 public function purge_varnish_cache() {
1247 // Whether Varnish PURGE is enabled (on by default). Can be turned off from
1248 // the settings screen on servers where Varnish is not in the request path,
1249 // to avoid generating needless 403 noise in the logs.
1250 $enabled = ! isset( $this->settings->enable_varnish_purge ) || ! empty( $this->settings->enable_varnish_purge );
1251
1252 /**
1253 * Filters whether ezCache should send a PURGE request to Varnish.
1254 *
1255 * Return false to skip the PURGE entirely.
1256 *
1257 * @param bool $enabled Whether the PURGE request should be sent.
1258 */
1259 if ( ! apply_filters( 'ezcache_should_purge_varnish', $enabled ) ) {
1260 return;
1261 }
1262
1263 $desktop_ua = apply_filters(
1264 'ezcache_desktop_useragent',
1265 '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)'
1266 );
1267
1268 $parseUrl = parse_url( home_url() );
1269
1270 $schema = 'http://';
1271 if ( isset( $parseUrl['scheme'] ) ) {
1272 $schema = $parseUrl['scheme'] . '://';
1273 }
1274
1275 $host = $parseUrl['host'];
1276
1277 // Send the PURGE to the local Varnish instance over loopback rather than to
1278 // the public host. The public hostname is preserved in the Host header so
1279 // Varnish still matches the right cache objects, while the request originates
1280 // from 127.0.0.1 — which is what Varnish/nginx PURGE ACLs typically allow,
1281 // avoiding the public 403 errors seen when the request leaves and re-enters
1282 // the server via its public IP.
1283 $purge_host = apply_filters( 'ezcache_varnish_purge_host', '127.0.0.1' );
1284
1285 $request_args = [
1286 'method' => 'PURGE',
1287 'headers' => [
1288 'Host' => $host,
1289 'User-Agent' => $desktop_ua,
1290 ],
1291 'sslverify' => false,
1292 ];
1293 $response = wp_remote_request( $schema . $purge_host . '/.*', $request_args );
1294 if ( is_wp_error( $response ) || $response['response']['code'] != '200' ) {
1295 if ( $schema === 'https://' ) {
1296 $schema = 'http://';
1297 } else {
1298 $schema = 'https://';
1299 }
1300 wp_remote_request( $schema . $purge_host . '/.*', $request_args );
1301 }
1302 }
1303
1304 /**
1305 * Delete expired cache
1306 */
1307 public function clear_expired_cache() {
1308 $settings = $this->settings;
1309
1310 try {
1311 $dir = new RecursiveDirectoryIterator( $this->root_cache_dir );
1312 $iterator = new RecursiveIteratorIterator( $dir );
1313 $files = new RegexIterator( $iterator, '/^.+\.(?:gz|html|js|css)$/i', RegexIterator::GET_MATCH );
1314 } catch ( UnexpectedValueException $ex ) {
1315 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
1316 $files = [];
1317 } else {
1318 throw $ex;
1319 }
1320 }
1321
1322 foreach ( $files as $file ) {
1323 if ( is_array( $file ) ) {
1324 $file = array_shift( $file );
1325 }
1326
1327 $stats = stat( $file );
1328 if ( $stats['mtime'] > ( time() - $settings->cache_lifetime ) ) {
1329 // skip not expired files
1330 continue;
1331 }
1332
1333 @unlink( $file );
1334 }
1335 }
1336
1337 /**
1338 * Get caching statistics and file sizes
1339 * @return array
1340 */
1341 public function get_cache_stats() {
1342 $settings = $this->settings;
1343 $cache_dir = $this->get_default_cache_path();
1344
1345 try {
1346 $dir = new RecursiveDirectoryIterator( $cache_dir );
1347 $iterator = new RecursiveIteratorIterator( $dir );
1348 $files = new RegexIterator( $iterator, '/^.+\.(?:gz|html|css|js)$/i', RegexIterator::GET_MATCH );
1349 } catch ( UnexpectedValueException $ex ) {
1350 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
1351 $files = [];
1352 } else {
1353 throw $ex;
1354 }
1355 }
1356
1357 $raw_data = [];
1358
1359 $mobile_count = 0;
1360 $mobile_size = 0;
1361 $mobile_expired_count = 0;
1362 $mobile_expired_size = 0;
1363 $desktop_count = 0;
1364 $desktop_size = 0;
1365 $desktop_expired_count = 0;
1366 $desktop_expired_size = 0;
1367 $js_count = 0;
1368 $js_size = 0;
1369 $js_expired_count = 0;
1370 $js_expired_size = 0;
1371 $css_count = 0;
1372 $css_size = 0;
1373 $css_expired_count = 0;
1374 $css_expired_size = 0;
1375
1376 foreach ( $files as $file ) {
1377 if ( is_array( $file ) ) {
1378 $file = array_shift( $file );
1379 }
1380
1381 $stats = stat( $file );
1382 $expired = $stats['mtime'] <= ( time() - $settings->cache_lifetime );
1383
1384 $raw_data[] = [
1385 'path' => $file,
1386 'stats' => $stats,
1387 'expired' => $expired,
1388 ];
1389
1390 if ( preg_match( '/^.+?-mobile\.html(\.gz)?$/i', $file ) ) {
1391 if ( ! $expired ) {
1392 $mobile_count ++;
1393 $mobile_size += $stats['size'];
1394 } else {
1395 $mobile_expired_count ++;
1396 $mobile_expired_size += $stats['size'];
1397 }
1398 } elseif ( preg_match( '/\.css$/i', $file ) ) {
1399 if ( $expired ) {
1400 $css_expired_count ++;
1401 $css_expired_size += $stats['size'];
1402 } else {
1403 $css_count ++;
1404 $css_size += $stats['size'];
1405 }
1406 } elseif ( preg_match( '/\.js$/i', $file ) ) {
1407 if ( $expired ) {
1408 $js_expired_count ++;
1409 $js_expired_size += $stats['size'];
1410 } else {
1411 $js_count ++;
1412 $js_size += $stats['size'];
1413 }
1414 } else {
1415 if ( ! $expired ) {
1416 $desktop_count ++;
1417 $desktop_size += $stats['size'];
1418 } else {
1419 $desktop_expired_count ++;
1420 $desktop_expired_size += $stats['size'];
1421 }
1422 }
1423 }
1424
1425 // we want to count only the number of pages which have cache, but we have 2 files for each page
1426 $mobile_count = $mobile_count / 2;
1427 $desktop_count = $desktop_count / 2;
1428
1429
1430 global $wpdb;
1431 $webp_images = 0;
1432 $webp_images_size = 0;
1433 $webp_images_original_size = 0;
1434
1435 $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'" );
1436 if ( $results ) {
1437 $webp_images = intval( $results->total );
1438 $webp_images_size = intval( $results->total_webp_size );
1439 $webp_images_original_size = intval( $results->total_original_size );
1440 }
1441
1442 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' );
1443 }
1444
1445 /**
1446 * Get the status of the cache
1447 *
1448 * @return array
1449 */
1450 public function get_status() {
1451 global $wpdb;
1452
1453 $wp_cache_enabled = defined( 'WP_CACHE' ) && WP_CACHE;
1454 $adv_cache_exists = file_exists( WP_CONTENT_DIR . '/advanced-cache.php' );
1455 $correct_advanced_cache = $adv_cache_exists && strpos( file_get_contents( WP_CONTENT_DIR . '/advanced-cache.php' ), 'ezCache Advanced Cache' ) !== false;
1456 $webp_table_exists = ! is_null( $wpdb->get_row( "SHOW TABLES LIKE '{$wpdb->prefix}ezcache_webp_images'" ) );
1457
1458 return [
1459 'cache_enabled' => $wp_cache_enabled,
1460 'adv_cache_exists' => $adv_cache_exists,
1461 'correct_cache_exists' => $correct_advanced_cache,
1462 'webp_table_exists' => $webp_table_exists,
1463 ];
1464 }
1465
1466 /**
1467 * Get a path by the URL
1468 *
1469 * @param string $url
1470 *
1471 * @return bool|string
1472 */
1473 public static function url_to_path( $url ) {
1474 $root_dir = trailingslashit( dirname( WP_CONTENT_DIR ) );
1475 $root_url = str_replace( wp_basename( WP_CONTENT_DIR ), '', content_url() );
1476 $url_host = wp_parse_url( $url, PHP_URL_HOST );
1477
1478 // relative path.
1479 if ( null === $url_host ) {
1480 $subdir_levels = substr_count( preg_replace( '/https?:\/\//', '', site_url() ), '/' );
1481 $url = trailingslashit( site_url() . str_repeat( '/..', $subdir_levels ) ) . ltrim( $url, '/' );
1482 }
1483
1484 $root_url = preg_replace( '/^https?:/', '', $root_url );
1485 $url_rep = preg_replace( '/^https?:/', '', $url );
1486 $file = str_replace( $root_url, $root_dir, $url_rep );
1487 $real_path = self::realpath( $file );
1488
1489 if ( ! file_exists( $real_path ) ) {
1490 return false;
1491 }
1492
1493 return $real_path;
1494 }
1495
1496 /**
1497 * Returns canonicalized absolute pathname.
1498 * The resulting path will have no symbolic link, '/./' or '/../' components.
1499 * Same as the defautl PHP realpath() function but works even when the files does not exist.
1500 *
1501 * @param string $file The path being checked.
1502 *
1503 * @return string
1504 * @see \realpath()
1505 *
1506 */
1507 public static function realpath( $file ) {
1508 $path = [];
1509
1510 foreach ( explode( '/', $file ) as $part ) {
1511 if ( '' === $part || '.' === $part ) {
1512 continue;
1513 }
1514
1515 if ( '..' !== $part ) {
1516 array_push( $path, $part );
1517 } elseif ( count( $path ) > 0 ) {
1518 array_pop( $path );
1519 }
1520 }
1521
1522 $prefix = 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ? '' : '/';
1523
1524 return $prefix . join( '/', $path );
1525 }
1526
1527 /**
1528 * Check if Development Mode is active (file-based, works before WP loads)
1529 */
1530 public static function is_dev_mode_active() {
1531 $flag_file = (defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : dirname(__DIR__)) . '/cache/ezcache/.dev-mode';
1532 if ( ! file_exists( $flag_file ) ) {
1533 return false;
1534 }
1535 $expires = (int) trim( @file_get_contents( $flag_file ) );
1536 if ( $expires === 0 ) {
1537 return true;
1538 }
1539 if ( time() >= $expires ) {
1540 @unlink( $flag_file );
1541 return false;
1542 }
1543 return true;
1544 }
1545
1546 public static function enable_dev_mode( $seconds = 3600 ) {
1547 $dir = WP_CONTENT_DIR . '/cache/ezcache';
1548 if ( ! is_dir( $dir ) ) {
1549 @mkdir( $dir, 0755, true );
1550 }
1551 $expires = ( $seconds === 0 ) ? 0 : time() + $seconds;
1552 file_put_contents( $dir . '/.dev-mode', (string) $expires );
1553 }
1554
1555 public static function disable_dev_mode() {
1556 @unlink( WP_CONTENT_DIR . '/cache/ezcache/.dev-mode' );
1557 }
1558
1559 public static function get_dev_mode_status() {
1560 $flag = WP_CONTENT_DIR . '/cache/ezcache/.dev-mode';
1561 if ( ! file_exists( $flag ) ) {
1562 return [ 'active' => false ];
1563 }
1564 $expires = (int) trim( @file_get_contents( $flag ) );
1565 if ( $expires > 0 && time() >= $expires ) {
1566 @unlink( $flag );
1567 return [ 'active' => false ];
1568 }
1569 return [
1570 'active' => true,
1571 'expires' => $expires === 0 ? 'permanent' : $expires,
1572 'remaining' => $expires === 0 ? null : $expires - time(),
1573 ];
1574 }
1575 }
1576