PluginProbe
ezCache / 2.6.4
ezCache v2.6.4
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.4, at includes/Cache.php

1,618 lines 43.6 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 // Never cache requests carrying a nonce or an action parameter. These are
359 // either one-time/per-request tokens (e.g. _wpnonce) or non-idempotent
360 // actions (add to cart, AJAX). Caching them is both wasteful (a new cache
361 // variation per value) and incorrect — a cached page could serve one user's
362 // nonce to another. The list is filterable for site-specific additions.
363 $bypass_query_params = apply_filters( 'ezcache_bypass_query_params', [
364 '_wpnonce', 'wc-ajax', 'add-to-cart', 'remove_item', 'removed_item',
365 'action', 'doing_wp_cron', 'add_to_wishlist',
366 ] );
367 foreach ( $bypass_query_params as $param ) {
368 if ( isset( $_GET[ $param ] ) ) {
369 return false;
370 }
371 }
372
373 if ( get_post_meta( get_the_ID(), '_ezcache_do_not_cache_post', true ) ) {
374 return false;
375 }
376
377 // check useragent
378 $rejected_useragents = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_user_agent ), - 1, PREG_SPLIT_NO_EMPTY );
379 $rejected_useragents = array_filter( $rejected_useragents );
380 if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
381 foreach ( $rejected_useragents as $ua ) {
382 if ( empty( $ua ) ) {
383 continue;
384 }
385
386 if ( false !== strpos( $_SERVER['HTTP_USER_AGENT'], trim( $ua ) ) ) {
387 return false;
388 }
389 }
390 }
391
392 // check URL
393 $rejected_uris = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_uri ), - 1, PREG_SPLIT_NO_EMPTY );
394 $rejected_uris = array_filter( $rejected_uris );
395 $domain = untrailingslashit( home_url() );
396 if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
397 foreach ( $rejected_uris as $url ) {
398 $url = str_replace( $domain, '', $url );
399 $url = '/' . trim( $url, '/' );
400 // Build the wildcard pattern by escaping each literal segment and
401 // joining the segments with `.*?`. This has to be done around
402 // preg_quote(), not after it: running preg_quote() first turns every
403 // `*` into `\*`, so a later str_replace('*', '.*?') corrupts it into
404 // `\.*?` (zero-or-more literal dots) and the wildcard silently never
405 // matches — which broke every pattern with a trailing or mid `*`.
406 $regex = implode( '.*?', array_map(
407 function ( $part ) { return preg_quote( $part, '/' ); },
408 explode( '*', $url )
409 ) );
410 if ( @preg_match( "/^{$regex}\/?$/u", urldecode( $_SERVER['REQUEST_URI'] ) ) ) {
411 return false;
412 }
413 }
414 }
415
416 return true;
417 }
418
419 /**
420 * Get the mobile browser name
421 *
422 * @return string
423 */
424 public function detect_mobile() {
425 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
426 return '';
427 }
428
429 $mobile_browsers = apply_filters( 'ezcache_mobile_browsers', [
430 '2.0 MMP',
431 '240x320',
432 '400X240',
433 'AvantGo',
434 'BlackBerry',
435 'Blazer',
436 'Cellphone',
437 'Danger',
438 'DoCoMo',
439 'Elaine/3.0',
440 'EudoraWeb',
441 'Googlebot-Mobile',
442 'hiptop',
443 'IEMobile',
444 'KYOCERA/WX310K',
445 'LG/U990',
446 'MIDP-2.',
447 'MMEF20',
448 'MOT-V',
449 'NetFront',
450 'Newt',
451 'Nintendo Wii',
452 'Nitro',
453 'Nokia',
454 'Opera Mini',
455 'Palm',
456 'PlayStation Portable',
457 'portalmmm',
458 'Proxinet',
459 'ProxiNet',
460 'SHARP-TQ-GX10',
461 'SHG-i900',
462 'Small',
463 'SonyEricsson',
464 'Symbian OS',
465 'SymbianOS',
466 'TS21i-10',
467 'UP.Browser',
468 'UP.Link',
469 'webOS',
470 'Windows CE',
471 'WinWAP',
472 'YahooSeeker/M1A1-R2D2',
473 'iPhone',
474 'iPod',
475 'iPad',
476 'Android',
477 'BlackBerry9530',
478 'LG-TU915 Obigo',
479 'LGE VX',
480 'webOS',
481 'Nokia5800',
482 ] );
483 $user_agent = strtolower( $_SERVER['HTTP_USER_AGENT'] );
484 foreach ( $mobile_browsers as $browser ) {
485 if ( strstr( $user_agent, trim( strtolower( $browser ) ) ) ) {
486 return $user_agent;
487 }
488 }
489
490 if ( isset( $_SERVER['HTTP_X_WAP_PROFILE'] ) ) {
491 return $_SERVER['HTTP_X_WAP_PROFILE'];
492 }
493
494 if ( isset( $_SERVER['HTTP_PROFILE'] ) ) {
495 return $_SERVER['HTTP_PROFILE'];
496 }
497
498 $browser_prefixes = apply_filters( 'ezcache_mobile_browser_prefixes', [
499 'w3c',
500 'w3c-',
501 'acs-',
502 'alav',
503 'alca',
504 'amoi',
505 'audi',
506 'avan',
507 'benq',
508 'bird',
509 'blac',
510 'blaz',
511 'brew',
512 'cell',
513 'cldc',
514 'cmd-',
515 'dang',
516 'doco',
517 'eric',
518 'hipt',
519 'htc_',
520 'inno',
521 'ipaq',
522 'ipod',
523 'jigs',
524 'kddi',
525 'keji',
526 'leno',
527 'lg-c',
528 'lg-d',
529 'lg-g',
530 'lge-',
531 'lg/u',
532 'maui',
533 'maxo',
534 'midp',
535 'mits',
536 'mmef',
537 'mobi',
538 'mot-',
539 'moto',
540 'mwbp',
541 'nec-',
542 'newt',
543 'noki',
544 'palm',
545 'pana',
546 'pant',
547 'phil',
548 'play',
549 'port',
550 'prox',
551 'qwap',
552 'sage',
553 'sams',
554 'sany',
555 'sch-',
556 'sec-',
557 'send',
558 'seri',
559 'sgh-',
560 'shar',
561 'sie-',
562 'siem',
563 'smal',
564 'smar',
565 'sony',
566 'sph-',
567 'symb',
568 't-mo',
569 'teli',
570 'tim-',
571 'tosh',
572 'tsm-',
573 'upg1',
574 'upsi',
575 'vk-v',
576 'voda',
577 'wap-',
578 'wapa',
579 'wapi',
580 'wapp',
581 'wapr',
582 'webc',
583 'winw',
584 'winw',
585 'xda',
586 'xda-',
587 ] );
588 foreach ( $browser_prefixes as $prefix ) {
589 if ( substr( $user_agent, 0, 4 ) == $prefix ) {
590 return $prefix;
591 }
592 }
593
594 $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( $_SERVER['HTTP_ACCEPT'] ) : '';
595 if ( strpos( $accept, 'wap' ) !== false ) {
596 return 'wap';
597 }
598
599 if ( isset( $_SERVER['ALL_HTTP'] ) && false !== strpos( strtolower( $_SERVER['ALL_HTTP'] ), 'operamini' ) ) {
600 return 'operamini';
601 }
602
603 return '';
604 }
605
606 /**
607 * Search & replace in a string
608 *
609 * @param string|string[] $search
610 * @param string $subject
611 *
612 * @return string
613 */
614 public function deep_replace( $search, $subject ) {
615 $subject = (string) $subject;
616
617 $count = 1;
618 while ( $count ) {
619 $subject = str_replace( $search, '', $subject, $count );
620 }
621
622 return $subject;
623 }
624
625 /**
626 * Get the cache directory URL for the current post
627 *
628 * @param int $post_id
629 *
630 * @param null|string $url
631 *
632 * @return mixed|string
633 */
634 public function get_current_url_cache_dir( $post_id = 0, $url = null ) {
635 static $url_cache_dir = [];
636
637 if ( isset( $url_cache_dir[ $post_id ] ) ) {
638 return $url_cache_dir[ $post_id ];
639 }
640
641 $uri = strtolower( $url ? ( '/' . ltrim( $url, '/' ) ) : $_SERVER['REQUEST_URI'] );
642
643 $DONOTREMEMBER = 0;
644 if ( 0 !== $post_id ) {
645 $site_url = site_url();
646 $permalink = get_permalink( $post_id );
647 if ( false === strpos( $permalink, $site_url ) ) {
648 $DONOTREMEMBER = 1;
649 if ( preg_match( '`^(https?:)?//([^/]+)(/.*)?$`i', $permalink, $matches ) ) {
650 $uri = isset( $matches[3] ) ? $matches[3] : '';
651 } elseif ( preg_match( '`^/([^/]+)(/.*)?$`i', $permalink, $matches ) ) {
652 $uri = $permalink;
653 } else {
654 $uri = '';
655 }
656 } else {
657 $uri = str_replace( $site_url, '', $permalink );
658 if ( 0 !== strpos( $uri, '/' ) ) {
659 $uri = '/' . $uri;
660 }
661 }
662 }
663
664 $uri = $this->deep_replace(
665 [
666 '..',
667 '\\',
668 'index.php',
669 ],
670 preg_replace(
671 '/[ <>\'\"\r\n\t()]/',
672 '',
673 preg_replace( "/(\?.*)?(#.*)?$/", '', $uri )
674 )
675 );
676
677 $uri = md5( $uri );
678 $dir = str_replace( '..', '', str_replace( '//', '/', $uri . '/' ) );
679
680 if ( $DONOTREMEMBER == 0 ) {
681 $url_cache_dir[ $post_id ] = $dir;
682 }
683
684 return $dir;
685 }
686
687 /**
688 * Get the cache directory path
689 *
690 * @param int $postid
691 *
692 * @param null|string $url
693 *
694 * @return string
695 */
696 public function get_real_cache_dir( $postid = 0, $url = null ) {
697 return $this->get_default_cache_path() . $this->get_current_url_cache_dir( $postid, $url );
698 }
699
700 /**
701 * Get the full cache file path
702 *
703 * @param int $postid
704 *
705 * @param null|string $url
706 *
707 * @return string
708 */
709 public function get_cache_file_path( $postid = 0, $url = null ) {
710 return $this->get_real_cache_dir( $postid, $url ) . $this->get_cache_filename();
711 }
712
713 /**
714 * Get the filename for the cached file
715 *
716 * @return string
717 */
718 /**
719 * Lowercased list of query-string parameters to ignore when building the
720 * cache key. Only meaningful when the ignore_query_params setting is on.
721 *
722 * @return array
723 */
724 private function get_ignored_query_params() {
725 static $cached = null;
726 if ( null !== $cached ) {
727 return $cached;
728 }
729 $raw = isset( $this->settings->ignored_query_params_list ) ? (string) $this->settings->ignored_query_params_list : '';
730 $list = preg_split( '/[\s,]+/', strtolower( $raw ), -1, PREG_SPLIT_NO_EMPTY );
731
732 /**
733 * Filters the query-string parameters ignored when building the cache key.
734 *
735 * @param array $list Lowercased parameter names.
736 */
737 $list = apply_filters( 'ezcache_ignored_query_params', $list );
738 $cached = array_values( array_unique( array_map( 'strtolower', (array) $list ) ) );
739
740 return $cached;
741 }
742
743 /**
744 * Normalize a raw query string for cache-key purposes. When the
745 * ignore_query_params feature is on, drop the ignored (tracking) parameters
746 * and sort the rest so different orderings and tracking values map to the
747 * same cache entry. Returns '' when nothing meaningful remains.
748 *
749 * @param string $query_string
750 * @return string
751 */
752 private function normalize_query_string( $query_string ) {
753 if ( '' === (string) $query_string ) {
754 return '';
755 }
756 if ( empty( $this->settings->ignore_query_params ) ) {
757 return $query_string; // feature off — behaviour unchanged
758 }
759 parse_str( (string) $query_string, $params );
760 if ( empty( $params ) ) {
761 return '';
762 }
763 $ignored = $this->get_ignored_query_params();
764 foreach ( array_keys( $params ) as $key ) {
765 if ( $this->query_param_is_ignored( strtolower( $key ), $ignored ) ) {
766 unset( $params[ $key ] );
767 }
768 }
769 if ( empty( $params ) ) {
770 return '';
771 }
772 ksort( $params );
773
774 return http_build_query( $params );
775 }
776
777 /**
778 * Whether a (lowercased) query parameter name matches the ignore list.
779 * Supports exact names and trailing-"*" prefix patterns (e.g. "utm_*").
780 * A bare "*" is skipped to avoid accidentally dropping every parameter.
781 *
782 * @param string $key Lowercased parameter name.
783 * @param array $ignored Lowercased ignore patterns.
784 * @return bool
785 */
786 private function query_param_is_ignored( $key, $ignored ) {
787 foreach ( $ignored as $pattern ) {
788 if ( '' === $pattern || '*' === $pattern ) {
789 continue;
790 }
791 if ( '*' === substr( $pattern, -1 ) ) {
792 $prefix = substr( $pattern, 0, -1 );
793 if ( '' !== $prefix && 0 === strpos( $key, $prefix ) ) {
794 return true;
795 }
796 } elseif ( $key === $pattern ) {
797 return true;
798 }
799 }
800
801 return false;
802 }
803
804 /**
805 * Build the full-page (Redis) cache URL for the current request, applying
806 * the same query-string normalization used for the disk cache key.
807 *
808 * @return string
809 */
810 private function build_fullpage_url() {
811 $scheme = ( is_ssl() ? 'https://' : 'http://' );
812 $host = $_SERVER['HTTP_HOST'] ?? '';
813 $uri = $_SERVER['REQUEST_URI'] ?? '/';
814 $path = $uri;
815 $qs = '';
816 $pos = strpos( $uri, '?' );
817 if ( false !== $pos ) {
818 $path = substr( $uri, 0, $pos );
819 $qs = substr( $uri, $pos + 1 );
820 }
821 $norm = $this->normalize_query_string( $qs );
822
823 return $scheme . $host . $path . ( '' !== $norm ? '?' . $norm : '' );
824 }
825
826 public function get_cache_filename() {
827 $settings = $this->settings;
828
829 // Add support for https and http caching
830 // also supports https requests coming from an nginx reverse proxy
831 $is_https = ( ( isset( $_SERVER['HTTPS'] ) && 'on' == strtolower( $_SERVER['HTTPS'] ) ) || ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == strtolower( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) );
832 $extra_str = $is_https ? '-https' : '';
833
834 if ( $settings->separate_mobile_cache ) {
835 $mobile_ua = $this->detect_mobile();
836 if ( ! empty( $mobile_ua ) ) {
837 $extra_str .= '-mobile';
838 }
839 }
840
841 if ( $settings->enable_webp_support && $this->webp_accepted() ) {
842 $extra_str .= '-webp';
843 }
844
845 $filename = 'index';
846 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
847 $normalized = $this->normalize_query_string( $_SERVER['QUERY_STRING'] );
848 // When every parameter was ignored, fall back to 'index' so the
849 // request maps to the same cache entry as the clean URL.
850 if ( '' !== $normalized ) {
851 $filename = md5( $normalized );
852 }
853 }
854
855 return $filename . $extra_str . '.html';
856 }
857
858 /**
859 * Check if we have a cached file and serve it
860 */
861 public function maybe_serve_cached_data() {
862 if ( ! $this->should_serve_cached_data() ) {
863 return;
864 }
865
866 // ── Redis Full-Page Cache fast path ────────────────────
867 // When enabled, try Redis first. A hit is sub-millisecond and skips
868 // the disk read entirely. On miss we fall through to the disk path
869 // below (and the response handler in maybe_write_cache_file will
870 // populate Redis for next time).
871 if (
872 ! empty( $this->settings->enable_redis_fullpage )
873 && class_exists( '\\Upress\\EzCache\\RedisObjectCache' )
874 ) {
875 $current_url = $this->build_fullpage_url();
876 $cached_html = \Upress\EzCache\RedisObjectCache::get_page( $current_url );
877 if ( false !== $cached_html && '' !== $cached_html ) {
878 header( 'X-Cached-With: ezCache (Redis)' );
879 header( 'Vary: Accept-Encoding, Cookie' );
880 echo $cached_html;
881 exit;
882 }
883 }
884
885 $cache_file = $this->get_cache_file_path();
886 $gzip_accepted = $this->gzip_accepted();
887
888 $cache_file = $cache_file . '.gz';
889 $filesize = file_exists( $cache_file ) ? @filesize( $cache_file ) : false;
890
891 if ( ! $filesize ) {
892 // the file is empty, we have nothing to serve
893 return;
894 }
895
896 header( "X-Cached-With: ezCache" );
897 header( "Vary: Accept-Encoding, Cookie" );
898 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_file ) ) . ' GMT' );
899
900 // Getting If-Modified-Since headers sent by the client.
901 if ( function_exists( 'apache_request_headers' ) ) {
902 $headers = apache_request_headers();
903 $http_if_modified_since = ( isset( $headers['If-Modified-Since'] ) ) ? $headers['If-Modified-Since'] : '';
904 } else {
905 $http_if_modified_since = ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : '';
906 }
907
908 // Checking if the client is validating his cache and if it is current.
909 if ( $http_if_modified_since && ( strtotime( $http_if_modified_since ) === @filemtime( $cache_file ) ) ) {
910 // Client's cache is current, so we just respond '304 Not Modified'.
911 header( $_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified', true, 304 );
912 exit;
913 }
914
915 // Serve the cache if file isn't store in the client browser cache.
916 // if the browser does not support gzip read the file and output it without gzip encoding
917 if ( ! $gzip_accepted ) {
918 readgzfile( $cache_file );
919 exit;
920 }
921
922 // otherwise output the gzipped file as-is
923 header( "Content-Length: {$filesize}" );
924 header( "Content-Encoding: gzip" );
925 readfile( $cache_file );
926 exit;
927 }
928
929 public function do_frontend_optimizations() {
930 if ( ! $this->should_serve_cached_data() ) {
931 return;
932 }
933
934 $settings = $this->settings;
935
936 if ( isset( $settings->disable_wp_emoji ) && $settings->disable_wp_emoji ) {
937 add_action( 'init', function () {
938 remove_action( 'admin_print_styles', 'print_emoji_styles' );
939 remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
940 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
941 remove_action( 'wp_print_styles', 'print_emoji_styles' );
942 remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
943 remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
944 remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
945 add_filter( 'emoji_svg_url', '__return_false' );
946 }, 999 );
947 }
948
949
950 if ( ! empty( $settings->critical_css ) ) {
951 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_critical_css' ], PHP_INT_MAX );
952 }
953 }
954
955 public function enqueue_critical_css() {
956 wp_register_style( 'ezcache-critical-css', false );
957 wp_enqueue_style( 'ezcache-critical-css' );
958 wp_add_inline_style( 'ezcache-critical-css', $this->settings->critical_css );
959 }
960
961 /**
962 * Write cache file if we need to
963 * @noinspection PhpUnused
964 */
965 public function maybe_write_cache_file() {
966 if ( ! $this->should_serve_cached_data() ) {
967 return;
968 }
969
970 ob_start( [ $this, 'optimize_and_write_cache_file' ] );
971 }
972
973 /**
974 * Optimize output and write the buffer to the cache file
975 *
976 * @param string $buffer
977 *
978 * @return string
979 */
980 public function optimize_and_write_cache_file( $buffer ) {
981 global $wpdb;
982
983 // we need these check to run after WordPress is finished preparing the page
984 if ( ! $this->should_save_cache() ) {
985 return $buffer;
986 }
987
988 // Only process and cache real HTML responses. Non-HTML output — text/plain
989 // (IndexNow key files, llms.txt), RSS/Atom feeds, JSON, etc. — must pass
990 // through untouched: appending the footer comment or running the HTML
991 // transforms (minify, WebP, combine) on it corrupts the content, and
992 // IndexNow in particular requires a byte-exact body. We bail only on an
993 // explicit non-HTML Content-Type; a missing header is treated as HTML so
994 // normal page caching is never disabled.
995 $content_type = '';
996 foreach ( headers_list() as $header ) {
997 if ( stripos( $header, 'content-type:' ) === 0 ) {
998 $content_type = strtolower( $header );
999 }
1000 }
1001 if ( '' !== $content_type
1002 && false === stripos( $content_type, 'text/html' )
1003 && false === stripos( $content_type, 'application/xhtml' ) ) {
1004 return $buffer;
1005 }
1006
1007 $real_cache_dir = $this->get_real_cache_dir();
1008 $cache_file = $this->get_cache_file_path() . '.gz';
1009 $asset_cache_dir = $this->get_default_cache_path() . 'min/';
1010 $asset_cache_url = trailingslashit( trailingslashit( get_site_url() ) . trim( str_replace( dirname( WP_CONTENT_DIR ), '', $asset_cache_dir ), '/' ) );
1011 $settings = $this->settings;
1012
1013 if ( $settings->optimize_google_fonts ) {
1014 $optimizer = new CombineGoogleFonts();
1015 $buffer = $optimizer->optimize( $buffer );
1016 }
1017
1018 if ( $settings->minify_css ) {
1019 if ( $settings->combine_css ) {
1020 $optimizer = new CssCombiner( $asset_cache_dir, $asset_cache_url, $settings->combine_css_footer );
1021 } else {
1022 $optimizer = new CssMinifier( $asset_cache_dir, $asset_cache_url );
1023 }
1024
1025 $buffer = $optimizer->optimize( $buffer );
1026 }
1027
1028 if ( $settings->minify_js ) {
1029 if ( $settings->combine_head_js ) {
1030 $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'head', $settings->combine_head_inline_js );
1031 $buffer = $optimizer->optimize( $buffer );
1032 }
1033
1034 if ( $settings->combine_body_js ) {
1035 $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'body', $settings->combine_body_inline_js );
1036 $buffer = $optimizer->optimize( $buffer );
1037 }
1038
1039 if ( ! $settings->combine_head_js && ! $settings->combine_body_js ) {
1040 $optimizer = new JsMinifier( $asset_cache_dir, $asset_cache_url );
1041 $buffer = $optimizer->optimize( $buffer );
1042 }
1043 }
1044
1045 if ( $settings->minify_html ) {
1046 wp_raise_memory_limit( 'image' );
1047
1048 $buffer = Minify_HTML::minify( $buffer, [
1049 'htmlCleanComments' => $settings->minify_html_comments,
1050
1051 'cssMinifier' => function ( $css ) use ( $settings ) {
1052 if ( ! $settings->minify_inline_css ) {
1053 return $css;
1054 }
1055
1056 $minifier = new CSS( $css );
1057 $minifier->setMaxImportSize( 0 );
1058 $minifier->setImportExtensions( [] );
1059
1060 return $minifier->minify();
1061 },
1062
1063 'jsMinifier' => function ( $js ) use ( $settings ) {
1064 if ( ! $settings->minify_inline_js ) {
1065 return $js;
1066 }
1067
1068 $minifier = new JS( $js );
1069
1070 return $minifier->minify();
1071 },
1072 ] );
1073 }
1074
1075 if ( $settings->enable_webp_support && $this->webp_accepted() ) {
1076 $optimizer = new WebpConverter( $real_cache_dir, $cache_file, $this->webp_processor, $wpdb );
1077 $buffer = $optimizer->optimize( $buffer );
1078 }
1079
1080 $buffer = trim( $buffer );
1081 if ( empty( $buffer ) ) {
1082 Logger::log( 'ezCache will not save cache file for a blank page' );
1083
1084 return $buffer;
1085 }
1086
1087 if ( ! apply_filters( 'wp_bost_hide_cache_time_comment', false ) ) {
1088 $total_time = number_format( microtime( true ) - $this->cache_start_time, 2 );
1089 $cache_type = ( \Upress\EzCache\Settings::get_settings()->enable_redis_fullpage ?? false ) ? 'Redis' : 'Disk';
1090 $buffer .= "\n<!-- Cached by ezCache | Full-Page Cache: {$cache_type} | Generated: " . date('Y-m-d H:i:s') . " | Time: {$total_time}s -->";
1091 }
1092
1093 $buffer = apply_filters( 'ezcache_before_save_cache', $buffer );
1094
1095 // ── Redis Full-Page Cache write ───────────────────────
1096 // Mirror the cached HTML to Redis when the flag is on. TTL matches
1097 // the disk-cache lifetime so both backends expire in sync.
1098 if (
1099 ! empty( $settings->enable_redis_fullpage )
1100 && class_exists( '\\Upress\\EzCache\\RedisObjectCache' )
1101 ) {
1102 $current_url = $this->build_fullpage_url();
1103 $ttl = ! empty( $settings->cache_lifetime ) ? (int) $settings->cache_lifetime : 604800;
1104 \Upress\EzCache\RedisObjectCache::set_page( $current_url, $buffer, $ttl );
1105 }
1106
1107 if ( ! file_exists( $real_cache_dir ) ) {
1108 if ( ! @wp_mkdir_p( $real_cache_dir ) ) {
1109 Logger::log( 'ezCache could not create directory ' . $real_cache_dir );
1110
1111 return $buffer;
1112 }
1113 }
1114
1115 // write gzipped file
1116 $handle = @fopen( $cache_file, 'w' );
1117
1118 if ( $handle && @flock( $handle, LOCK_EX ) ) {
1119 fwrite( $handle, gzencode( $buffer, 6, FORCE_GZIP ) );
1120 flock( $handle, LOCK_UN );
1121 } else {
1122 Logger::log( 'ezCache could not write to ' . str_replace( ABSPATH, '', $cache_file ) );
1123 }
1124
1125 if ( $handle ) {
1126 fclose( $handle );
1127 }
1128
1129 return $buffer;
1130 }
1131
1132 /**
1133 * Delete a path recursively
1134 *
1135 * @param string $path
1136 */
1137 public function rmdir_recursive( $path ) {
1138 if ( ! file_exists( $path ) ) {
1139 return;
1140 }
1141
1142 $files = glob( $path . '/*' );
1143 foreach ( $files as $file ) {
1144 if ( file_exists( $file ) && is_dir( $file ) ) {
1145 $this->rmdir_recursive( $file );
1146 } elseif ( file_exists( $file ) ) {
1147 unlink( $file );
1148 }
1149 }
1150
1151 rmdir( $path );
1152 }
1153
1154 /**
1155 * Preload the homepage and immediately create cache for it
1156 */
1157 public function preload_homepage() {
1158 $desktop_ua = apply_filters(
1159 'ezcache_desktop_useragent',
1160 '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)'
1161 );
1162 $mobile_ua = apply_filters(
1163 'ezcache_mobile_useragent',
1164 '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)'
1165 );
1166
1167 wp_safe_remote_get( site_url(), [
1168 'user-agent' => $desktop_ua,
1169 'timeout' => 0.1,
1170 ] );
1171
1172 wp_safe_remote_get( site_url(), [
1173 'user-agent' => $mobile_ua,
1174 'timeout' => 0.1,
1175 ] );
1176 }
1177
1178 function delete_missing_webp_images( $delete_all = false ) {
1179 global $wpdb;
1180
1181 // delete the actual files
1182 $ids = [ 0 ];
1183 $where = $delete_all ? '' : "WHERE `status` = 'completed'";
1184 $images = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}ezcache_webp_images` {$where}" );
1185 foreach ( $images as $image ) {
1186 if ( ! file_exists( $image->webp_path ) ) {
1187 $ids[] = $image->id;
1188 } elseif ( ( $delete_all || ! file_exists( $image->path ) ) && file_exists( $image->webp_path ) ) {
1189 unlink( $image->webp_path );
1190 $ids[] = $image->id;
1191 }
1192 }
1193
1194 // clean the database
1195 $wpdb->query(
1196 $wpdb->prepare(
1197 "DELETE FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'failed' OR `id` IN ( " . substr( str_repeat( "%d, ", count( $ids ) ), 0, - 2 ) . " )",
1198 $ids
1199 )
1200 );
1201
1202 $wpdb->query( "OPTIMIZE TABLE `{$wpdb->prefix}ezcache_webp_images`" );
1203 }
1204
1205 function delete_all_webp_images() {
1206 $this->delete_missing_webp_images( true );
1207 }
1208
1209 /**
1210 * Clear all caches
1211 *
1212 * @param bool $clear_webp Should deleting cache clear the WebP images
1213 */
1214 public function clear_cache( $clear_webp = false ) {
1215 $this->rmdir_recursive( $this->root_cache_dir );
1216 @wp_mkdir_p( $this->root_cache_dir );
1217
1218 if ( $clear_webp ) {
1219 $this->delete_all_webp_images();
1220 } else {
1221 $this->delete_missing_webp_images();
1222 }
1223
1224 $this->purge_varnish_cache();
1225
1226 // Also flush Redis (both object cache and full-page keys live under ezcache:*).
1227 // If Redis is disabled or unavailable this is a no-op.
1228 if ( class_exists( '\\Upress\\EzCache\\RedisObjectCache' ) ) {
1229 \Upress\EzCache\RedisObjectCache::flush();
1230 }
1231
1232 $this->preload_homepage();
1233
1234 /**
1235 * Fires after the entire cache has been cleared.
1236 * Used by the Preload module to start a fresh preload run.
1237 */
1238 do_action( 'ezcache_after_clear_cache' );
1239 }
1240
1241 /**
1242 * Clear cache for a single post
1243 *
1244 * @param int $post_id
1245 */
1246 public function clear_cache_single( $post_id ) {
1247 $real_cache_dir = $this->get_real_cache_dir( $post_id );
1248
1249 $this->rmdir_recursive( $real_cache_dir );
1250
1251 $this->purge_varnish_cache();
1252
1253 // Remove the matching Redis full-page key so the next request rebuilds.
1254 if ( class_exists( '\\Upress\\EzCache\\RedisObjectCache' ) ) {
1255 $url = get_permalink( $post_id );
1256 if ( $url ) {
1257 \Upress\EzCache\RedisObjectCache::delete_page( $url );
1258 }
1259 }
1260
1261 /**
1262 * Fires after a single post's cache has been cleared.
1263 *
1264 * @param int $post_id
1265 */
1266 do_action( 'ezcache_after_clear_cache_single', $post_id );
1267 }
1268
1269 public function clear_cache_url( $url ) {
1270 $real_cache_dir = $this->get_real_cache_dir( 0, $url );
1271
1272 $this->rmdir_recursive( $real_cache_dir );
1273
1274 $this->purge_varnish_cache();
1275
1276 if ( class_exists( '\\Upress\\EzCache\\RedisObjectCache' ) ) {
1277 \Upress\EzCache\RedisObjectCache::delete_page( $url );
1278 }
1279
1280 /**
1281 * Fires after a URL's cache has been cleared.
1282 *
1283 * @param string $url
1284 */
1285 do_action( 'ezcache_after_clear_cache_url', $url );
1286 }
1287
1288 public function purge_varnish_cache() {
1289 // Whether Varnish PURGE is enabled (on by default). Can be turned off from
1290 // the settings screen on servers where Varnish is not in the request path,
1291 // to avoid generating needless 403 noise in the logs.
1292 $enabled = ! isset( $this->settings->enable_varnish_purge ) || ! empty( $this->settings->enable_varnish_purge );
1293
1294 /**
1295 * Filters whether ezCache should send a PURGE request to Varnish.
1296 *
1297 * Return false to skip the PURGE entirely.
1298 *
1299 * @param bool $enabled Whether the PURGE request should be sent.
1300 */
1301 if ( ! apply_filters( 'ezcache_should_purge_varnish', $enabled ) ) {
1302 return;
1303 }
1304
1305 $desktop_ua = apply_filters(
1306 'ezcache_desktop_useragent',
1307 '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)'
1308 );
1309
1310 $parseUrl = parse_url( home_url() );
1311
1312 $schema = 'http://';
1313 if ( isset( $parseUrl['scheme'] ) ) {
1314 $schema = $parseUrl['scheme'] . '://';
1315 }
1316
1317 $host = $parseUrl['host'];
1318
1319 // Send the PURGE to the local Varnish instance over loopback rather than to
1320 // the public host. The public hostname is preserved in the Host header so
1321 // Varnish still matches the right cache objects, while the request originates
1322 // from 127.0.0.1 — which is what Varnish/nginx PURGE ACLs typically allow,
1323 // avoiding the public 403 errors seen when the request leaves and re-enters
1324 // the server via its public IP.
1325 $purge_host = apply_filters( 'ezcache_varnish_purge_host', '127.0.0.1' );
1326
1327 $request_args = [
1328 'method' => 'PURGE',
1329 'headers' => [
1330 'Host' => $host,
1331 'User-Agent' => $desktop_ua,
1332 ],
1333 'sslverify' => false,
1334 ];
1335 $response = wp_remote_request( $schema . $purge_host . '/.*', $request_args );
1336 if ( is_wp_error( $response ) || $response['response']['code'] != '200' ) {
1337 if ( $schema === 'https://' ) {
1338 $schema = 'http://';
1339 } else {
1340 $schema = 'https://';
1341 }
1342 wp_remote_request( $schema . $purge_host . '/.*', $request_args );
1343 }
1344 }
1345
1346 /**
1347 * Delete expired cache
1348 */
1349 public function clear_expired_cache() {
1350 $settings = $this->settings;
1351
1352 try {
1353 $dir = new RecursiveDirectoryIterator( $this->root_cache_dir );
1354 $iterator = new RecursiveIteratorIterator( $dir );
1355 $files = new RegexIterator( $iterator, '/^.+\.(?:gz|html|js|css)$/i', RegexIterator::GET_MATCH );
1356 } catch ( UnexpectedValueException $ex ) {
1357 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
1358 $files = [];
1359 } else {
1360 throw $ex;
1361 }
1362 }
1363
1364 foreach ( $files as $file ) {
1365 if ( is_array( $file ) ) {
1366 $file = array_shift( $file );
1367 }
1368
1369 $stats = stat( $file );
1370 if ( $stats['mtime'] > ( time() - $settings->cache_lifetime ) ) {
1371 // skip not expired files
1372 continue;
1373 }
1374
1375 @unlink( $file );
1376 }
1377 }
1378
1379 /**
1380 * Get caching statistics and file sizes
1381 * @return array
1382 */
1383 public function get_cache_stats() {
1384 $settings = $this->settings;
1385 $cache_dir = $this->get_default_cache_path();
1386
1387 try {
1388 $dir = new RecursiveDirectoryIterator( $cache_dir );
1389 $iterator = new RecursiveIteratorIterator( $dir );
1390 $files = new RegexIterator( $iterator, '/^.+\.(?:gz|html|css|js)$/i', RegexIterator::GET_MATCH );
1391 } catch ( UnexpectedValueException $ex ) {
1392 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
1393 $files = [];
1394 } else {
1395 throw $ex;
1396 }
1397 }
1398
1399 $raw_data = [];
1400
1401 $mobile_count = 0;
1402 $mobile_size = 0;
1403 $mobile_expired_count = 0;
1404 $mobile_expired_size = 0;
1405 $desktop_count = 0;
1406 $desktop_size = 0;
1407 $desktop_expired_count = 0;
1408 $desktop_expired_size = 0;
1409 $js_count = 0;
1410 $js_size = 0;
1411 $js_expired_count = 0;
1412 $js_expired_size = 0;
1413 $css_count = 0;
1414 $css_size = 0;
1415 $css_expired_count = 0;
1416 $css_expired_size = 0;
1417
1418 foreach ( $files as $file ) {
1419 if ( is_array( $file ) ) {
1420 $file = array_shift( $file );
1421 }
1422
1423 $stats = stat( $file );
1424 $expired = $stats['mtime'] <= ( time() - $settings->cache_lifetime );
1425
1426 $raw_data[] = [
1427 'path' => $file,
1428 'stats' => $stats,
1429 'expired' => $expired,
1430 ];
1431
1432 if ( preg_match( '/^.+?-mobile\.html(\.gz)?$/i', $file ) ) {
1433 if ( ! $expired ) {
1434 $mobile_count ++;
1435 $mobile_size += $stats['size'];
1436 } else {
1437 $mobile_expired_count ++;
1438 $mobile_expired_size += $stats['size'];
1439 }
1440 } elseif ( preg_match( '/\.css$/i', $file ) ) {
1441 if ( $expired ) {
1442 $css_expired_count ++;
1443 $css_expired_size += $stats['size'];
1444 } else {
1445 $css_count ++;
1446 $css_size += $stats['size'];
1447 }
1448 } elseif ( preg_match( '/\.js$/i', $file ) ) {
1449 if ( $expired ) {
1450 $js_expired_count ++;
1451 $js_expired_size += $stats['size'];
1452 } else {
1453 $js_count ++;
1454 $js_size += $stats['size'];
1455 }
1456 } else {
1457 if ( ! $expired ) {
1458 $desktop_count ++;
1459 $desktop_size += $stats['size'];
1460 } else {
1461 $desktop_expired_count ++;
1462 $desktop_expired_size += $stats['size'];
1463 }
1464 }
1465 }
1466
1467 // we want to count only the number of pages which have cache, but we have 2 files for each page
1468 $mobile_count = $mobile_count / 2;
1469 $desktop_count = $desktop_count / 2;
1470
1471
1472 global $wpdb;
1473 $webp_images = 0;
1474 $webp_images_size = 0;
1475 $webp_images_original_size = 0;
1476
1477 $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'" );
1478 if ( $results ) {
1479 $webp_images = intval( $results->total );
1480 $webp_images_size = intval( $results->total_webp_size );
1481 $webp_images_original_size = intval( $results->total_original_size );
1482 }
1483
1484 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' );
1485 }
1486
1487 /**
1488 * Get the status of the cache
1489 *
1490 * @return array
1491 */
1492 public function get_status() {
1493 global $wpdb;
1494
1495 $wp_cache_enabled = defined( 'WP_CACHE' ) && WP_CACHE;
1496 $adv_cache_exists = file_exists( WP_CONTENT_DIR . '/advanced-cache.php' );
1497 $correct_advanced_cache = $adv_cache_exists && strpos( file_get_contents( WP_CONTENT_DIR . '/advanced-cache.php' ), 'ezCache Advanced Cache' ) !== false;
1498 $webp_table_exists = ! is_null( $wpdb->get_row( "SHOW TABLES LIKE '{$wpdb->prefix}ezcache_webp_images'" ) );
1499
1500 return [
1501 'cache_enabled' => $wp_cache_enabled,
1502 'adv_cache_exists' => $adv_cache_exists,
1503 'correct_cache_exists' => $correct_advanced_cache,
1504 'webp_table_exists' => $webp_table_exists,
1505 ];
1506 }
1507
1508 /**
1509 * Get a path by the URL
1510 *
1511 * @param string $url
1512 *
1513 * @return bool|string
1514 */
1515 public static function url_to_path( $url ) {
1516 $root_dir = trailingslashit( dirname( WP_CONTENT_DIR ) );
1517 $root_url = str_replace( wp_basename( WP_CONTENT_DIR ), '', content_url() );
1518 $url_host = wp_parse_url( $url, PHP_URL_HOST );
1519
1520 // relative path.
1521 if ( null === $url_host ) {
1522 $subdir_levels = substr_count( preg_replace( '/https?:\/\//', '', site_url() ), '/' );
1523 $url = trailingslashit( site_url() . str_repeat( '/..', $subdir_levels ) ) . ltrim( $url, '/' );
1524 }
1525
1526 $root_url = preg_replace( '/^https?:/', '', $root_url );
1527 $url_rep = preg_replace( '/^https?:/', '', $url );
1528 $file = str_replace( $root_url, $root_dir, $url_rep );
1529 $real_path = self::realpath( $file );
1530
1531 if ( ! file_exists( $real_path ) ) {
1532 return false;
1533 }
1534
1535 return $real_path;
1536 }
1537
1538 /**
1539 * Returns canonicalized absolute pathname.
1540 * The resulting path will have no symbolic link, '/./' or '/../' components.
1541 * Same as the defautl PHP realpath() function but works even when the files does not exist.
1542 *
1543 * @param string $file The path being checked.
1544 *
1545 * @return string
1546 * @see \realpath()
1547 *
1548 */
1549 public static function realpath( $file ) {
1550 $path = [];
1551
1552 foreach ( explode( '/', $file ) as $part ) {
1553 if ( '' === $part || '.' === $part ) {
1554 continue;
1555 }
1556
1557 if ( '..' !== $part ) {
1558 array_push( $path, $part );
1559 } elseif ( count( $path ) > 0 ) {
1560 array_pop( $path );
1561 }
1562 }
1563
1564 $prefix = 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ? '' : '/';
1565
1566 return $prefix . join( '/', $path );
1567 }
1568
1569 /**
1570 * Check if Development Mode is active (file-based, works before WP loads)
1571 */
1572 public static function is_dev_mode_active() {
1573 $flag_file = (defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : dirname(__DIR__)) . '/cache/ezcache/.dev-mode';
1574 if ( ! file_exists( $flag_file ) ) {
1575 return false;
1576 }
1577 $expires = (int) trim( @file_get_contents( $flag_file ) );
1578 if ( $expires === 0 ) {
1579 return true;
1580 }
1581 if ( time() >= $expires ) {
1582 @unlink( $flag_file );
1583 return false;
1584 }
1585 return true;
1586 }
1587
1588 public static function enable_dev_mode( $seconds = 3600 ) {
1589 $dir = WP_CONTENT_DIR . '/cache/ezcache';
1590 if ( ! is_dir( $dir ) ) {
1591 @mkdir( $dir, 0755, true );
1592 }
1593 $expires = ( $seconds === 0 ) ? 0 : time() + $seconds;
1594 file_put_contents( $dir . '/.dev-mode', (string) $expires );
1595 }
1596
1597 public static function disable_dev_mode() {
1598 @unlink( WP_CONTENT_DIR . '/cache/ezcache/.dev-mode' );
1599 }
1600
1601 public static function get_dev_mode_status() {
1602 $flag = WP_CONTENT_DIR . '/cache/ezcache/.dev-mode';
1603 if ( ! file_exists( $flag ) ) {
1604 return [ 'active' => false ];
1605 }
1606 $expires = (int) trim( @file_get_contents( $flag ) );
1607 if ( $expires > 0 && time() >= $expires ) {
1608 @unlink( $flag );
1609 return [ 'active' => false ];
1610 }
1611 return [
1612 'active' => true,
1613 'expires' => $expires === 0 ? 'permanent' : $expires,
1614 'remaining' => $expires === 0 ? null : $expires - time(),
1615 ];
1616 }
1617 }
1618