PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / cache / file-based-page-cache-functions.php

file-based-page-cache-functions.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at cache/file-based-page-cache-functions.php

2,174 lines 76.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('No direct access allowed');
4
5 /**
6 * Extensions directory.
7 */
8 if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', dirname(__FILE__).'/extensions');
9
10 /**
11 * Directory that stores the cache, including gzipped files and mobile specific cache
12 */
13 if (!defined('WPO_CACHE_FILES_DIR')) define('WPO_CACHE_FILES_DIR', untrailingslashit(WP_CONTENT_DIR).'/cache/wpo-cache');
14
15 /**
16 * Minimum Firefox version for WebP support
17 */
18 if (!defined('WPO_MIN_FIREFOX_VERSION_FOR_WEBP')) define('WPO_MIN_FIREFOX_VERSION_FOR_WEBP', '65.0.0');
19
20 /**
21 * Holds utility functions used by file based cache
22 */
23
24 /**
25 * Cache output before it goes to the browser. If moving/renaming this function, then also change the check above.
26 *
27 * @param String $buffer Page HTML.
28 * @param Int $flags OB flags to be passed through.
29 *
30 * @return String
31 */
32 if (!function_exists('wpo_cache')) :
33 function wpo_cache($buffer, $flags) {
34
35 // This case appears to happen for unclear reasons without WP being fully loaded, e.g. https://wordpress.org/support/topic/fatal-error-since-wp-5-8-update/ . It is simplest just to short-circuit it.
36 if ('' === $buffer) return '';
37
38 // This array records reasons why no caching took place. Be careful not to allow actions to proceed that should not - i.e. take note of its state appropriately.
39 $no_cache_because = array();
40
41 if (strlen($buffer) < 255) {
42 // translators: %s is the number of bytes
43 $no_cache_because[] = sprintf(__('Output is too small (less than %d bytes) to be worth caching', 'wp-optimize'), 255);
44 }
45
46 if (defined('REST_REQUEST') && REST_REQUEST) {
47 if (!wpo_rest_caching_enabled()) {
48 $no_cache_because[] = __('This is a REST API request (identified by REST_REQUEST constant) and you have not enabled REST API caching', 'wp-optimize');
49 } else {
50 // Don't process REST requests here
51 return $buffer;
52 }
53 }
54
55 $restricted_page_type_cache = apply_filters('wpo_restricted_cache_page_type', false);
56
57 if ($restricted_page_type_cache) {
58 $no_cache_because[] = $restricted_page_type_cache;
59 }
60
61 $conditional_tag_exceptions = apply_filters('wpo_url_in_conditional_tags_exceptions', false);
62
63 if ($conditional_tag_exceptions) {
64 $no_cache_because[] = $conditional_tag_exceptions;
65 }
66
67 // Don't cache pages for logged in users.
68 if (!function_exists('is_user_logged_in') || (function_exists('wp_get_current_user') && is_user_logged_in())) {
69 if (!wpo_cache_loggedin_users()) {
70 $no_cache_because[] = __('User is logged in', 'wp-optimize');
71 } elseif (!empty($GLOBALS['wpo_cache_config']['enable_user_caching'])) {
72 // Will only run when "Serve cached pages to logged in users" is checked
73 $no_cache_because[] = __('User is logged in, this works only when the cache is preloaded', 'wp-optimize');
74 }
75 }
76
77 // No root cache folder, so short-circuit here
78 if (!file_exists(WPO_CACHE_DIR)) {
79 $no_cache_because[] = __('WP-O cache parent directory was not found', 'wp-optimize').' ('.WPO_CACHE_DIR.')';
80 } elseif (!file_exists(WPO_CACHE_FILES_DIR)) {
81 // Try creating a folder for cached files, if it was flushed recently
82 if (!mkdir(WPO_CACHE_FILES_DIR)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- wp_mkdir_p not available this early
83 $no_cache_because[] = __('WP-O cache directory was not found', 'wp-optimize').' ('.WPO_CACHE_FILES_DIR.')';
84 } else {
85 wpo_disable_cache_directories_viewing();
86 }
87 }
88
89 // If comments are opened and the user has saved his information.
90 if (function_exists('comments_open') && function_exists('get_post') && get_post() && comments_open()) {
91 $commenter = wp_get_current_commenter();
92 // if any of the fields contain something, do not save to cache
93 if ('' !== $commenter['comment_author'] || '' !== $commenter['comment_author_email'] || '' !== $commenter['comment_author_url']) {
94 $no_cache_because[] = __('Comments are opened and the visitor saved his information.', 'wp-optimize');
95 }
96 }
97
98 if (wpo_restricted_cache_page_with_cart_items()) {
99 $no_cache_because[] = __('User has items in WooCommerce cart.', 'wp-optimize');
100 }
101
102 $can_cache_page = true;
103
104 if (defined('DONOTCACHEPAGE') && DONOTCACHEPAGE) {
105 $can_cache_page = false;
106 }
107
108 /**
109 * Defines if the page can be cached or not
110 *
111 * @param boolean $can_cache_page
112 */
113 $can_cache_page_filter = apply_filters('wpo_can_cache_page', $can_cache_page);
114
115 if (!$can_cache_page_filter) {
116 if ($can_cache_page) {
117 $can_cache_page = false;
118 $no_cache_because[] = __('wpo_can_cache_page filter forbade it', 'wp-optimize');
119 } else {
120 $no_cache_because[] = __('DONOTCACHEPAGE constant forbade it and wpo_can_cache_page filter did not over-ride it', 'wp-optimize');
121 }
122 }
123
124 // Don't cache with fatal error pages.
125 $last_error = error_get_last();
126 if (is_array($last_error) && E_ERROR === $last_error['type']) {
127 $no_cache_because[] = __('This page has a fatal error', 'wp-optimize');
128 }
129
130 if (http_response_code() >= 500) {
131 // translators: %s is the HTTP response code for critical errors
132 $no_cache_because[] = sprintf(__('This page has a critical error (HTTP code %s)', 'wp-optimize'), http_response_code());
133 } elseif (http_response_code() >= 400) {
134 // translators: %s is the HTTP response code for unauthorised access
135 $no_cache_because[] = sprintf(__('This page returned an HTTP unauthorised response code (%s)', 'wp-optimize'), http_response_code());
136 }
137
138 // Get cache file name
139 $file_ext = '.html';
140 $is_feed_cache = false;
141
142 if (wpo_feeds_caching_enabled()) {
143 if (is_feed()) {
144 $file_ext = '.rss-xml';
145 $is_feed_cache = true;
146 }
147 }
148
149 $cache_filename = wpo_cache_filename($file_ext);
150
151 if (defined('WPO_CACHE_DONT_PROCESS_THIS_PAGE') && WPO_CACHE_DONT_PROCESS_THIS_PAGE) {
152 $no_cache_because[] = __('The WPO_CACHE_DONT_PROCESS_THIS_PAGE constant is set.', 'wp-optimize');
153 }
154
155 if (empty($no_cache_because)) {
156
157 $buffer = apply_filters('wpo_pre_cache_buffer', $buffer, $flags);
158
159 $url_path = wpo_get_url_path();
160
161 $path = WPO_CACHE_FILES_DIR . '/' .$url_path;
162
163 if (!wp_mkdir_p($path)) {
164 $no_cache_because[] = __('Attempt to create subfolder within cache directory failed', 'wp-optimize').' ('.$url_path.')';
165 }
166 }
167
168 if (!empty($no_cache_because)) {
169
170 if (function_exists('do_action')) {
171 do_action('wpo_page_not_cached', $no_cache_because);
172 }
173 $message = implode(', ', $no_cache_because);
174
175 // Add http headers
176 wpo_cache_add_nocache_http_header($message);
177
178 if ((!defined('DOING_CRON') || !DOING_CRON) && (!defined('REST_REQUEST') || !REST_REQUEST)) {
179 $not_cached_details = '';
180
181 // Output the reason only when the user has turned on debugging
182 if (((defined('WP_DEBUG') && WP_DEBUG) || isset($_GET['wpo_cache_debug']))) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not using the value, only checks for existence
183 $not_cached_details = "because: ".htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . " ";
184 }
185
186 $buffer .= sprintf("\n<!-- WP Optimize page cache - https://teamupdraft.com/wp-optimize/ - page NOT cached %s-->\n", $not_cached_details);
187 }
188
189 return $buffer;
190
191 } else {
192
193 // Prevent mixed content when there's an http request but the site URL uses https.
194 $home_url = get_home_url();
195
196 if (!is_ssl() && 'https' === strtolower(parse_url($home_url, PHP_URL_SCHEME))) { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url not available this early
197 $https_home_url = $home_url;
198 $http_home_url = str_ireplace('https://', 'http://', $https_home_url);
199 $buffer = str_replace(esc_url($http_home_url), esc_url($https_home_url), $buffer);
200 }
201
202 $modified_time = time(); // Take this as soon before writing as possible
203 $timezone_string = '';
204 $utc = isset($GLOBALS['wpo_cache_config']['gmt_offset']) ? (float) $GLOBALS['wpo_cache_config']['gmt_offset'] : 0;
205 $modified_time += $utc * 3600;
206
207 if (!empty($GLOBALS['wpo_cache_config']['timezone_string'])) {
208 $timezone_string = 'UTC' !== $GLOBALS['wpo_cache_config']['timezone_string'] ? $GLOBALS['wpo_cache_config']['timezone_string'] : '';
209 }
210
211 if (!empty($timezone_string)) {
212 $timezone_postfix = "(".$timezone_string." UTC:". $utc .")";
213 } else {
214 $timezone_postfix = "(UTC:" . $utc . ")";
215 }
216
217 $add_to_footer = '';
218
219 /**
220 * Filter whether to display the html comment <!-- Cached by WP-Optimize ... -->
221 *
222 * @param boolean $show - Whether to display the html comment
223 * @return boolean
224 */
225 if ((preg_match('#</html>#i', $buffer) || wpo_is_cacheable_sitemap_request())
226 && (apply_filters('wpo_cache_show_cached_by_comment', true) || (defined('WP_DEBUG') && WP_DEBUG))
227 ) {
228 $date_time_format = 'F j, Y g:i a';
229 if (!empty($GLOBALS['wpo_cache_config']['date_format']) && !empty($GLOBALS['wpo_cache_config']['time_format'])) {
230 $date_time_format = $GLOBALS['wpo_cache_config']['date_format'] . ' ' . $GLOBALS['wpo_cache_config']['time_format'];
231 }
232
233 if (!empty($GLOBALS['wpo_cache_config']['enable_mobile_caching']) && wpo_is_mobile()) {
234 $add_to_footer .= "\n<!-- Cached by WP-Optimize - for mobile devices - https://teamupdraft.com/wp-optimize/ - Last modified: " . gmdate($date_time_format, $modified_time) . " " . $timezone_postfix . " -->\n";
235 } else {
236 $add_to_footer .= "\n<!-- Cached by WP-Optimize - https://teamupdraft.com/wp-optimize/ - Last modified: " . gmdate($date_time_format, $modified_time) . " " . $timezone_postfix . " -->\n";
237 }
238 }
239
240 // Create an empty index.php file in the cache directory for disable directory viewing.
241 if (!is_file($path . '/index.php')) file_put_contents($path . '/index.php', ''); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
242
243 /**
244 * Save $buffer into cache file.
245 */
246
247 $cache_file = $path . '/' .$cache_filename;
248
249 if (defined('WPO_CACHE_FILENAME_DEBUG') && WPO_CACHE_FILENAME_DEBUG) {
250 $add_to_footer .= "\n<!-- WP Optimize page cache debug information -->\n";
251 if (!empty($GLOBALS['wpo_cache_filename_debug']) && is_array($GLOBALS['wpo_cache_filename_debug'])) {
252 $add_to_footer .= "<!-- \n" . join("\n", array_map(function($s) {
253 return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
254 }, $GLOBALS['wpo_cache_filename_debug'])) . "\n -->";
255 }
256 }
257
258 if (function_exists('gzencode') && apply_filters('wpo_allow_cache_gzip_files', true)) {
259 // Only replace inside the addition, not inside the main buffer (e.g. post content)
260 $add_to_footer = str_replace('by WP-Optimize', 'by WP-Optimize (gzip)', $add_to_footer);
261 }
262
263 // Allow extensions to inject content before </body> in cached HTML (HTML pages only, not sitemaps or RSS feeds).
264 // Content is inserted before the last </body> tag to produce valid HTML.
265 // Falls back to appending after </html> if no </body> is found (e.g., partial HTML responses).
266 if (!wpo_is_cacheable_sitemap_request() && !$is_feed_cache) {
267 $footer_injection = apply_filters('wpo_cache_add_to_footer', '', $cache_filename);
268 if ('' !== $footer_injection) {
269 $body_close_pos = strripos($buffer, '</body>');
270 if (false !== $body_close_pos) {
271 $buffer = substr($buffer, 0, $body_close_pos) . $footer_injection . substr($buffer, $body_close_pos);
272 } else {
273 $buffer .= $footer_injection;
274 }
275 }
276 }
277
278 // XML documents must not contain HTML comments in the footer, as this would invalidate the XML
279 if (wpo_is_cacheable_sitemap_request() && '' !== $add_to_footer) {
280 $pattern = '#</([a-zA-Z0-9:_-]+)>\s*$#';
281 $replacement = $add_to_footer . "\n</$1>";
282 $buffer = preg_replace($pattern, $replacement, $buffer, 1); // Insert the comment before the final closing tag
283 } else {
284 $buffer .= $add_to_footer;
285 }
286
287 // if we can then cache gzipped content in .gz file.
288 if (function_exists('gzencode') && apply_filters('wpo_allow_cache_gzip_files', true)) {
289 $gzipped_buffer = gzencode($buffer, apply_filters('wpo_cache_gzip_level', 6));
290 file_put_contents($cache_file . '.gz', $gzipped_buffer); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
291 }
292
293 file_put_contents($cache_file, $buffer); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
294
295 if (is_callable('WP_Optimize')) {
296 // delete cached information about cache size.
297 WP_Optimize()->get_page_cache()->delete_cache_size_information();
298 } else {
299 // If the shutdown occurs before plugins are loaded,
300 // then this will trigger a fatal error, so, we check first
301 if (!doing_action('shutdown')) {
302 // phpcs:disable
303 // Edge case handling for debugging purpose
304 error_log('[WPO_CACHE] WP_Optimize() is not callable.');
305 $message = 'Please report this to WP-O support: ';
306 if (function_exists('wp_debug_backtrace_summary')) {
307 $message .= wp_debug_backtrace_summary();
308 } else {
309 $message .= wpo_debug_backtrace_summary();
310 }
311 error_log($message);
312 // phpcs:enable
313 }
314 }
315
316 header('Cache-Control: no-cache'); // Check back every time to see if re-download is necessary.
317 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
318 header('WPO-Cache-Status: saving to cache');
319
320 // Enable gzipped output only if it is supported and the output buffer level is ≤2
321 // (i.e., no extra handlers beyond default and WPO_Page_Optimizer::optimize() are active)
322 $wpo_cache_can_output_gzip_content = wpo_cache_can_output_gzip_content() && ob_get_level() <= 2;
323
324 // Allow to override gzip output via the 'wpo_cache_can_output_gzip_content' filter
325 $wpo_cache_can_output_gzip_content = apply_filters('wpo_cache_can_output_gzip_content', $wpo_cache_can_output_gzip_content);
326
327 if ($wpo_cache_can_output_gzip_content) {
328
329 if (!wpo_cache_is_in_response_headers_list('Content-Encoding', 'gzip')) {
330 header('Content-Encoding: gzip');
331 }
332
333 ini_set('zlib.output_compression', 'Off'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- disabling php gzip to avoid double compression.
334
335 return ob_gzhandler($buffer, $flags);
336 } else {
337 return $buffer;
338 }
339 }
340 }
341 endif;
342
343 /**
344 * Load files for support plugins.
345 */
346 if (!function_exists('wpo_cache_load_extensions')) :
347 function wpo_cache_load_extensions() {
348 $extensions = glob(WPO_CACHE_EXT_DIR . '/*.php');
349
350 // Add external extensions
351 if (defined('WPO_CACHE_CUSTOM_EXT_DIR') && is_dir(WPO_CACHE_CUSTOM_EXT_DIR)) {
352 $extensions = array_merge($extensions, glob(WPO_CACHE_CUSTOM_EXT_DIR . '/*.php'));
353 }
354
355 if (empty($extensions)) return;
356
357 foreach ($extensions as $extension) {
358 if (is_file($extension)) require_once $extension;
359 }
360 }
361 endif;
362
363 /**
364 * Check whether the current request is a search query.
365 *
366 * Uses `is_search()` when available and falls back to checking for a
367 * * non-empty string `s` query parameter for early execution points.
368 *
369 * @return bool True if a search query parameter is present, false otherwise.
370 */
371 if (!function_exists('wpo_is_search')) {
372 function wpo_is_search(): bool {
373 if (function_exists('is_search') && is_search()) {
374 return true;
375 }
376
377 return isset($_GET['s']) && is_string($_GET['s']) && '' !== trim($_GET['s']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads, Nonce not available, only comparing
378 }
379 }
380
381 /**
382 * Determine whether the current request represents a page type
383 * that should not be cached.
384 *
385 * This function checks for known non-cacheable scenarios such as
386 * - Search results
387 * - 404 pages
388 * - Password-protected content
389 * - Front page when excluded via settings
390 * - RSS feeds (when feed caching is disabled)
391 * - Unsafe file paths (e.g., .htaccess)
392 *
393 * The first matched restriction reason will overwrite the passed
394 * value and be returned as a human-readable string.
395 *
396 * @param string $restricted Existing restriction reason, if any.
397 * @return string Restriction reason if caching is disallowed, otherwise the original value passed in `$restricted`.
398 */
399 if (!function_exists('wpo_restricted_cache_page_type')) {
400 function wpo_restricted_cache_page_type($restricted) {
401 global $post;
402
403 // Don't cache search or password protected.
404 if (wpo_is_search() || (function_exists('bbp_is_search') && bbp_is_search()) || (function_exists('is_404') && is_404()) || !empty($post->post_password)) {
405 $restricted = 'Page type is not cacheable (search, 404 or password-protected)';
406 }
407
408 // Don't cache the front page if option is set.
409 if (in_array('/', wpo_get_url_exceptions()) && function_exists('is_front_page') && is_front_page()) {
410
411 $restricted = __('In the settings, caching is disabled for the front page', 'wp-optimize');
412 }
413
414 // Don't cache htacesss. Remember to properly escape any output to prevent injection.
415 $request_uri = isset($_SERVER['REQUEST_URI']) ? htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8') : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP functions may not be available, so using php native functions. only outputting to browser
416 if (strpos($request_uri, '.htaccess') !== false) {
417 $restricted = 'The file path is unsuitable for caching ('.$request_uri.')';
418 }
419
420 // Don't cache feeds.
421 if (function_exists('is_feed') && is_feed() && !wpo_feeds_caching_enabled()) {
422 $restricted = __('We don\'t cache RSS feeds', 'wp-optimize');
423 }
424
425 return $restricted;
426 }
427 }
428
429 /**
430 * Returns true if we need cache content for loggedin users.
431 *
432 * @return bool
433 */
434 if (!function_exists('wpo_cache_loggedin_users')) :
435 function wpo_cache_loggedin_users() {
436 return !empty($GLOBALS['wpo_cache_config']['enable_user_caching']) || !empty($GLOBALS['wpo_cache_config']['enable_user_specific_cache']) || (function_exists('wpo_we_cache_per_role') && wpo_we_cache_per_role());
437 }
438 endif;
439
440 /**
441 * Get filename for store cache, depending on gzip, mobile and cookie settings.
442 *
443 * @param string $ext
444 * @return string
445 */
446 if (!function_exists('wpo_cache_filename')) :
447 function wpo_cache_filename($ext = '.html') {
448
449 $wpo_cache_filename_debug = array();
450
451 $filename = 'index';
452
453 if (wpo_cache_mobile_caching_enabled() && wpo_is_mobile()) {
454 $filename = 'mobile.' . $filename;
455 }
456
457 if (wpo_webp_images_enabled() && !wpo_is_using_webp_images_redirection() && wpo_is_browser_supports_webp()) {
458 $filename = $filename . '.webp';
459 }
460
461 $cookies = wpo_cache_cookies();
462
463 $cache_key = '';
464
465 /**
466 * Add cookie values to filename if need.
467 * This section was inspired by things learned from WP-Rocket.
468 */
469 if (!empty($cookies)) {
470 foreach ($cookies as $key => $cookie_name) {
471 if (is_array($cookie_name) && isset($_COOKIE[$key])) {
472 foreach ($cookie_name as $cookie_key) {
473 if (isset($_COOKIE[$key][$cookie_key]) && '' !== $_COOKIE[$key][$cookie_key]) {
474 $cookie_value = $_COOKIE[$key][$cookie_key]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP functions may not available, also removing everything except letters, numbers, hyphens and equal signs below
475 $_cache_key = $cookie_key.'='.$cookie_value;
476 $_cache_key = preg_replace('/[^a-z0-9_\-\=]/i', '-', $_cache_key);
477 $cache_key .= '-' . $_cache_key;
478 $wpo_cache_filename_debug[] = 'Cookie: name: ' . $key . '[' . $cookie_key . '], value: *** , cache_key:' . $_cache_key;
479 }
480 }
481 continue;
482 }
483
484 if (isset($_COOKIE[$cookie_name]) && '' !== $_COOKIE[$cookie_name]) {
485 $_cache_key = $cookie_name.'='. $_COOKIE[$cookie_name]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP functions may not available, also removing everything except letters, numbers, hyphens and equal signs below
486 $_cache_key = preg_replace('/[^a-z0-9_\-\=]/i', '-', $_cache_key);
487 $cache_key .= '-' . $_cache_key;
488 $wpo_cache_filename_debug[] = 'Cookie: name: ' . $cookie_name . ', value: *** , cache_key:' . $_cache_key;
489 }
490 }
491 }
492
493 $query_variables = wpo_cache_query_variables();
494
495 /**
496 * Add GET variables to cache file name if need.
497 */
498 if (!empty($query_variables)) {
499 foreach ($query_variables as $variable) {
500 if (isset($_GET[$variable]) && '' !== $_GET[$variable]) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Executes before WP fully loads, Nonce not available
501 $query_variable_value = $_GET[$variable]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP functions may not available, also removing everything except letters, numbers, hyphens and equal signs below
502 $_cache_key = $variable.'='.$query_variable_value;
503 $_cache_key = preg_replace('/[^a-z0-9_\-\=]/i', '-', $_cache_key);
504 $cache_key .= '-' . $_cache_key;
505 $wpo_cache_filename_debug[] = 'GET parameter: name: ' . $variable . ', value:' . htmlentities($query_variable_value) . ', cache_key:' . $_cache_key;
506 }
507 }
508 }
509
510 $filename = wpo_build_cache_filename($filename, $cache_key);
511 $filename = apply_filters('wpo_cache_filename', $filename);
512
513 $wpo_cache_filename_debug[] = 'Extension: ' . $ext;
514 $wpo_cache_filename_debug[] = 'Filename: ' . $filename;
515
516 $GLOBALS['wpo_cache_filename_debug'] = $wpo_cache_filename_debug;
517
518 return $filename . $ext;
519 }
520 endif;
521
522
523 if (!function_exists('wpo_build_cache_filename')) :
524 /**
525 * Builds a cache filename using the original filename, cache key,
526 * and trims it if it exceeds the file system limit.
527 *
528 * @param string $filename
529 * @param string $cache_key
530 * @return string
531 */
532 function wpo_build_cache_filename($filename, $cache_key) {
533 if ('' !== $cache_key) {
534 // Add human-readable cache key to the filename
535 $filename .= preg_replace('/\-+/', '-', '-'.$cache_key);
536 }
537
538 // Trimming filename if it exceeds 240 characters due to filesystem limitations
539 if (strlen($filename) > 240) {
540 $filename = substr($filename, 0, 199) . '-' . sha1($filename);
541 }
542
543 return $filename;
544 }
545 endif;
546
547
548 if (!function_exists('wpo_rest_cache_filename')) :
549 /**
550 * Builds the rest cache filename, uses passed params.
551 *
552 * @param array $params
553 * @return string
554 */
555 function wpo_rest_cache_filename($params) {
556 $filename = 'index';
557 $cache_key = '';
558
559 if (!empty($params)) {
560 ksort($params);
561 foreach ($params as $key => $value) {
562 if (is_array($value)) $value = serialize($value);
563 $_cache_key = $key.'_'.$value;
564 $_cache_key = preg_replace('/[^a-z0-9_\-]/i', '-', $_cache_key);
565 $cache_key .= '-' . $_cache_key;
566 }
567 }
568
569 return wpo_build_cache_filename($filename, $cache_key) . '.json';
570 }
571 endif;
572
573 /**
574 * Returns site url from site_url() function or if it is not available from cache configuration.
575 */
576 if (!function_exists('wpo_site_url')) :
577 function wpo_site_url() {
578 if (is_callable('site_url')) return site_url('/');
579
580 $site_url = empty($GLOBALS['wpo_cache_config']['site_url']) ? '' : $GLOBALS['wpo_cache_config']['site_url'];
581 return $site_url;
582 }
583 endif;
584
585 /**
586 * Get cookie names which impact on cache file name.
587 *
588 * @return array
589 */
590 if (!function_exists('wpo_cache_cookies')) :
591 function wpo_cache_cookies() {
592 $cookies = empty($GLOBALS['wpo_cache_config']['wpo_cache_cookies']) ? array() : $GLOBALS['wpo_cache_config']['wpo_cache_cookies'];
593 return $cookies;
594 }
595 endif;
596
597 /**
598 * Get GET variable names which impact on cache file name.
599 *
600 * @return array
601 */
602 if (!function_exists('wpo_cache_query_variables')) :
603 function wpo_cache_query_variables() {
604 if (defined('WPO_CACHE_URL_PARAMS') && WPO_CACHE_URL_PARAMS) {
605 $variables = array_keys($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Executes early, nonce not available
606 } else {
607 $variables = empty($GLOBALS['wpo_cache_config']['wpo_cache_query_variables']) ? array() : $GLOBALS['wpo_cache_config']['wpo_cache_query_variables'];
608 }
609
610 if (!empty($variables)) {
611 sort($variables);
612 }
613
614 return wpo_cache_maybe_ignore_query_variables($variables);
615 }
616 endif;
617
618 /**
619 * Get list of all received HTTP headers.
620 *
621 * @return array
622 */
623 if (!function_exists('wpo_get_http_headers')) :
624 function wpo_get_http_headers() {
625
626 static $headers;
627
628 if (!empty($headers)) return $headers;
629
630 $headers = array();
631
632 // if is apache server then use get allheaders() function.
633 if (function_exists('getallheaders')) {
634 $headers = getallheaders();
635 } else {
636 // https://www.php.net/manual/en/function.getallheaders.php
637 foreach ($_SERVER as $key => $value) {
638
639 $key = strtolower($key);
640
641 if ('HTTP_' === substr($key, 0, 5)) {
642 $headers[str_replace(' ', '-', ucwords(str_replace('_', ' ', substr($key, 5))))] = $value;
643 } elseif ('content_type' === $key) {
644 $headers["Content-Type"] = $value;
645 } elseif ('content_length' === $key) {
646 $headers["Content-Length"] = $value;
647 }
648 }
649 }
650
651 return $headers;
652 }
653 endif;
654
655 /**
656 * Check if requested Accept-Encoding headers has gzip value.
657 *
658 * @return bool
659 */
660 if (!function_exists('wpo_cache_gzip_accepted')) :
661 function wpo_cache_gzip_accepted() {
662 $headers = wpo_get_http_headers();
663
664 if (isset($headers['Accept-Encoding']) && preg_match('/gzip/i', $headers['Accept-Encoding'])) return true;
665
666 return false;
667 }
668 endif;
669
670 /**
671 * Check if we can output gzip content in current answer, i.e. check Accept-Encoding headers has gzip value
672 * and function ob_gzhandler is available.
673 *
674 * @return bool
675 */
676 if (!function_exists('wpo_cache_can_output_gzip_content')) :
677 function wpo_cache_can_output_gzip_content() {
678 return wpo_cache_gzip_accepted() && function_exists('ob_gzhandler');
679 }
680 endif;
681
682 /**
683 * Check if header with certain name exists in already prepared headers and has value comparable with $header_value.
684 *
685 * @param string $header_name header name
686 * @param string $header_value header value as regexp.
687 *
688 * @return bool
689 */
690 if (!function_exists('wpo_cache_is_in_response_headers_list')) :
691 function wpo_cache_is_in_response_headers_list($header_name, $header_value) {
692 $headers_list = headers_list();
693
694 if (!empty($headers_list)) {
695 $header_name = strtolower($header_name);
696
697 foreach ($headers_list as $value) {
698 $value = explode(':', $value);
699
700 if (strtolower($value[0]) === $header_name) {
701 if (preg_match('/'.$header_value.'/', $value[1])) {
702 return true;
703 } else {
704 return false;
705 }
706 }
707 }
708 }
709
710 return false;
711 }
712 endif;
713
714 /**
715 * Check if mobile cache is enabled and current request is from moblile device.
716 *
717 * @return bool
718 */
719 if (!function_exists('wpo_cache_mobile_caching_enabled')) :
720 function wpo_cache_mobile_caching_enabled() {
721 if (!empty($GLOBALS['wpo_cache_config']['enable_mobile_caching'])) return true;
722 return false;
723 }
724 endif;
725
726 /**
727 * Check if webp images enabled
728 *
729 * @return bool
730 */
731 if (!function_exists('wpo_webp_images_enabled')) :
732 function wpo_webp_images_enabled() {
733 if (!empty($GLOBALS['wpo_cache_config']['use_webp_images'])) return true;
734 return false;
735 }
736 endif;
737
738 /**
739 * Check whether the requesting browser supports WebP images
740 *
741 * @return bool
742 */
743 if (!function_exists('wpo_is_browser_supports_webp')) :
744 function wpo_is_browser_supports_webp() {
745 // Direct Accept header check (works for image sub-resource requests)
746 if (isset($_SERVER['HTTP_ACCEPT']) && false !== strpos($_SERVER['HTTP_ACCEPT'], 'image/webp')) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads, only doing string comparison
747 return true;
748 }
749
750 // Fallback for older Firefox versions, which support WebP but don't send 'image/webp' in the Accept header.
751 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads, only extracting a version number via regex for a version_compare, not used for output or storage
752
753 if (!empty($user_agent) && preg_match('/Firefox\/([\d\.]+[a-z\d]*)/', $user_agent, $matches)) {
754 return version_compare(WPO_MIN_FIREFOX_VERSION_FOR_WEBP, $matches[1], '<=');
755 }
756
757 return false;
758 }
759 endif;
760
761 /**
762 * Check whether webp images are served using redirect
763 *
764 * @return bool
765 */
766 if (!function_exists('wpo_is_using_webp_images_redirection')) :
767 function wpo_is_using_webp_images_redirection() {
768 if (empty($GLOBALS['wpo_cache_config']['uploads'])) return false;
769
770 $uploads_dir = $GLOBALS['wpo_cache_config']['uploads'];
771 $htaccess_file = $uploads_dir . '/.htaccess';
772 if (!file_exists($htaccess_file)) return false;
773 $htaccess_content = file_get_contents($htaccess_file); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- WP_Filesystem not available this early
774 $comment_sections = array('Register webp mime type', 'WP-Optimize WebP Rules');
775
776 if (function_exists('str_contains')) {
777 return str_contains($htaccess_content, $comment_sections[0]) && str_contains($htaccess_content, $comment_sections[1]);
778 } else {
779 return strpos($htaccess_content, $comment_sections[0]) && strpos($htaccess_content, $comment_sections[1]);
780 }
781 }
782 endif;
783
784 /**
785 * Verify if the current request is related to the Activity Stream
786 *
787 * @return bool
788 */
789 if (!function_exists('wpo_is_activity_stream_requested')) :
790 function wpo_is_activity_stream_requested() {
791 return (isset($_SERVER['HTTP_ACCEPT']) && preg_match('/(application\/(ld\+json|activity\+json|json))/i', $_SERVER['HTTP_ACCEPT'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads, only doing string comparison
792 }
793 endif;
794
795 /**
796 * Verify if the current request is robots.txt
797 */
798 if (!function_exists('wpo_is_robots_txt_requested')) :
799 function wpo_is_robots_txt_requested() {
800 return (isset($_SERVER['REQUEST_URI']) && 'robots.txt' === basename($_SERVER['REQUEST_URI'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended -- Executes before WP fully loads, only doing string comparison
801 }
802 endif;
803
804 /**
805 * Serves the cache and exits
806 *
807 * @return void
808 */
809 if (!function_exists('wpo_serve_cache')) :
810 function wpo_serve_cache() {
811 // Do not serve cache for cron requests.
812 if (defined('DOING_CRON') && DOING_CRON) return;
813
814 $file_name = wpo_cache_filename();
815
816 if (defined('WPO_CACHE_DONT_PROCESS_THIS_PAGE') && WPO_CACHE_DONT_PROCESS_THIS_PAGE) return;
817
818 $file_name_rss_xml = wpo_cache_filename('.rss-xml');
819 $send_as_feed = false;
820 $send_as_rest_response = false;
821 $headers_file = '';
822
823 $path_dir = WPO_CACHE_FILES_DIR . '/' . wpo_get_url_path() . '/';
824 $path = $path_dir . $file_name;
825
826 if (wpo_feeds_caching_enabled()) {
827 // check for .xml cache file if .html cache file doesn't exist
828 if (!file_exists($path_dir . $file_name) && file_exists($path_dir . $file_name_rss_xml)) {
829 $path = $path_dir . $file_name_rss_xml;
830 $send_as_feed = true;
831 }
832 }
833
834 if (wpo_rest_caching_enabled()) {
835 $file_name_rest_json = wpo_rest_cache_filename($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Executes early, nonce not available
836
837 if (is_file($path_dir . $file_name_rest_json)) {
838 $path = $path_dir . $file_name_rest_json;
839
840 if (is_file($path . '.headers')) {
841 $headers_file = $path . '.headers';
842 }
843
844 $send_as_rest_response = true;
845 }
846 }
847
848 $use_gzip = wpo_serve_cache_should_use_gzip($path);
849
850 if ($use_gzip) $path .= '.gz';
851
852 $modified_time = file_exists($path) ? (int) filemtime($path) : time();
853
854 $modified_time = apply_filters('wpo_cache_modified_time', $modified_time, $path);
855
856 // Cache has expired, purge and exit.
857 if (!empty($GLOBALS['wpo_cache_config']['page_cache_length'])) {
858 if (time() > ($GLOBALS['wpo_cache_config']['page_cache_length'] + $modified_time)) {
859 wpo_delete_files($path);
860 return;
861 }
862 }
863
864 if ($use_gzip) {
865 // Disable zlib output compression to avoid double content compression
866 ini_set('zlib.output_compression', 'Off'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- disabling php gzip to avoid double compression.
867 }
868
869 header('Cache-Control: no-cache'); // Check back later
870
871 if (!empty($modified_time) && !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $modified_time) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- `strtotime` ensures that the value is an integer
872
873 if ($use_gzip) wpo_send_gzip_header();
874
875 if ($send_as_feed) {
876 header('Content-type: application/rss+xml');
877 }
878
879 $allowed_protocols = array('HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/3');
880 $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Header value
881 if (in_array($protocol, $allowed_protocols, true)) {
882 $safe_protocol = $protocol;
883 } else {
884 $safe_protocol = 'HTTP/1.1';
885 }
886
887 header('WPO-Cache-Status: cached');
888 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
889 header( $safe_protocol. ' 304 Not Modified', true, 304);
890 exit;
891 }
892
893 if (file_exists($path) && is_readable($path)) {
894
895 if (!$send_as_rest_response && wpo_is_canonical_redirection_needed()) return;
896
897 if ($use_gzip) wpo_send_gzip_header();
898
899 header('WPO-Cache-Status: cached');
900
901 if ($send_as_rest_response) {
902 wpo_send_rest_cache_headers($headers_file);
903 }
904
905 // send correct headers for xml and txt files
906 $filename = basename(dirname($path));
907
908 if (preg_match('/\.xml$/i', $filename)) {
909 header('Content-type: text/xml');
910 }
911
912 if (preg_match('/\.txt$/i', $filename)) {
913 header('Content-type: text/plain');
914 }
915
916 if ($send_as_feed) {
917 header('Content-type: application/rss+xml');
918 }
919
920 if (!empty($modified_time)) {
921 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
922 }
923
924 readfile($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- If we use `get_contents` we need to echo it, it will result in not escaped error
925 exit;
926
927 }
928 }
929 endif;
930
931 /**
932 * Checks if we should use gzip in response when serve cache
933 *
934 * @param string $path - path to the cached file
935 * @return bool
936 */
937 if (!function_exists('wpo_serve_cache_should_use_gzip')) :
938 function wpo_serve_cache_should_use_gzip($path) {
939 // if we can use gzip and gzipped file exist in cache we use it.
940 // if headers already sent we don't use gzipped file content.
941 return !headers_sent() && wpo_cache_gzip_accepted() && file_exists($path . '.gz');
942 }
943 endif;
944
945 /**
946 * Sends the Content-Encoding: gzip header if it has not already been sent
947 *
948 * @return void
949 */
950 if (!function_exists('wpo_send_gzip_header')) :
951 function wpo_send_gzip_header() {
952 $gzip_header_already_sent = wpo_cache_is_in_response_headers_list('Content-Encoding', 'gzip');
953 if (!$gzip_header_already_sent) header('Content-Encoding: gzip');
954 }
955 endif;
956
957 /**
958 * Sends the necessary and cached headers for the REST response.
959 *
960 * @param string $headers_file file with cached headers
961 * @return void
962 */
963 if (!function_exists('wpo_send_rest_cache_headers')) :
964 function wpo_send_rest_cache_headers($headers_file) {
965 header('Content-type: application/json');
966 header('Cache-Control: no-store');
967 header('X-Content-Type-Options: nosniff');
968 header('X-Robots-Tag: noindex');
969
970 if ('' !== $headers_file) {
971 $headers_json = file_get_contents($headers_file);
972 $headers_to_send = json_decode($headers_json, true);
973
974 if (!empty($headers_to_send) && is_array($headers_to_send)) {
975 foreach ($headers_to_send as $header => $value) {
976 header($header.': '.$value);
977 }
978 }
979 }
980 }
981 endif;
982
983 /**
984 * Check if all requirements needed to serve the cache are met.
985 *
986 * @return bool|array returns false or an array with messages if one of the requirements is not met
987 */
988 if (!function_exists('wpo_can_serve_from_cache')) :
989 function wpo_can_serve_from_cache() {
990
991 $no_cache_because = array();
992
993 if (wpo_is_robots_txt_requested()) {
994 return false;
995 }
996
997 if (wpo_is_activity_stream_requested()) {
998 return false;
999 }
1000
1001 // Fix for compatibility issue with Jetpack's infinity scroll feature
1002 if (isset($_GET['infinity']) && 'scrolling' === $_GET['infinity']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Executes early, nonce not available
1003 return false;
1004 }
1005
1006 // check in not disabled current user agent
1007 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) && is_string($_SERVER['HTTP_USER_AGENT']) ? stripslashes($_SERVER['HTTP_USER_AGENT']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- value used for comparison only, not output
1008 if (!empty($user_agent) && false === wpo_is_accepted_user_agent($user_agent)) {
1009 $no_cache_because[] = "In the settings, caching is disabled for matches for this request's user agent";
1010 }
1011
1012 $is_cache_page_forced = function_exists('apply_filters') ? apply_filters('wpo_cache_page_force', false) : false;
1013 $is_get_request = isset($_SERVER['REQUEST_METHOD']) && 'GET' === $_SERVER['REQUEST_METHOD'];
1014
1015 // Don't cache non-GET requests.
1016 if (!$is_cache_page_forced && !$is_get_request) {
1017 $no_cache_because[] = 'The request method was not GET ('.(isset($_SERVER['REQUEST_METHOD']) ? htmlspecialchars($_SERVER['REQUEST_METHOD'], ENT_QUOTES, 'UTF-8') : '-').')'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Not needed only using it to display
1018 }
1019
1020 // Don't cache if logged in.
1021 if (!empty($_COOKIE)) {
1022
1023 if (!wpo_cache_loggedin_users() && wpo_is_wp_user_cookies_exist()) {
1024 $no_cache_because[] = 'WordPress login cookies were detected';
1025 }
1026
1027 if (!empty($_COOKIE['wpo_commented_post'])) {
1028 $no_cache_because[] = 'The user has commented on a post (comment cookie set)';
1029 }
1030
1031 // get cookie exceptions from options.
1032 $cache_exception_cookies = empty($GLOBALS['wpo_cache_config']['cache_exception_cookies']) ? array() : $GLOBALS['wpo_cache_config']['cache_exception_cookies'];
1033
1034 // check if any cookie exists from an exception list.
1035 if (!empty($cache_exception_cookies)) {
1036 foreach ($_COOKIE as $key => $value) {
1037 foreach ($cache_exception_cookies as $cookie) {
1038 if ('' !== trim($cookie) && false !== strpos($key, $cookie)) {
1039 $no_cache_because[] = 'An excepted cookie was set ('.$key.')';
1040 break 2;
1041 }
1042 }
1043 }
1044 }
1045 }
1046
1047 if (wpo_restricted_cache_page_with_cart_items()) {
1048 $no_cache_because[] = 'User has items in WooCommerce cart.';
1049 }
1050
1051 $restricted_page_type_cache = wpo_restricted_cache_page_type('');
1052 if (!empty($restricted_page_type_cache)) {
1053 $no_cache_because[] = $restricted_page_type_cache;
1054 }
1055
1056 $current_url = wpo_current_url();
1057
1058 // Deal with allowed urls
1059 if (wpo_cache_specific_urls_only() && !wpo_url_in_cache_include($current_url)) {
1060 $no_cache_because[] = 'Cache only specific URLs enabled, but URL not in list';
1061 }
1062
1063 // Deal with optional cache exceptions only when specific-URL caching is disabled
1064 if (!wpo_cache_specific_urls_only() && wpo_url_in_exceptions($current_url)) {
1065 $no_cache_because[] = 'In the settings, caching is disabled for matches for the current URL';
1066 }
1067
1068 if (!empty($_GET)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Executes early, nonce not available
1069 $get_variable_names = wpo_cache_query_variables();
1070
1071 $get_variables = wpo_cache_maybe_ignore_query_variables(array_keys($_GET)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Executes early, nonce not available. Value only used for comparison
1072
1073 // if GET variables include one or more undefined variable names, then we don't cache.
1074 $get_variables_diff = array_diff($get_variables, $get_variable_names);
1075
1076 if (!empty($get_variables_diff) && !wpo_is_cacheable_sitemap_request()) {
1077 $no_cache_because[] = "In the settings, caching is disabled for matches for one of the current request's GET parameters";
1078 }
1079 }
1080
1081 $request_uri = isset($_SERVER['REQUEST_URI']) ? strval(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads
1082 $file_extension = strtolower(pathinfo($request_uri, PATHINFO_EXTENSION));
1083
1084 // Don't cache disallowed extensions. Prevents wp-cron.php, xmlrpc.php, etc.
1085 if (!preg_match('#index\.php$#i', $request_uri) && !wpo_is_cacheable_sitemap_request() && in_array($file_extension, array('php', 'xml', 'xsl'))) {
1086 $no_cache_because[] = 'The request extension is not suitable for caching';
1087 }
1088
1089 if (!empty($no_cache_because)) return $no_cache_because;
1090
1091 return true;
1092 }
1093 endif;
1094
1095 /**
1096 * Checks if the current request has WooCommerce cart items.
1097 *
1098 * @return bool Returns true if the user has items in the WooCommerce cart, false otherwise.
1099 */
1100 if (!function_exists('wpo_restricted_cache_page_with_cart_items')) :
1101 function wpo_restricted_cache_page_with_cart_items(): bool {
1102 return !empty($_COOKIE['woocommerce_items_in_cart']) || !empty($_COOKIE['woocommerce_cart_hash']);
1103 }
1104 endif;
1105
1106 /**
1107 * Checks if the current request is a cacheable sitemap request
1108 *
1109 * @return bool
1110 */
1111 if (!function_exists('wpo_is_cacheable_sitemap_request')) :
1112 function wpo_is_cacheable_sitemap_request() {
1113 $is_sitemap_defined = defined('WPO_CACHE_SITEMAP') && WPO_CACHE_SITEMAP;
1114
1115 if (!$is_sitemap_defined) return false;
1116
1117 $request_uri = isset($_SERVER['REQUEST_URI']) ? strval(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads
1118 $is_sitemap_request = 1 === preg_match('#[a-zA-Z0-9_-]*?sitemap([a-zA-Z0-9_-]+)?\.xml$#i', $request_uri);
1119
1120 if ($is_sitemap_request) return true;
1121
1122 return false;
1123 }
1124 endif;
1125
1126 /**
1127 * Checks if WordPress user cookies are set.
1128 *
1129 * @return bool
1130 */
1131 if (!function_exists('wpo_is_wp_user_cookies_exist')) :
1132 function wpo_is_wp_user_cookies_exist(): bool {
1133 if (empty($_COOKIE)) return false;
1134
1135 $wp_user_cookies = array('wordpress_sec_', 'wordpress_logged_in_');
1136
1137 foreach (array_keys($_COOKIE) as $cookie_name) {
1138 foreach ($wp_user_cookies as $user_cookie_name) {
1139 if (0 === strpos($cookie_name, $user_cookie_name)) {
1140 return true;
1141 }
1142 }
1143 }
1144
1145 return false;
1146 }
1147 endif;
1148
1149 /**
1150 * Checks and does redirection, if needed
1151 *
1152 * @return bool
1153 */
1154 if (!function_exists('wpo_is_canonical_redirection_needed')) :
1155 function wpo_is_canonical_redirection_needed() {
1156 $permalink_structure = isset($GLOBALS['wpo_cache_config']['permalink_structure']) ? $GLOBALS['wpo_cache_config']['permalink_structure'] : '';
1157 $site_url = wpo_site_url();
1158
1159 // Exit if server variables are not available.
1160 if (!isset($_SERVER['HTTP_HOST'])) return false;
1161
1162 $schema = isset($_SERVER['HTTPS']) && 'on' === $_SERVER['HTTPS'] ? "https" : "http";
1163 $request_uri = isset($_SERVER['REQUEST_URI']) ? strval(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads, only using return value of parse_url
1164 $url_part = "://" . $_SERVER['HTTP_HOST'] . $request_uri; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads, only using return value of parse_url
1165 $requested_url = $schema . $url_part;
1166 $url_parts = parse_url($requested_url); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url not available this early
1167 $extension = isset($url_parts['path']) ? pathinfo($url_parts['path'], PATHINFO_EXTENSION) : '';
1168
1169 if (!empty($permalink_structure) && $requested_url !== $site_url && ((isset($url_parts['path']) && '/' !== $url_parts['path']) || isset($url_parts['query']))) {
1170 $request_uri = rtrim($request_uri, '?');
1171 if ('/' === substr($permalink_structure, -1) && empty($extension) && empty($url_parts['query']) && empty($url_parts['fragment'])) {
1172 $url = preg_replace('/(.+?)([\/]*)(\[\?\#][^\/]+|$)/', '$1/$3', $request_uri);
1173 if (0 !== strcmp($request_uri, $url)) return true;
1174 } else {
1175 $url = rtrim($request_uri, '/');
1176 if (0 !== strcmp($request_uri, $url)) return true;
1177 }
1178 }
1179 return false;
1180 }
1181 endif;
1182
1183 /**
1184 * Clears the cache
1185 */
1186 if (!function_exists('wpo_cache_flush')) :
1187 function wpo_cache_flush() {
1188
1189 if (defined('WPO_CACHE_FILES_DIR') && '' !== WPO_CACHE_FILES_DIR) wpo_delete_files(WPO_CACHE_FILES_DIR);
1190
1191 if (function_exists('wp_cache_flush')) {
1192 wp_cache_flush();
1193 }
1194
1195 do_action('wpo_cache_flush');
1196 }
1197 endif;
1198
1199 /**
1200 * Get URL path for caching
1201 *
1202 * @since 1.0
1203 * @return string
1204 */
1205 if (!function_exists('wpo_get_url_path')) :
1206 function wpo_get_url_path($url = '') {
1207 $url = '' === $url ? wpo_current_url() : $url;
1208 $url_parts = parse_url($url); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url not available this early
1209
1210 // Normalize path to avoid issues with encoded characters, and to ensure that the path is consistent.
1211 if (isset($url_parts['path'])) {
1212 $url_parts['path'] = wpo_normalize_url_path($url_parts['path']);
1213 }
1214
1215 if (isset($url_parts['path']) && false !== stripos($url_parts['path'], '/index.')) {
1216 $url_parts['path'] = preg_replace('/(.*?)index\.(php|html)(\/.+)/i', '$1index-$2$3', $url_parts['path']);
1217 $url_parts['path'] = preg_replace('/index\.(php|html)/i', 'index-$1', $url_parts['path']);
1218 }
1219
1220 if (!isset($url_parts['host'])) $url_parts['host'] = '';
1221 if (!isset($url_parts['path'])) $url_parts['path'] = '';
1222
1223 return $url_parts['host'].$url_parts['path'];
1224 }
1225 endif;
1226
1227 /**
1228 * Get requested url.
1229 *
1230 * @return string
1231 */
1232 if (!function_exists('wpo_current_url')) :
1233 function wpo_current_url() {
1234 // Note: We use `static $url` to save the first value we retrieve, as some plugins change $_SERVER later on in the process (e.g. Weglot).
1235 // Otherwise this function would return a different URL at the beginning and end of the cache process.
1236 static $url = '';
1237 if ('' !== $url) return $url;
1238 $http_host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads
1239 $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Executes before WP fully loads
1240 $url = rtrim('http' . ((isset($_SERVER['HTTPS']) && ('on' === $_SERVER['HTTPS'] || 1 === (int) $_SERVER['HTTPS']) ||
1241 isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO']) ? 's' : '' )
1242 . '://' . $http_host.$request_uri, '/');
1243 $filtered_url = filter_var($url, FILTER_VALIDATE_URL);
1244 return false !== $filtered_url ? $filtered_url : preg_replace('/[^a-z0-9\-._~:\/?#\[\]@!$&\'()*+,;=%]/i', '', $url);
1245 }
1246 endif;
1247
1248 /**
1249 * Return list of conditional tag exceptions.
1250 *
1251 * @return array
1252 */
1253 if (!function_exists('wpo_get_conditional_tags_exceptions')) :
1254 function wpo_get_conditional_tags_exceptions() {
1255 static $exceptions = null;
1256
1257 if (null !== $exceptions) return $exceptions;
1258
1259 if (!empty($GLOBALS['wpo_cache_config'])) {
1260 if (empty($GLOBALS['wpo_cache_config']['cache_exception_conditional_tags'])) {
1261 $exceptions = array();
1262
1263 } else {
1264
1265 $exceptions = $GLOBALS['wpo_cache_config']['cache_exception_conditional_tags'];
1266
1267 }
1268
1269 } elseif (class_exists('WPO_Page_Cache')) {
1270
1271 $config = WPO_Page_Cache::instance()->config->get();
1272
1273 if (is_array($config) && array_key_exists('cache_exception_conditional_tags', $config)) {
1274 $exceptions = $config['cache_exception_conditional_tags'];
1275 } else {
1276 $exceptions = array();
1277 }
1278
1279 $exceptions = is_array($exceptions) ? $exceptions : preg_split('#(\n|\r|\r\n)#', $exceptions);
1280 $exceptions = array_filter($exceptions, 'trim');
1281
1282 } else {
1283 $exceptions = array();
1284 }
1285
1286 return $exceptions;
1287 }
1288 endif;
1289
1290 /**
1291 * Return list of url exceptions.
1292 *
1293 * @return array
1294 */
1295 if (!function_exists('wpo_get_url_exceptions')) :
1296 function wpo_get_url_exceptions() {
1297 static $exceptions = null;
1298
1299 if (null !== $exceptions) return $exceptions;
1300
1301 // if called from file-based-page-cache.php when WP loading
1302 // and cache settings exists then use it otherwise get settings from database.
1303 if (!empty($GLOBALS['wpo_cache_config'])) {
1304 if (empty($GLOBALS['wpo_cache_config']['cache_exception_urls'])) {
1305 $exceptions = array();
1306 } else {
1307 $exceptions = is_array($GLOBALS['wpo_cache_config']['cache_exception_urls']) ? $GLOBALS['wpo_cache_config']['cache_exception_urls'] : preg_split('#(\n|\r)#', $GLOBALS['wpo_cache_config']['cache_exception_urls']);
1308 }
1309 } elseif (class_exists('WPO_Page_Cache')) {
1310 $config = WPO_Page_Cache::instance()->config->get();
1311
1312 if (is_array($config) && array_key_exists('cache_exception_urls', $config)) {
1313 $exceptions = $config['cache_exception_urls'];
1314 } else {
1315 $exceptions = array();
1316 }
1317
1318 $exceptions = is_array($exceptions) ? $exceptions : preg_split('#(\n|\r)#', $exceptions);
1319 $exceptions = array_filter($exceptions, 'trim');
1320 } else {
1321 $exceptions = array();
1322 }
1323
1324 return apply_filters('wpo_get_url_exceptions', $exceptions);
1325 }
1326 endif;
1327
1328 /**
1329 * Returns a list of URLs that are included in the cache.
1330 *
1331 * @return array
1332 */
1333 if (!function_exists('wpo_get_cache_include_urls')) :
1334 function wpo_get_cache_include_urls() {
1335 static $cache_include_urls = null;
1336
1337 if (null !== $cache_include_urls) return $cache_include_urls;
1338
1339 // if called from file-based-page-cache.php when WP loading
1340 // and cache settings exists then use it otherwise get settings from database.
1341 if (!empty($GLOBALS['wpo_cache_config'])) {
1342 if (empty($GLOBALS['wpo_cache_config']['cache_include_urls'])) {
1343 $cache_include_urls = array();
1344 } else {
1345 $cache_include_urls = is_array($GLOBALS['wpo_cache_config']['cache_include_urls']) ? $GLOBALS['wpo_cache_config']['cache_include_urls'] : preg_split('#(\n|\r)#', $GLOBALS['wpo_cache_config']['cache_include_urls']);
1346 $cache_include_urls = array_filter($cache_include_urls, 'trim');
1347 }
1348 } else {
1349 $cache_include_urls = array();
1350 }
1351
1352 return apply_filters('wpo_get_cache_include_urls', $cache_include_urls);
1353 }
1354 endif;
1355
1356 /**
1357 * Return true of exception url matches current url
1358 *
1359 * @param string $exception Exceptions to check URL against.
1360 * @param bool $regex Whether to check with regex or not.
1361 * @return bool true if matched, false otherwise
1362 */
1363 if (!function_exists('wpo_current_url_exception_match')) :
1364 function wpo_current_url_exception_match($exception) {
1365
1366 return wpo_url_exception_match(wpo_current_url(), $exception);
1367 }
1368 endif;
1369
1370 /**
1371 * Check if url in conditional tags exceptions list.
1372 *
1373 * @return string
1374 */
1375 if (!function_exists('wpo_url_in_conditional_tags_exceptions')) :
1376 function wpo_url_in_conditional_tags_exceptions() {
1377
1378 $exceptions = wpo_get_conditional_tags_exceptions();
1379 $restricted = '';
1380 $allowed_functions = array('is_single', 'is_page', 'is_front_page', 'is_home', 'is_archive', 'is_tag', 'is_category', 'is_feed', 'is_search', 'is_author', 'is_woocommerce', 'is_shop', 'is_product', 'is_account_page', 'is_product_category', 'is_product_tag', 'is_wc_endpoint_url', 'is_bbpress', 'bbp_is_forum_archive', 'bbp_is_topic_archive', 'bbp_is_topic_tag', 'bbp_is_single_forum', 'bbp_is_single_topic', 'bbp_is_single_view', 'bbp_is_single_user', 'bbp_is_user_home', 'bbp_is_search');
1381 //Filter for add more conditional tags to whitelist in the exceptions list.
1382 $allowed_functions = apply_filters('wpo_allowed_conditional_tags_exceptions', $allowed_functions);
1383 if (!empty($exceptions)) {
1384 foreach ($exceptions as $exception) {
1385 if (false !== strpos($exception, 'is_')) {
1386 $exception_function = $exception;
1387 if ('()' === substr($exception, -2)) {
1388 $exception_function = substr($exception, 0, -2);
1389 }
1390
1391 if (in_array($exception_function, $allowed_functions) && function_exists($exception_function) && call_user_func($exception_function)) {
1392 // translators: %s is the function name for conditional tag
1393 $restricted = sprintf(__('In the settings, caching is disabled for %s', 'wp-optimize'), $exception_function);
1394 }
1395 }
1396 }
1397 }
1398 return $restricted;
1399 }
1400 endif;
1401
1402
1403 /**
1404 * Check if url in exceptions list.
1405 *
1406 * @param string $url
1407 *
1408 * @return bool
1409 */
1410 if (!function_exists('wpo_url_in_exceptions')) :
1411 function wpo_url_in_exceptions($url) {
1412 $exceptions = wpo_get_url_exceptions();
1413
1414 if (!empty($exceptions)) {
1415 foreach ($exceptions as $exception) {
1416
1417 // don't check / - front page using regexp, we handle it in wpo_restricted_cache_page_type()
1418 if ('/' === $exception) continue;
1419
1420 if (wpo_url_exception_match($url, $exception)) {
1421 // Exception match.
1422 return true;
1423 }
1424 }
1425 }
1426
1427 return false;
1428 }
1429 endif;
1430
1431 /**
1432 * Checks if URL matches against listed include.
1433 *
1434 * Supports:
1435 * - Root-based paths (e.g., /page)
1436 * - Wildcards (*) in the last segment
1437 * - One optional parent directory (sub-dir multisite)
1438 *
1439 * @param string $url
1440 * @return bool
1441 */
1442 if (!function_exists('wpo_url_in_cache_include')) :
1443 function wpo_url_in_cache_include($url): bool {
1444 $url = preg_replace('/\?.*/', '', $url); // Remove query string
1445 $url = rtrim($url, '/'); // normalize URL (remove trailing slash)
1446
1447 // Get path only
1448 $path = parse_url($url, PHP_URL_PATH); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url not available this early
1449 $path = $path ?? '/';
1450 $path = rtrim($path, '/');
1451
1452 $cache_include_urls = wpo_get_cache_include_urls();
1453
1454 if (!empty($cache_include_urls) && is_array($cache_include_urls)) {
1455 foreach ($cache_include_urls as $include_url) {
1456 $include_url = rtrim($include_url, '/'); // normalize include URL (remove trailing slash)
1457 if (strpos($include_url, '/') === 0) {
1458 // Support wildcards and allow one subdirectory max
1459 $pattern = preg_quote($include_url, '#');
1460 $pattern = str_replace('\*', '.*', $pattern);
1461
1462 if (preg_match('#^(/[^/]+)?' . $pattern . '$#i', $path)) {
1463 return true;
1464 }
1465 }
1466
1467 if (wpo_url_exception_match($url, $include_url)) {
1468 return true;
1469 }
1470 }
1471 }
1472
1473 return false;
1474 }
1475 endif;
1476
1477 /**
1478 * Checks if an URL matches against listed exceptions.
1479 *
1480 * @param string $url - complete url string i.e. http(s)://domain/path
1481 * @param string $exception - complete url or absolute path, can contain (.*) wildcards; Sometimes can be urlencoded
1482 *
1483 * @return bool
1484 */
1485 if (!function_exists('wpo_url_exception_match')) :
1486 function wpo_url_exception_match($url, $exception) {
1487 if (preg_match('#^[\s]*$#', $exception)) {
1488 return false;
1489 }
1490
1491 $exception = trim($exception);
1492
1493 // Used to test websites placed in subdirectories.
1494 $sub_dir = '';
1495
1496 // If exception defined from root i.e. /page1 then remove domain part in url.
1497 if (preg_match('/^\//', $exception)) {
1498 // get site sub directory.
1499 $sub_dir = preg_replace('#^(http|https):\/\/.*\/#Ui', '', wpo_site_url());
1500 // add prefix slash and remove slash.
1501 $sub_dir = ('' === $sub_dir || null === $sub_dir) ? '' : '/' . rtrim($sub_dir, '/');
1502 // get relative path
1503 $url = preg_replace('#^(http|https):\/\/.*\/#Ui', '/', $url);
1504 }
1505
1506 $url = urldecode(rtrim($url, '/')) . '/';
1507 $exception = rtrim($exception, '/');
1508
1509 $exception = wpo_mask_to_regex($exception, true);
1510
1511 if (!$exception) return false;
1512
1513 $exception = urldecode($exception);
1514
1515 return (preg_match('#^'.$exception.'$#i', $url) || preg_match('#^'.$sub_dir.$exception.'$#i', $url));
1516 }
1517 endif;
1518
1519 /**
1520 * Checks if its a mobile device
1521 *
1522 * @see https://developer.wordpress.org/reference/functions/wp_is_mobile/
1523 */
1524 if (!function_exists('wpo_is_mobile')) :
1525 function wpo_is_mobile() {
1526 $user_agent = empty($_SERVER['HTTP_USER_AGENT']) ? null : $_SERVER['HTTP_USER_AGENT']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only used for string comparison
1527 if (empty($user_agent)) {
1528 $is_mobile = false;
1529 // many mobile devices (all iPhone, iPad, etc.)
1530 } elseif (strpos($user_agent, 'Mobile') !== false
1531 || strpos($user_agent, 'Android') !== false
1532 || strpos($user_agent, 'Silk/') !== false
1533 || strpos($user_agent, 'Kindle') !== false
1534 || strpos($user_agent, 'BlackBerry') !== false
1535 || strpos($user_agent, 'Opera Mini') !== false
1536 || strpos($user_agent, 'Opera Mobi') !== false
1537 ) {
1538 $is_mobile = true;
1539 } else {
1540 $is_mobile = false;
1541 }
1542
1543 return $is_mobile;
1544 }
1545 endif;
1546
1547 /**
1548 * Converts a wildcard mask to a regular expression pattern.
1549 *
1550 * @param string $mask
1551 * @param boolean $add_trailing_slash Whether to add a trailing slash to the regex pattern if the mask doesn't already end with a wildcard. This allows matching URLs with or without a trailing slash.
1552 * @return string|false Regular expression pattern if conversion is successful, false if the input mask is empty after trimming.
1553 */
1554 if (!function_exists('wpo_mask_to_regex')) :
1555 function wpo_mask_to_regex($mask, $add_trailing_slash = false) {
1556 $mask = trim($mask);
1557
1558 if ('' === $mask) {
1559 return false;
1560 }
1561
1562 // Convert wildcard to regex
1563 $mask = str_replace('*', '.*', $mask);
1564
1565 // If the mask doesn't already end with a wildcard, add a trailing slash to match URLs with or without a trailing slash.
1566 if ($add_trailing_slash && !preg_match('#\(\.\*\)$#', $mask)) {
1567 $mask = rtrim($mask, '/') . '/';
1568 }
1569
1570 // Escape regex characters
1571 $mask = preg_quote($mask);
1572
1573 // Restore wildcard and dash
1574 $mask = str_replace(
1575 array('\.\*', '\-'),
1576 array('.*', '-'),
1577 $mask
1578 );
1579
1580 return $mask;
1581 }
1582 endif;
1583
1584
1585 /**
1586 * Check if current browser agent is not disabled in options.
1587 *
1588 * @param string $user_agent
1589 *
1590 * @return bool
1591 */
1592 if (!function_exists('wpo_is_accepted_user_agent')) :
1593 function wpo_is_accepted_user_agent($user_agent) {
1594
1595 if (empty($GLOBALS['wpo_cache_config'])) return true;
1596
1597 $exceptions = is_array($GLOBALS['wpo_cache_config']['cache_exception_browser_agents']) ? $GLOBALS['wpo_cache_config']['cache_exception_browser_agents'] : preg_split('#(\n|\r)#', $GLOBALS['wpo_cache_config']['cache_exception_browser_agents']);
1598
1599 if (!empty($exceptions)) {
1600 foreach ($exceptions as $exception) {
1601 if ('' === trim($exception)) continue;
1602
1603 $exception = wpo_mask_to_regex($exception);
1604 if ($exception && preg_match('#'.$exception.'#i', $user_agent)) return false;
1605 }
1606 }
1607
1608 return true;
1609 }
1610 endif;
1611
1612 if (!function_exists('wpo_delete_files')) :
1613 /**
1614 * Deletes a specified source file or directory.
1615 *
1616 * If $src is a file, only that file will be deleted. If $src is a directory, the behavior depends on the
1617 * $recursive parameter. When $recursive is true, the directory and its contents (including files and subdirectories)
1618 * will be deleted. When $recursive is false, only the files in the top-level directory(eg. $src directory) will be deleted,
1619 * while the $src directory itself and its subdirectories will remain untouched.
1620 *
1621 * @param string $src The path to the source file or directory to delete.
1622 * @param bool $recursive (Optional) When set to true, the directory and its contents (including files and subdirectories)
1623 * will be deleted. If false, only the files in the top-level directory will be deleted while
1624 * its subdirectories will be preserved. Defaults to true.
1625 *
1626 * @return bool Returns true if the specified file or all files within the specified directory (and its subdirectories,
1627 * when $recursive is true) are successfully deleted. Returns false if any file(s) could not be deleted
1628 * due to file permissions or other reasons.
1629 */
1630 function wpo_delete_files($src, $recursive = true) {
1631 // If the source doesn't exist, consider it deleted and return true
1632 if (!file_exists($src)) {
1633 return true;
1634 }
1635
1636 /*
1637 * If the source is a file, delete it and return the result.
1638 * If `unlink()` fails, we also verify if the file still exists before returning the result, as another
1639 * PHP process may have already deleted the file between the execution of `is_file()` and `unlink()` operations.
1640 */
1641 if (is_file($src)) {
1642 // phpcs:disable
1643 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1644 // WordPress.WP.AlternativeFunctions.unlink_unlink -- wp_delete_file may not be available this early
1645 if (!@unlink($src) && file_exists($src)) {
1646 return false;
1647 }
1648 // phpcs:enable
1649
1650 return true;
1651 }
1652
1653 $success = true;
1654
1655 // If recursive is false, delete only the top-level files and return the result
1656 if (!$recursive) {
1657 $dir_handle = @opendir($src); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1658
1659 /*
1660 * If opendir() is successful, process directory contents. If not, check if the directory exists.
1661 * If it exists, return false (failure). Otherwise, assume it's already deleted and return true (success).
1662 */
1663 if (false !== $dir_handle) {
1664
1665 while (false !== ($file = readdir($dir_handle))) {
1666 if ('.' === $file || '..' === $file) {
1667 continue;
1668 }
1669
1670 $full_path = rtrim($src, '/\\') . DIRECTORY_SEPARATOR . $file;
1671
1672 // If it's a file, delete it
1673 if (is_file($full_path)) {
1674 if (!wpo_delete_files($full_path)) {
1675 $success = false;
1676 }
1677 }
1678 }
1679
1680 closedir($dir_handle);
1681 } else {
1682 if (file_exists($src)) {
1683 $success = false;
1684 }
1685 }
1686
1687 return $success;
1688 }
1689
1690 // If recursive is true, delete all files and directories recursively
1691 $dir_handle = @opendir($src); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1692
1693 /*
1694 * If opendir() is successful, process directory contents. If not, check if the directory exists.
1695 * If it exists, return false (failure). Otherwise, assume it's already deleted and return true (success).
1696 */
1697 if (false !== $dir_handle) {
1698
1699 while (false !== ($file = readdir($dir_handle))) {
1700 if ('.' === $file || '..' === $file) {
1701 continue;
1702 }
1703
1704 $full_path = rtrim($src, '/\\') . DIRECTORY_SEPARATOR . $file;
1705
1706 if (!wpo_delete_files($full_path)) {
1707 $success = false;
1708 }
1709 }
1710
1711 closedir($dir_handle);
1712 } else {
1713 if (file_exists($src)) {
1714 $success = false;
1715 }
1716 }
1717
1718 /*
1719 * Delete the source directory itself.
1720 * Success of `rmdir` operation is not recorded; we only ultimately care about emptying, not removing
1721 * entirely (empty folders in our context are harmless)
1722 */
1723 if ($success) {
1724 // phpcs:disable
1725 // WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP_Filesystem not available this early
1726 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress errors from displaying
1727 @rmdir($src);
1728 // phpcs:enable
1729 }
1730
1731 // Delete cached information about cache size
1732 WP_Optimize()->get_page_cache()->delete_cache_size_information();
1733
1734 return $success;
1735 }
1736 endif;
1737
1738 if (!function_exists('wpo_is_empty_dir')) :
1739 /**
1740 * Check if selected directory is empty or has only index.php which we added for security reasons.
1741 *
1742 * @param string $dir
1743 *
1744 * @return bool
1745 */
1746 function wpo_is_empty_dir($dir) {
1747 if (!file_exists($dir) || !is_dir($dir)) return false;
1748
1749 $handle = opendir($dir);
1750
1751 if (false === $handle) return false;
1752
1753 $is_empty = true;
1754 $file = readdir($handle);
1755
1756 while (false !== $file) {
1757
1758 if ('.' !== $file && '..' !== $file && 'index.php' !== $file) {
1759 $is_empty = false;
1760 break;
1761 }
1762
1763 $file = readdir($handle);
1764 }
1765
1766 closedir($handle);
1767 return $is_empty;
1768 }
1769 endif;
1770
1771 /**
1772 * Either store for later output, or output now. Only the most-recent call will be effective.
1773 *
1774 * @param String|Null $output - if not null, then the string to use when called by the shutdown action.
1775 */
1776 if (!function_exists('wpo_cache_add_footer_output')) :
1777 function wpo_cache_add_footer_output($output = null) {
1778
1779 static $buffered = null;
1780
1781 if (null === $buffered) {
1782 add_action('shutdown', 'wpo_cache_add_footer_output', 11);
1783 $buffered = $output;
1784 } elseif ('shutdown' === current_filter()) {
1785 // Only add the line if it was a page, not something else (e.g. REST response)
1786 if (did_action('wp_footer') && !preg_match('/\/wp\-json\//', $_SERVER['REQUEST_URI']) && apply_filters('wpo_cache_show_cached_by_comment', true)) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Executes before WP fully loads, global value only used for string comparison
1787 echo "\n<!-- WP Optimize page cache - https://teamupdraft.com/wp-optimize/ - ".esc_html($buffered)." -->\n";
1788 } elseif (defined('WPO_CACHE_DEBUG') && WPO_CACHE_DEBUG && (!defined('REST_REQUEST') || !REST_REQUEST)) {
1789 error_log('[CACHE DEBUG] '.wpo_current_url() . ' - ' . $buffered); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Edge case, used for debugging
1790 }
1791 }
1792 }
1793 endif;
1794
1795 /**
1796 * Remove variable names that shouldn't influence cache.
1797 *
1798 * @param array $variables List of variable names.
1799 *
1800 * @return array
1801 */
1802 if (!function_exists('wpo_cache_maybe_ignore_query_variables')) :
1803 function wpo_cache_maybe_ignore_query_variables($variables) {
1804
1805 /**
1806 * Filters the current $_GET variables that will be used when caching or excluding from cache.
1807 * Currently:
1808 * - 'wpo_cache_debug' (Shows the reason for not being cached even when WP_DEBUG isn't set)
1809 * - 'doing_wp_cron' (alternative cron)
1810 * - 'aiosp_sitemap_path', 'aiosp_sitemap_page' (All in one SEO sitemap)
1811 * - 'xml_sitemap', 'seopress_sitemap', 'seopress_news', 'seopress_video', 'seopress_cpt', 'seopress_paged' (SEOPress sitemap)
1812 * - 'sitemap', 'sitemap_n' (YOAST SEO sitemap)
1813 */
1814 $exclude_variables = array(
1815 'wpo_cache_debug', // Shows the reason for not being cached even when WP_DEBUG isn't set
1816 'doing_wp_cron', // alternative cron
1817 'aiosp_sitemap_path', // All in one SEO sitemap
1818 'aiosp_sitemap_page',
1819 'xml_sitemap', // SEOPress sitemap
1820 'seopress_sitemap',
1821 'seopress_news',
1822 'seopress_video',
1823 'seopress_cpt',
1824 'seopress_paged',
1825 'sitemap', // YOAST SEO sitemap
1826 'sitemap_n',
1827 );
1828
1829 // Analytics extension - only works in premium version
1830 if (file_exists(WPO_CACHE_EXT_DIR . '/analytics.php')) {
1831 $analytics_variables = include(WPO_CACHE_EXT_DIR . '/analytics.php');
1832 $user_defined_variables = wpo_cache_config_get('cache_ignore_query_variables');
1833 $user_defined_variables = is_array($user_defined_variables) ? $user_defined_variables : array();
1834 $exclude_variables = array_merge($exclude_variables, $analytics_variables, $user_defined_variables);
1835 }
1836
1837 if (empty($exclude_variables)) return $variables;
1838
1839 foreach ($exclude_variables as $variable) {
1840 $exclude = array_search($variable, $variables);
1841 if (false !== $exclude) {
1842 array_splice($variables, $exclude, 1);
1843 }
1844 }
1845
1846 return $variables;
1847 }
1848 endif;
1849
1850 /**
1851 * Get cache config
1852 *
1853 * @param string $key - The config item
1854 * @param mixed $default - The default value
1855 *
1856 * @return mixed
1857 */
1858 if (!function_exists('wpo_cache_config_get')) :
1859 function wpo_cache_config_get($key, $default = false) {
1860 $config = $GLOBALS['wpo_cache_config'];
1861
1862 if (!$config) return false;
1863
1864 if (isset($config[$key])) {
1865 return $config[$key];
1866 } else {
1867 return $default;
1868 }
1869 }
1870 endif;
1871
1872 /**
1873 * Checks if cache only specific urls option enabled
1874 *
1875 * @return boolean
1876 */
1877 if (!function_exists('wpo_cache_specific_urls_only')) :
1878 function wpo_cache_specific_urls_only(): bool {
1879 return wpo_cache_config_get('cache_specific_urls_only', false);
1880 }
1881 endif;
1882
1883 if (!function_exists('wpo_read_cache_directory_htaccess')) :
1884 /**
1885 * Read .htaccess file for the cache directory.
1886 *
1887 * @return string
1888 */
1889 function wpo_read_cache_directory_htaccess() {
1890 $htaccess_filename = WPO_CACHE_FILES_DIR . '/.htaccess';
1891 return is_file($htaccess_filename) ? file_get_contents($htaccess_filename) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- WP_Filesystem not available this early
1892 }
1893 endif;
1894
1895 if (!function_exists('wpo_write_cache_directory_htaccess')) :
1896 /**
1897 * Write .htaccess file for the cache directory.
1898 *
1899 * @param string $htaccess_content
1900 *
1901 * @return void
1902 */
1903 function wpo_write_cache_directory_htaccess($htaccess_content) {
1904 $htaccess_filename = WPO_CACHE_FILES_DIR . '/.htaccess';
1905 // phpcs:disable
1906 // WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
1907 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress errors from displaying
1908 @file_put_contents($htaccess_filename, $htaccess_content);
1909 // phpcs:enable
1910 }
1911 endif;
1912
1913 if (!function_exists('wpo_allow_access_to_index_cache_files')) :
1914 /**
1915 * Update the .htaccess file to allow access to index.html files in the cache directory.
1916 *
1917 * @return void
1918 */
1919 function wpo_allow_access_to_index_cache_files() {
1920 $htaccess_content = wpo_read_cache_directory_htaccess();
1921
1922 if (false === strpos($htaccess_content, 'Allow access to index.html files')) {
1923 $allow_access_to_index_html = "\n\n# Allow access to index.html files\n<FilesMatch \"index\\.html$\">\n\tOrder allow,deny\n\tAllow from all\n</FilesMatch>";
1924 $htaccess_content .= $allow_access_to_index_html;
1925 wpo_write_cache_directory_htaccess($htaccess_content);
1926 }
1927 }
1928 endif;
1929
1930
1931 if (!function_exists('wpo_disable_cache_directories_viewing')) :
1932 /**
1933 * Create config files to disable cache directory viewing
1934 *
1935 * @return void
1936 */
1937 function wpo_disable_cache_directories_viewing() {
1938 global $is_apache, $is_IIS, $is_iis7;
1939
1940 if (!is_dir(WPO_CACHE_FILES_DIR)) return;
1941
1942 // Create a .htaccess file for apache server.
1943 if ($is_apache) {
1944 $htaccess_filename = WPO_CACHE_FILES_DIR . '/.htaccess';
1945
1946 // CS does not like heredoc
1947 // phpcs:disable
1948 $htaccess_content = <<<EOF
1949 # Disable directory browsing
1950 Options -Indexes
1951
1952 # Disable access to any files
1953 <FilesMatch ".*">
1954 Order allow,deny
1955 Deny from all
1956 </FilesMatch>
1957 EOF;
1958 // phpcs:enable
1959
1960 if (!is_file($htaccess_filename)) wpo_write_cache_directory_htaccess($htaccess_content);
1961 }
1962
1963 // Create web.config file for IIS servers.
1964 if ($is_IIS || $is_iis7) {
1965 $webconfig_filename = WPO_CACHE_FILES_DIR . '/web.config';
1966 $webconfig_content = "<configuration>\n<system.webServer>\n<authorization>\n<deny users=\"*\" />\n</authorization>\n</system.webServer>\n</configuration>\n";
1967
1968 // phpcs:disable
1969 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress the error when there is file permission issues
1970 // WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
1971 if (!is_file($webconfig_filename)) @file_put_contents($webconfig_filename, $webconfig_content);
1972 // phpcs:enable
1973 }
1974
1975 // Create empty index.php file for all servers.
1976 // phpcs:disable
1977 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress the error when there is file permission issues
1978 // WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
1979 if (!is_file(WPO_CACHE_FILES_DIR . '/index.php')) @file_put_contents(WPO_CACHE_FILES_DIR . '/index.php', '');
1980 // phpcs:enable
1981 }
1982 endif;
1983
1984 /**
1985 * Add the headers indicating why the page is not cached or served from cache
1986 *
1987 * @param string $message - The headers
1988 *
1989 * @return void
1990 */
1991 if (!function_exists('wpo_cache_add_nocache_http_header')) :
1992 function wpo_cache_add_nocache_http_header($message = '') {
1993 if (!headers_sent()) {
1994 header('WPO-Cache-Status: not cached');
1995 header('WPO-Cache-Message: '. trim(str_replace(array("\r", "\n", ':'), ' ', strip_tags($message)))); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- wp_strip_all_tags not available this early
1996 }
1997 }
1998 endif;
1999
2000 /**
2001 * Add the headers indicating why the page is not cached or served from cache by integrating with the send_headers filter
2002 *
2003 * @param string $message - The headers
2004 *
2005 * @return void
2006 */
2007 if (!function_exists('wpo_cache_add_nocache_http_header_with_send_headers_action')) :
2008 function wpo_cache_add_nocache_http_header_with_send_headers_action($message) {
2009 if ('' !== $message && !headers_sent()) {
2010 wpo_cache_add_nocache_http_header($message);
2011 }
2012 }
2013 endif;
2014
2015 /**
2016 * Check if feeds caching enabled
2017 *
2018 * @return bool
2019 */
2020 if (!function_exists('wpo_feeds_caching_enabled')) :
2021 function wpo_feeds_caching_enabled() {
2022 return apply_filters('wpo_feeds_caching_enabled', true);
2023 }
2024 endif;
2025
2026 /**
2027 * Check if REST caching enabled
2028 *
2029 * @return bool
2030 */
2031 if (!function_exists('wpo_rest_caching_enabled')) :
2032 function wpo_rest_caching_enabled() {
2033 return wpo_cache_config_get('enable_rest_caching', false);
2034 }
2035 endif;
2036
2037 if (!function_exists('wpo_debug_backtrace_summary')) {
2038 function wpo_debug_backtrace_summary($ignore_class = null, $skip_frames = 0, $pretty = true) {
2039 static $truncate_paths;
2040
2041 $trace = debug_backtrace(false); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Edge case, using for debugging purpose
2042 $caller = array();
2043 $check_class = !is_null($ignore_class);
2044 $skip_frames++; // Skip this function.
2045
2046 if (!isset($truncate_paths)) {
2047 $truncate_paths = array(
2048 wpo_normalize_path(WP_CONTENT_DIR),
2049 wpo_normalize_path(ABSPATH),
2050 );
2051 }
2052
2053 foreach ($trace as $call) {
2054 if ($skip_frames > 0) {
2055 $skip_frames--;
2056 } elseif (isset($call['class'])) {
2057 if ($check_class && $ignore_class === $call['class']) {
2058 continue; // Filter out calls.
2059 }
2060
2061 $caller[] = "{$call['class']}{$call['type']}{$call['function']}";
2062 } else {
2063 if (in_array($call['function'], array('do_action', 'apply_filters', 'do_action_ref_array', 'apply_filters_ref_array'), true)) {
2064 $caller[] = "{$call['function']}('{$call['args'][0]}')";
2065 } elseif (in_array($call['function'], array('include', 'include_once', 'require', 'require_once'), true)) {
2066 $filename = isset($call['args'][0]) ? $call['args'][0] : '';
2067 $caller[] = $call['function'] . "('" . str_replace($truncate_paths, '', wpo_normalize_path($filename)) . "')";
2068 } else {
2069 $caller[] = $call['function'];
2070 }
2071 }
2072 }
2073 if ($pretty) {
2074 return implode(', ', array_reverse($caller));
2075 } else {
2076 return $caller;
2077 }
2078 }
2079 }
2080
2081 if (!function_exists('wpo_normalize_path')) {
2082 function wpo_normalize_path($path) {
2083 // Standardise all paths to use '/'.
2084 $path = str_replace('\\', '/', $path);
2085
2086 // Replace multiple slashes down to a singular, allowing for network shares having two slashes.
2087 $path = preg_replace('|(?<=.)/+|', '/', $path);
2088
2089 // Windows paths should uppercase the drive letter.
2090 if (':' === substr($path, 1, 1)) {
2091 $path = ucfirst($path);
2092 }
2093
2094 return $path;
2095 }
2096 }
2097
2098 /**
2099 * Normalize url path
2100 *
2101 * @param string $url_path
2102 * @return string
2103 */
2104 if (!function_exists('wpo_normalize_url_path')) :
2105 function wpo_normalize_url_path($url_path) {
2106 $prev = null;
2107 $iterations = 0;
2108
2109 while ($url_path !== $prev && 5 > $iterations) {
2110 $prev = $url_path;
2111 $url_path = rawurldecode($url_path);
2112 $iterations++;
2113 }
2114
2115 $url_path = preg_replace('/\.\.?\//', '-', $url_path); // replace './' and '../' with '-' to prevent directory traversal
2116 $url_path = strtolower($url_path);
2117
2118 return $url_path;
2119 }
2120 endif;
2121
2122 /**
2123 * Get path to wp-config.php when called from WP-CLI.
2124 *
2125 * @return string
2126 */
2127 if (!function_exists('wpo_wp_cli_locate_wp_config')) :
2128 function wpo_wp_cli_locate_wp_config() {
2129 $config_path = '';
2130
2131 if (is_callable('\WP_CLI\Utils\locate_wp_config')) {
2132 $config_path = \WP_CLI\Utils\locate_wp_config();
2133 }
2134
2135 return $config_path;
2136 }
2137 endif;
2138
2139
2140 /**
2141 * Retrieves and sanitizes a value from a superglobal array in a way similar to WordPress's sanitize_text_field(),
2142 * for use before WordPress is fully loaded
2143 *
2144 * @param string $key
2145 * @param string $global_type
2146 * @return string sanitized string.
2147 */
2148 if (!function_exists('wpo_early_sanitize_superglobal_text')) :
2149 function wpo_early_sanitize_superglobal_text($key, $global_type = 'server') {
2150
2151 $str = '';
2152
2153 // phpcs:disable
2154 // Sanitized later in the code, without using WordPress functions as they are not available at this stage
2155 if ('server' === $global_type) {
2156 $str = $_SERVER[$key] ?? '';
2157 }
2158 // phpcs:enable
2159
2160 if ('' === $str || !is_scalar($str)) {
2161 return '';
2162 }
2163
2164 // Remove backslashes (simulate wp_unslash()).
2165 $str = stripslashes((string) $str);
2166
2167 // Remove ASCII control characters (0x00–0x1F and 0x7F).
2168 $str = preg_replace('/[\x00-\x1F\x7F]/u', '', $str);
2169
2170 // Trim whitespace and encode special HTML characters.
2171 return htmlspecialchars(trim($str), ENT_QUOTES, 'UTF-8');
2172 }
2173 endif;
2174