PluginProbe
ezCache / trunk
ezCache vtrunk
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
← All changes | includes/Cache.php +704 -189 1.2trunk View file →
@@ -1,5 +1,6 @@
1 1 <?php
2 +
2 3 namespace Upress\EzCache;
3 4
4 5 use MatthiasMullie\Minify\CSS;
5 6 use MatthiasMullie\Minify\JS;
@@ -7,8 +8,9 @@
7 8 use RecursiveIteratorIterator;
8 9 use RegexIterator;
9 10 use UnexpectedValueException;
10 11 use Upress\EzCache\BackgroundProcesses\ConvertWebpProcess;
12 +use Upress\EzCache\FileOptimizer\CombineGoogleFonts;
11 13 use Upress\EzCache\FileOptimizer\CssMinifier;
12 14 use Upress\EzCache\FileOptimizer\CssCombiner;
13 15 use Upress\EzCache\FileOptimizer\JsMinifier;
14 16 use Upress\EzCache\FileOptimizer\JsCombiner;
@@ -13,8 +15,10 @@
13 15 use Upress\EzCache\FileOptimizer\JsMinifier;
14 16 use Upress\EzCache\FileOptimizer\JsCombiner;
15 17 use Upress\EzCache\FileOptimizer\WebpConverter;
16 18 use Upress\EzCache\ThirdParty\Minify_HTML;
19 +use Upress\EzCache\Utilities\Logger;
20 +use Upress\EzCache\PremiumFeatures;
17 21
18 22 class Cache {
19 23 protected static $instance;
20 24 protected $settings;
@@ -21,9 +25,8 @@
21 25 protected $cache_start_time;
22 26 protected $webp_processor;
23 27 protected $root_cache_dir = WP_CONTENT_DIR . '/cache/ezcache/';
24 28
25 -
26 29 public static function instance() {
27 30 if ( ! self::$instance ) {
28 31 self::$instance = new self();
29 32 }
@@ -31,10 +34,10 @@
31 34 return self::$instance;
32 35 }
33 36
34 37 private function __construct() {
35 - $this->settings = Settings::get_settings();
36 - $this->webp_processor = new ConvertWebpProcess();
38 + $this->settings = Settings::get_settings();
39 + $this->webp_processor = new ConvertWebpProcess();
37 40 $this->cache_start_time = microtime( true );
38 41 }
39 42
40 43 /**
@@ -41,8 +44,9 @@
41 44 * @return string
42 45 */
43 46 public function get_default_cache_path() {
44 47 $hostname = preg_replace( '/:.*$/', '', $this->get_http_host() );
48 +
45 49 return $this->root_cache_dir . $hostname . '/';
46 50 }
47 51
48 52 /**
@@ -62,44 +66,50 @@
62 66 return '';
63 67 }
64 68
65 69 /**
66 - * Get the logged in cookie value
67 - * @return string
70 + * Check if the current user has a log in cookie set (ie. the user is logged in)
71 + * @return bool
68 72 */
69 - public function get_cookies_values() {
70 - static $string = '';
73 + public function has_login_cookie() {
74 + $cookiehash = '';
75 + if ( defined( 'COOKIEHASH' ) ) {
76 + $cookiehash = preg_quote( constant( 'COOKIEHASH' ), '|' );
77 + }
71 78
72 - if ( $string != '' ) {
73 - return $string;
79 + $regex = "|^wordpress_logged_in_{$cookiehash}|";
80 + if ( defined( 'LOGGED_IN_COOKIE' ) ) {
81 + $regex = "|^" . preg_quote( constant( 'LOGGED_IN_COOKIE' ), '|' ) . '|';
74 82 }
75 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 = '';
76 99 if ( defined( 'COOKIEHASH' ) ) {
77 100 $cookiehash = preg_quote( constant( 'COOKIEHASH' ) );
78 - } else {
79 - $cookiehash = '';
80 101 }
81 102
82 - $regex = "/^wp-postpass_{$cookiehash}|^comment_author_{$cookiehash}";
83 - if ( defined( 'LOGGED_IN_COOKIE' ) ) {
84 - $regex .= "|^" . preg_quote( constant( 'LOGGED_IN_COOKIE' ) );
85 - } else {
86 - $regex .= "|^wordpress_logged_in_{$cookiehash}";
87 - }
88 - $regex .= "/";
89 - while ( $key = key( $_COOKIE ) ) {
103 + $regex = "/^wp-postpass_{$cookiehash}|^comment_author_{$cookiehash}/";
104 +
105 + foreach ( $_COOKIE as $key => $value ) {
90 106 if ( preg_match( $regex, $key ) ) {
91 - $string .= $_COOKIE[ $key ] . ",";
107 + return true;
92 108 }
93 - next( $_COOKIE );
94 109 }
95 - reset( $_COOKIE );
96 110
97 - if ( $string != '' ) {
98 - $string = md5( $string );
99 - }
100 -
101 - return $string;
111 + return false;
102 112 }
103 113
104 114 /**
105 115 * Check if the request supports gzip compression
@@ -106,8 +116,12 @@
106 116 *
107 117 * @return bool
108 118 */
109 119 public function gzip_accepted() {
120 + if ( defined( 'EZCACHE_DISABLE_GZIP' ) && EZCACHE_DISABLE_GZIP ) {
121 + return false;
122 + }
123 +
110 124 return isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' );
111 125 }
112 126
113 127 /**
@@ -130,9 +144,9 @@
130 144 }
131 145
132 146 $script = isset( $_SERVER['PHP_SELF'] ) ? basename( $_SERVER['PHP_SELF'] ) : '';
133 147 if ( $script !== 'index.php' ) {
134 - if ( in_array( $script, array( 'wp-login.php', 'xmlrpc.php', 'wp-cron.php' ) ) ) {
148 + if ( in_array( $script, [ 'wp-login.php', 'xmlrpc.php', 'wp-cron.php' ] ) ) {
135 149 return true;
136 150 } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
137 151 return true;
138 152 } elseif ( PHP_SAPI == 'cli' || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
@@ -143,51 +157,126 @@
143 157 return false;
144 158 }
145 159
146 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 + /**
147 187 * Should we serve the cached file
148 188 *
149 189 * @return bool
150 190 */
151 191 public function should_serve_cached_data() {
152 - $settings = $this->settings;
153 -
154 - if ( $settings->no_cache_known_users && $this->get_cookies_values() ) {
192 + // Dev Mode — bypass cache entirely
193 + if ( self::is_dev_mode_active() ) {
155 194 return false;
156 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 + }
157 220
158 - if ( ( isset( $_SERVER['REQUEST_METHOD'] ) && in_array( $_SERVER['REQUEST_METHOD'], [
221 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ( isset( $_SERVER['REQUEST_METHOD'] ) && in_array( $_SERVER['REQUEST_METHOD'], [
222 + 'HEAD',
159 223 'POST',
160 224 'PUT',
161 225 'PATCH',
162 - 'DELETE'
226 + 'DELETE',
163 227 ] ) ) || isset( $_GET['customize_changeset_uuid'] ) || isset( $_POST['wp_customize'] ) ) {
164 228 return false;
165 229 }
166 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 +
167 241 if ( $this->is_backend() ) {
168 242 return false;
169 243 }
170 244
171 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
172 - if ( $settings->no_cache_query_params && ! empty( $_GET )
173 - && ! isset( $_GET['utm_source'], $_GET['utm_medium'], $_GET['utm_campaign'] )
174 - && ! isset( $_GET['utm_expid'] )
175 - && ! isset( $_GET['fb_action_ids'], $_GET['fb_action_types'], $_GET['fb_source'] )
176 - && ! isset( $_GET['gclid'] )
177 - && ! isset( $_GET['permalink_name'] )
178 - && ! isset( $_GET['lp-variation-id'] )
179 - && ! isset( $_GET['lang'] )
180 - && ! isset( $_GET['s'] )
181 - && ! isset( $_GET['age-verified'] )
182 - && ! isset( $_GET['ao_noptimize'] )
183 - && ! isset( $_GET['usqp'] )
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 + )
184 266 ) {
267 +
185 268 return false;
186 269 }
187 270
188 - if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
189 - return false;
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 + }
190 279 }
191 280
192 281 return true;
193 282 }
@@ -198,8 +287,10 @@
198 287 *
199 288 * @return bool
200 289 */
201 290 public function should_save_cache() {
291 + global $wp_query;
292 +
202 293 if ( ! $this->should_serve_cached_data() ) {
203 294 return false;
204 295 }
205 296
@@ -204,21 +295,8 @@
204 295 }
205 296
206 297 $settings = $this->settings;
207 298
208 - if ( ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
209 - return false;
210 - }
211 - if ( ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
212 - return false;
213 - }
214 - if ( ( defined( 'JSON_REQUEST' ) && JSON_REQUEST ) ) {
215 - return false;
216 - }
217 - if ( ( defined( 'WC_API_REQUEST' ) && WC_API_REQUEST ) ) {
218 - return false;
219 - }
220 -
221 299 // check if we have any errors or otherwise settings preventing caching
222 300 $error = error_get_last();
223 301 if ( null !== $error && ( $error['type'] & ( E_ERROR | E_CORE_ERROR | E_PARSE | E_COMPILE_ERROR | E_USER_ERROR ) ) ) {
224 302 return false;
@@ -261,10 +339,16 @@
261 339 }
262 340 if ( $settings->bypass_cache->author && is_author() ) {
263 341 return false;
264 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 + }
265 349
266 - if ( is_robots() || get_query_var( 'sitemap' ) || get_query_var( 'xsl' ) || get_query_var( 'xml_sitemap' ) ) {
350 + if ( is_null( $wp_query ) || is_robots() || get_query_var( 'sitemap' ) || get_query_var( 'xsl' ) || get_query_var( 'xml_sitemap' ) ) {
267 351 return false;
268 352 }
269 353
270 354 if ( isset( $_GET['preview'] ) || isset( $_POST['wp_customize'] ) ) {
@@ -270,18 +354,35 @@
270 354 if ( isset( $_GET['preview'] ) || isset( $_POST['wp_customize'] ) ) {
271 355 return false;
272 356 }
273 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 +
274 373 if ( get_post_meta( get_the_ID(), '_ezcache_do_not_cache_post', true ) ) {
275 374 return false;
276 375 }
277 376
278 377 // check useragent
279 - $rejected_useragents = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_user_agent ), -1, PREG_SPLIT_NO_EMPTY );
378 + $rejected_useragents = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_user_agent ), - 1, PREG_SPLIT_NO_EMPTY );
280 379 $rejected_useragents = array_filter( $rejected_useragents );
281 380 if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
282 - foreach( $rejected_useragents as $ua ) {
283 - if ( empty( $ua ) ) continue;
381 + foreach ( $rejected_useragents as $ua ) {
382 + if ( empty( $ua ) ) {
383 + continue;
384 + }
284 385
285 386 if ( false !== strpos( $_SERVER['HTTP_USER_AGENT'], trim( $ua ) ) ) {
286 387 return false;
287 388 }
@@ -288,17 +389,26 @@
288 389 }
289 390 }
290 391
291 392 // check URL
292 - $rejected_uris = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_uri ), -1, PREG_SPLIT_NO_EMPTY );
393 + $rejected_uris = preg_split( "/\\r\\n|\\r|\\n/u", trim( $settings->rejected_uri ), - 1, PREG_SPLIT_NO_EMPTY );
293 394 $rejected_uris = array_filter( $rejected_uris );
294 - $domain = untrailingslashit( home_url() );
395 + $domain = untrailingslashit( home_url() );
295 396 if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
296 - foreach( $rejected_uris as $url ) {
397 + foreach ( $rejected_uris as $url ) {
297 398 $url = str_replace( $domain, '', $url );
298 399 $url = '/' . trim( $url, '/' );
299 - $url = str_replace( ['\/*', '*'], ['\/?.*?', '.*?'], preg_quote( $url, '/' ) );
300 - if ( @preg_match( "/^{$url}\/?$/", $_SERVER['REQUEST_URI']) ) {
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'] ) ) ) {
301 411 return false;
302 412 }
303 413 }
304 414 }
@@ -367,11 +477,11 @@
367 477 'BlackBerry9530',
368 478 'LG-TU915 Obigo',
369 479 'LGE VX',
370 480 'webOS',
371 - 'Nokia5800'
481 + 'Nokia5800',
372 482 ] );
373 - $user_agent = strtolower( $_SERVER['HTTP_USER_AGENT'] );
483 + $user_agent = strtolower( $_SERVER['HTTP_USER_AGENT'] );
374 484 foreach ( $mobile_browsers as $browser ) {
375 485 if ( strstr( $user_agent, trim( strtolower( $browser ) ) ) ) {
376 486 return $user_agent;
377 487 }
@@ -472,9 +582,9 @@
472 582 'webc',
473 583 'winw',
474 584 'winw',
475 585 'xda',
476 - 'xda-'
586 + 'xda-',
477 587 ] );
478 588 foreach ( $browser_prefixes as $prefix ) {
479 589 if ( substr( $user_agent, 0, 4 ) == $prefix ) {
480 590 return $prefix;
@@ -516,18 +626,20 @@
516 626 * Get the cache directory URL for the current post
517 627 *
518 628 * @param int $post_id
519 629 *
630 + * @param null|string $url
631 + *
520 632 * @return mixed|string
521 633 */
522 - public function get_current_url_cache_dir( $post_id = 0 ) {
523 - static $url_cache_dir = array();
634 + public function get_current_url_cache_dir( $post_id = 0, $url = null ) {
635 + static $url_cache_dir = [];
524 636
525 637 if ( isset( $url_cache_dir[ $post_id ] ) ) {
526 638 return $url_cache_dir[ $post_id ];
527 639 }
528 640
529 - $uri = strtolower( $_SERVER['REQUEST_URI'] );
641 + $uri = strtolower( $url ? ( '/' . ltrim( $url, '/' ) ) : $_SERVER['REQUEST_URI'] );
530 642
531 643 $DONOTREMEMBER = 0;
532 644 if ( 0 !== $post_id ) {
533 645 $site_url = site_url();
@@ -552,12 +664,12 @@
552 664 $uri = $this->deep_replace(
553 665 [
554 666 '..',
555 667 '\\',
556 - 'index.php'
668 + 'index.php',
557 669 ],
558 670 preg_replace(
559 - '/[ <>\'\"\r\n\t\(\)]/',
671 + '/[ <>\'\"\r\n\t()]/',
560 672 '',
561 673 preg_replace( "/(\?.*)?(#.*)?$/", '', $uri )
562 674 )
563 675 );
@@ -576,12 +688,14 @@
576 688 * Get the cache directory path
577 689 *
578 690 * @param int $postid
579 691 *
692 + * @param null|string $url
693 + *
580 694 * @return string
581 695 */
582 - public function get_real_cache_dir( $postid = 0 ) {
583 - return $this->get_default_cache_path() . $this->get_current_url_cache_dir( $postid );
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 );
584 698 }
585 699
586 700 /**
587 701 * Get the full cache file path
@@ -587,12 +701,14 @@
587 701 * Get the full cache file path
588 702 *
589 703 * @param int $postid
590 704 *
705 + * @param null|string $url
706 + *
591 707 * @return string
592 708 */
593 - public function get_cache_file_path( $postid = 0 ) {
594 - return $this->get_real_cache_dir( $postid ) . $this->get_cache_filename();
709 + public function get_cache_file_path( $postid = 0, $url = null ) {
710 + return $this->get_real_cache_dir( $postid, $url ) . $this->get_cache_filename();
595 711 }
596 712
597 713 /**
598 714 * Get the filename for the cached file
@@ -598,8 +714,116 @@
598 714 * Get the filename for the cached file
599 715 *
600 716 * @return string
601 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 +
602 826 public function get_cache_filename() {
603 827 $settings = $this->settings;
604 828
605 829 // Add support for https and http caching
@@ -613,15 +837,20 @@
613 837 $extra_str .= '-mobile';
614 838 }
615 839 }
616 840
617 - if ( $settings->enable_webp_support && $this->webp_accepted()) {
841 + if ( $settings->enable_webp_support && $this->webp_accepted() ) {
618 842 $extra_str .= '-webp';
619 843 }
620 844
621 845 $filename = 'index';
622 846 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
623 - $filename = md5( $_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 + }
624 853 }
625 854
626 855 return $filename . $extra_str . '.html';
627 856 }
@@ -633,14 +862,32 @@
633 862 if ( ! $this->should_serve_cached_data() ) {
634 863 return;
635 864 }
636 865
637 - $cache_file = $this->get_cache_file_path();
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();
638 886 $gzip_accepted = $this->gzip_accepted();
639 887
640 - $serve_gzip = $gzip_accepted && file_exists( $cache_file . '.gz' );
641 - $cache_file = $serve_gzip ? ($cache_file . '.gz') : $cache_file;
642 - $filesize = file_exists( $cache_file ) ? @filesize( $cache_file ) : false;
888 + $cache_file = $cache_file . '.gz';
889 + $filesize = file_exists( $cache_file ) ? @filesize( $cache_file ) : false;
643 890
644 891 if ( ! $filesize ) {
645 892 // the file is empty, we have nothing to serve
646 893 return;
@@ -647,9 +894,8 @@
647 894 }
648 895
649 896 header( "X-Cached-With: ezCache" );
650 897 header( "Vary: Accept-Encoding, Cookie" );
651 -// header( "Content-Length: {$filesize}" );
652 898 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', filemtime( $cache_file ) ) . ' GMT' );
653 899
654 900 // Getting If-Modified-Since headers sent by the client.
655 901 if ( function_exists( 'apache_request_headers' ) ) {
@@ -666,19 +912,56 @@
666 912 exit;
667 913 }
668 914
669 915 // Serve the cache if file isn't store in the client browser cache.
670 - if ( $serve_gzip ) {
916 + // if the browser does not support gzip read the file and output it without gzip encoding
917 + if ( ! $gzip_accepted ) {
671 918 readgzfile( $cache_file );
672 919 exit;
673 920 }
674 921
922 + // otherwise output the gzipped file as-is
923 + header( "Content-Length: {$filesize}" );
924 + header( "Content-Encoding: gzip" );
675 925 readfile( $cache_file );
676 926 exit;
677 927 }
678 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 +
679 961 /**
680 962 * Write cache file if we need to
963 + * @noinspection PhpUnused
681 964 */
682 965 public function maybe_write_cache_file() {
683 966 if ( ! $this->should_serve_cached_data() ) {
684 967 return;
@@ -701,17 +984,41 @@
701 984 if ( ! $this->should_save_cache() ) {
702 985 return $buffer;
703 986 }
704 987
705 - $real_cache_dir = $this->get_real_cache_dir();
706 - $cache_file = $this->get_cache_file_path();
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';
707 1009 $asset_cache_dir = $this->get_default_cache_path() . 'min/';
708 - $asset_cache_url = trailingslashit( trailingslashit( get_home_url() ) . trim( str_replace( dirname( WP_CONTENT_DIR ), '', $asset_cache_dir ), '/' ) );
709 - $settings = $this->settings;
1010 + $asset_cache_url = trailingslashit( trailingslashit( get_site_url() ) . trim( str_replace( dirname( WP_CONTENT_DIR ), '', $asset_cache_dir ), '/' ) );
1011 + $settings = $this->settings;
710 1012
1013 + if ( $settings->optimize_google_fonts ) {
1014 + $optimizer = new CombineGoogleFonts();
1015 + $buffer = $optimizer->optimize( $buffer );
1016 + }
1017 +
711 1018 if ( $settings->minify_css ) {
712 1019 if ( $settings->combine_css ) {
713 - $optimizer = new CssCombiner( $asset_cache_dir, $asset_cache_url );
1020 + $optimizer = new CssCombiner( $asset_cache_dir, $asset_cache_url, $settings->combine_css_footer );
714 1021 } else {
715 1022 $optimizer = new CssMinifier( $asset_cache_dir, $asset_cache_url );
716 1023 }
717 1024
@@ -719,40 +1026,48 @@
719 1026 }
720 1027
721 1028 if ( $settings->minify_js ) {
722 1029 if ( $settings->combine_head_js ) {
723 - $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'head' );
724 - $buffer = $optimizer->optimize( $buffer );
1030 + $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'head', $settings->combine_head_inline_js );
1031 + $buffer = $optimizer->optimize( $buffer );
725 1032 }
726 1033
727 1034 if ( $settings->combine_body_js ) {
728 - $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'body' );
729 - $buffer = $optimizer->optimize( $buffer );
1035 + $optimizer = new JsCombiner( $asset_cache_dir, $asset_cache_url, 'body', $settings->combine_body_inline_js );
1036 + $buffer = $optimizer->optimize( $buffer );
730 1037 }
731 1038
732 - if( ! $settings->combine_head_js && ! $settings->combine_body_js ) {
1039 + if ( ! $settings->combine_head_js && ! $settings->combine_body_js ) {
733 1040 $optimizer = new JsMinifier( $asset_cache_dir, $asset_cache_url );
734 - $buffer = $optimizer->optimize( $buffer );
1041 + $buffer = $optimizer->optimize( $buffer );
735 1042 }
736 1043 }
737 1044
738 1045 if ( $settings->minify_html ) {
1046 + wp_raise_memory_limit( 'image' );
1047 +
739 1048 $buffer = Minify_HTML::minify( $buffer, [
740 - 'cssMinifier' => function( $css ) use ($settings) {
1049 + 'htmlCleanComments' => $settings->minify_html_comments,
1050 +
1051 + 'cssMinifier' => function ( $css ) use ( $settings ) {
741 1052 if ( ! $settings->minify_inline_css ) {
742 1053 return $css;
743 1054 }
744 1055
745 1056 $minifier = new CSS( $css );
1057 + $minifier->setMaxImportSize( 0 );
1058 + $minifier->setImportExtensions( [] );
1059 +
746 1060 return $minifier->minify();
747 1061 },
748 1062
749 - 'jsMinifier' => function( $js ) use ($settings) {
1063 + 'jsMinifier' => function ( $js ) use ( $settings ) {
750 1064 if ( ! $settings->minify_inline_js ) {
751 1065 return $js;
752 1066 }
753 1067
754 1068 $minifier = new JS( $js );
1069 +
755 1070 return $minifier->minify();
756 1071 },
757 1072 ] );
758 1073 }
@@ -758,37 +1073,60 @@
758 1073 }
759 1074
760 1075 if ( $settings->enable_webp_support && $this->webp_accepted() ) {
761 1076 $optimizer = new WebpConverter( $real_cache_dir, $cache_file, $this->webp_processor, $wpdb );
762 - $buffer = $optimizer->optimize( $buffer );
1077 + $buffer = $optimizer->optimize( $buffer );
763 1078 }
764 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 +
765 1087 if ( ! apply_filters( 'wp_bost_hide_cache_time_comment', false ) ) {
766 1088 $total_time = number_format( microtime( true ) - $this->cache_start_time, 2 );
767 - $buffer .= "\n<!-- Cached by ezCache -->\n<!-- Cache created in {$total_time}s -->";
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 -->";
768 1091 }
769 1092
770 1093 $buffer = apply_filters( 'ezcache_before_save_cache', $buffer );
771 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 +
772 1107 if ( ! file_exists( $real_cache_dir ) ) {
773 1108 if ( ! @wp_mkdir_p( $real_cache_dir ) ) {
774 - error_log( 'ezCache could not create directory ' . $real_cache_dir );
1109 + Logger::log( 'ezCache could not create directory ' . $real_cache_dir );
1110 +
775 1111 return $buffer;
776 1112 }
777 1113 }
778 1114
779 - // write regular file
780 - if ( ! @file_put_contents( $cache_file, $buffer ) ) {
781 - error_log( 'ezCache could not write to ' . str_replace( ABSPATH, '', $cache_file ) );
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 ) );
782 1123 }
783 1124
784 - // write gzipped file
785 - if ( ! @file_put_contents( $cache_file . '.gz', gzencode( $buffer, 6, FORCE_GZIP ) ) ) {
786 - error_log( 'ezCache could not write to ' . str_replace( ABSPATH, '', $cache_file . '.gz' ) );
1125 + if ( $handle ) {
1126 + fclose( $handle );
787 1127 }
788 1128
789 - $this->webp_processor->dispatch();
790 -
791 1129 return $buffer;
792 1130 }
793 1131
794 1132 /**
@@ -802,10 +1140,15 @@
802 1140 }
803 1141
804 1142 $files = glob( $path . '/*' );
805 1143 foreach ( $files as $file ) {
806 - is_dir( $file ) ? $this->rmdir_recursive( $file ) : unlink( $file );
1144 + if ( file_exists( $file ) && is_dir( $file ) ) {
1145 + $this->rmdir_recursive( $file );
1146 + } elseif ( file_exists( $file ) ) {
1147 + unlink( $file );
1148 + }
807 1149 }
1150 +
808 1151 rmdir( $path );
809 1152 }
810 1153
811 1154 /**
@@ -820,36 +1163,39 @@
820 1163 'ezcache_mobile_useragent',
821 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)'
822 1165 );
823 1166
824 - wp_safe_remote_get( home_url(), [
1167 + wp_safe_remote_get( site_url(), [
825 1168 'user-agent' => $desktop_ua,
826 - 'timeout' => 0.1,
1169 + 'timeout' => 0.1,
827 1170 ] );
828 1171
829 - wp_safe_remote_get( home_url(), [
1172 + wp_safe_remote_get( site_url(), [
830 1173 'user-agent' => $mobile_ua,
831 - 'timeout' => 0.1,
1174 + 'timeout' => 0.1,
832 1175 ] );
833 1176 }
834 1177
835 - function delete_missing_webp_images() {
1178 + function delete_missing_webp_images( $delete_all = false ) {
836 1179 global $wpdb;
837 1180
838 - $ids = [0];
839 - $images = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'completed'" );
840 - foreach( $images as $image ) {
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 ) {
841 1186 if ( ! file_exists( $image->webp_path ) ) {
842 1187 $ids[] = $image->id;
843 - } elseif ( ! file_exists( $image->path ) && file_exists( $image->webp_path ) ) {
1188 + } elseif ( ( $delete_all || ! file_exists( $image->path ) ) && file_exists( $image->webp_path ) ) {
844 1189 unlink( $image->webp_path );
845 1190 $ids[] = $image->id;
846 1191 }
847 1192 }
848 1193
1194 + // clean the database
849 1195 $wpdb->query(
850 1196 $wpdb->prepare(
851 - "DELETE FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'failed' OR `id` IN ( " . substr( str_repeat( "%d, ", count( $ids ) ), 0, -2 ) . " )",
1197 + "DELETE FROM `{$wpdb->prefix}ezcache_webp_images` WHERE `status` = 'failed' OR `id` IN ( " . substr( str_repeat( "%d, ", count( $ids ) ), 0, - 2 ) . " )",
852 1198 $ids
853 1199 )
854 1200 );
855 1201
@@ -855,20 +1201,42 @@
855 1201
856 1202 $wpdb->query( "OPTIMIZE TABLE `{$wpdb->prefix}ezcache_webp_images`" );
857 1203 }
858 1204
1205 + function delete_all_webp_images() {
1206 + $this->delete_missing_webp_images( true );
1207 + }
1208 +
859 1209 /**
860 1210 * Clear all caches
1211 + *
1212 + * @param bool $clear_webp Should deleting cache clear the WebP images
861 1213 */
862 - public function clear_cache() {
863 - $cache_dir = $this->get_default_cache_path();
1214 + public function clear_cache( $clear_webp = false ) {
1215 + $this->rmdir_recursive( $this->root_cache_dir );
1216 + @wp_mkdir_p( $this->root_cache_dir );
864 1217
865 - $this->rmdir_recursive( $cache_dir );
866 - @wp_mkdir_p( $cache_dir );
1218 + if ( $clear_webp ) {
1219 + $this->delete_all_webp_images();
1220 + } else {
1221 + $this->delete_missing_webp_images();
1222 + }
867 1223
868 - $this->delete_missing_webp_images();
1224 + $this->purge_varnish_cache();
869 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 +
870 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' );
871 1239 }
872 1240
873 1241 /**
874 1242 * Clear cache for a single post
@@ -878,10 +1246,104 @@
878 1246 public function clear_cache_single( $post_id ) {
879 1247 $real_cache_dir = $this->get_real_cache_dir( $post_id );
880 1248
881 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 );
882 1267 }
883 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 +
884 1346 /**
885 1347 * Delete expired cache
886 1348 */
887 1349 public function clear_expired_cache() {
@@ -887,12 +1349,12 @@
887 1349 public function clear_expired_cache() {
888 1350 $settings = $this->settings;
889 1351
890 1352 try {
891 - $dir = new RecursiveDirectoryIterator( $this->root_cache_dir );
892 - $ite = new RecursiveIteratorIterator( $dir );
893 - $files = new RegexIterator( $ite, '/^.+\.(?:gz|html|js|css)$/i', RegexIterator::GET_MATCH );
894 - } catch( UnexpectedValueException $ex) {
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 ) {
895 1357 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
896 1358 $files = [];
897 1359 } else {
898 1360 throw $ex;
@@ -898,9 +1360,9 @@
898 1360 throw $ex;
899 1361 }
900 1362 }
901 1363
902 - foreach($files as $file) {
1364 + foreach ( $files as $file ) {
903 1365 if ( is_array( $file ) ) {
904 1366 $file = array_shift( $file );
905 1367 }
906 1368
@@ -918,16 +1380,16 @@
918 1380 * Get caching statistics and file sizes
919 1381 * @return array
920 1382 */
921 1383 public function get_cache_stats() {
922 - $settings = $this->settings;
1384 + $settings = $this->settings;
923 1385 $cache_dir = $this->get_default_cache_path();
924 1386
925 1387 try {
926 - $dir = new RecursiveDirectoryIterator( $cache_dir );
927 - $ite = new RecursiveIteratorIterator( $dir );
928 - $files = new RegexIterator( $ite, '/^.+\.(?:gz|html|css|js)$/i', RegexIterator::GET_MATCH );
929 - } catch( UnexpectedValueException $ex) {
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 ) {
930 1392 if ( strpos( $ex->getMessage(), 'No such file or directory' ) ) {
931 1393 $files = [];
932 1394 } else {
933 1395 throw $ex;
@@ -935,58 +1397,58 @@
935 1397 }
936 1398
937 1399 $raw_data = [];
938 1400
939 - $mobile_count = 0;
940 - $mobile_size = 0;
941 - $mobile_expired_count = 0;
942 - $mobile_expired_size = 0;
943 - $desktop_count = 0;
944 - $desktop_size = 0;
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;
945 1407 $desktop_expired_count = 0;
946 - $desktop_expired_size = 0;
947 - $js_count = 0;
948 - $js_size = 0;
949 - $js_expired_count = 0;
950 - $js_expired_size = 0;
951 - $css_count = 0;
952 - $css_size = 0;
953 - $css_expired_count = 0;
954 - $css_expired_size = 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;
955 1417
956 - foreach($files as $file) {
1418 + foreach ( $files as $file ) {
957 1419 if ( is_array( $file ) ) {
958 1420 $file = array_shift( $file );
959 1421 }
960 1422
961 - $stats = stat( $file );
962 - $expired = $stats['mtime'] <= (time() - $settings->cache_lifetime);
1423 + $stats = stat( $file );
1424 + $expired = $stats['mtime'] <= ( time() - $settings->cache_lifetime );
963 1425
964 1426 $raw_data[] = [
965 - 'path' => $file,
966 - 'stats' => $stats,
1427 + 'path' => $file,
1428 + 'stats' => $stats,
967 1429 'expired' => $expired,
968 1430 ];
969 1431
970 1432 if ( preg_match( '/^.+?-mobile\.html(\.gz)?$/i', $file ) ) {
971 1433 if ( ! $expired ) {
972 - $mobile_count++;
1434 + $mobile_count ++;
973 1435 $mobile_size += $stats['size'];
974 1436 } else {
975 - $mobile_expired_count++;
1437 + $mobile_expired_count ++;
976 1438 $mobile_expired_size += $stats['size'];
977 1439 }
978 - } elseif( preg_match( '/\.css$/i', $file ) ) {
1440 + } elseif ( preg_match( '/\.css$/i', $file ) ) {
979 1441 if ( $expired ) {
980 - $css_expired_count++;
1442 + $css_expired_count ++;
981 1443 $css_expired_size += $stats['size'];
982 1444 } else {
983 1445 $css_count ++;
984 1446 $css_size += $stats['size'];
985 1447 }
986 - } elseif( preg_match( '/\.js$/i', $file ) ) {
1448 + } elseif ( preg_match( '/\.js$/i', $file ) ) {
987 1449 if ( $expired ) {
988 - $js_expired_count++;
1450 + $js_expired_count ++;
989 1451 $js_expired_size += $stats['size'];
990 1452 } else {
991 1453 $js_count ++;
992 1454 $js_size += $stats['size'];
@@ -992,12 +1454,12 @@
992 1454 $js_size += $stats['size'];
993 1455 }
994 1456 } else {
995 1457 if ( ! $expired ) {
996 - $desktop_count++;
1458 + $desktop_count ++;
997 1459 $desktop_size += $stats['size'];
998 1460 } else {
999 - $desktop_expired_count++;
1461 + $desktop_expired_count ++;
1000 1462 $desktop_expired_size += $stats['size'];
1001 1463 }
1002 1464 }
1003 1465 }
@@ -1002,15 +1464,15 @@
1002 1464 }
1003 1465 }
1004 1466
1005 1467 // we want to count only the number of pages which have cache, but we have 2 files for each page
1006 - $mobile_count = $mobile_count / 2;
1468 + $mobile_count = $mobile_count / 2;
1007 1469 $desktop_count = $desktop_count / 2;
1008 1470
1009 1471
1010 1472 global $wpdb;
1011 - $webp_images = 0;
1012 - $webp_images_size = 0;
1473 + $webp_images = 0;
1474 + $webp_images_size = 0;
1013 1475 $webp_images_original_size = 0;
1014 1476
1015 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'" );
1016 1478 if ( $results ) {
@@ -1018,9 +1480,9 @@
1018 1480 $webp_images_size = intval( $results->total_webp_size );
1019 1481 $webp_images_original_size = intval( $results->total_original_size );
1020 1482 }
1021 1483
1022 - 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' );
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' );
1023 1485 }
1024 1486
1025 1487 /**
1026 1488 * Get the status of the cache
@@ -1027,16 +1489,20 @@
1027 1489 *
1028 1490 * @return array
1029 1491 */
1030 1492 public function get_status() {
1031 - $wp_cache_enabled = defined( 'WP_CACHE' ) && WP_CACHE;
1032 - $adv_cache_exists = file_exists( WP_CONTENT_DIR . '/advanced-cache.php' );
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' );
1033 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'" ) );
1034 1499
1035 1500 return [
1036 - 'cache_enabled' => $wp_cache_enabled,
1037 - 'adv_cache_exists' => $adv_cache_exists,
1501 + 'cache_enabled' => $wp_cache_enabled,
1502 + 'adv_cache_exists' => $adv_cache_exists,
1038 1503 'correct_cache_exists' => $correct_advanced_cache,
1504 + 'webp_table_exists' => $webp_table_exists,
1039 1505 ];
1040 1506 }
1041 1507
1042 1508 /**
@@ -1047,26 +1513,27 @@
1047 1513 * @return bool|string
1048 1514 */
1049 1515 public static function url_to_path( $url ) {
1050 1516 $root_dir = trailingslashit( dirname( WP_CONTENT_DIR ) );
1051 - $root_url = str_replace( wp_basename( WP_CONTENT_DIR ), '', WP_CONTENT_URL );
1517 + $root_url = str_replace( wp_basename( WP_CONTENT_DIR ), '', content_url() );
1052 1518 $url_host = wp_parse_url( $url, PHP_URL_HOST );
1053 1519
1520 + // relative path.
1054 1521 if ( null === $url_host ) {
1055 - $subdir_levels = substr_count( preg_replace( '/https?:\/\//','', site_url() ), '/' );
1056 - $url = site_url() . str_repeat( '/..', $subdir_levels ) . $url;
1522 + $subdir_levels = substr_count( preg_replace( '/https?:\/\//', '', site_url() ), '/' );
1523 + $url = trailingslashit( site_url() . str_repeat( '/..', $subdir_levels ) ) . ltrim( $url, '/' );
1057 1524 }
1058 1525
1059 - $root_url = preg_replace( '/^https?:/', '', $root_url );
1060 - $url = preg_replace( '/^https?:/', '', $url );
1061 - $file = str_replace( $root_url, $root_dir, $url );
1062 - $file = self::realpath( $file );
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 );
1063 1530
1064 - if ( ! file_exists( $file ) ) {
1531 + if ( ! file_exists( $real_path ) ) {
1065 1532 return false;
1066 1533 }
1067 1534
1068 - return $file;
1535 + return $real_path;
1069 1536 }
1070 1537
1071 1538 /**
1072 1539 * Returns canonicalized absolute pathname.
@@ -1072,16 +1539,16 @@
1072 1539 * Returns canonicalized absolute pathname.
1073 1540 * The resulting path will have no symbolic link, '/./' or '/../' components.
1074 1541 * Same as the defautl PHP realpath() function but works even when the files does not exist.
1075 1542 *
1076 - * @see \realpath()
1077 - *
1078 1543 * @param string $file The path being checked.
1079 1544 *
1080 1545 * @return string
1546 + * @see \realpath()
1547 + *
1081 1548 */
1082 1549 public static function realpath( $file ) {
1083 - $path = array();
1550 + $path = [];
1084 1551
1085 1552 foreach ( explode( '/', $file ) as $part ) {
1086 1553 if ( '' === $part || '.' === $part ) {
1087 1554 continue;
@@ -1088,10 +1555,9 @@
1088 1555 }
1089 1556
1090 1557 if ( '..' !== $part ) {
1091 1558 array_push( $path, $part );
1092 - }
1093 - elseif ( count( $path ) > 0 ) {
1559 + } elseif ( count( $path ) > 0 ) {
1094 1560 array_pop( $path );
1095 1561 }
1096 1562 }
1097 1563
@@ -1097,6 +1563,55 @@
1097 1563
1098 1564 $prefix = 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) ? '' : '/';
1099 1565
1100 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 + ];
1101 1616 }
1102 1617 }