PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.6.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.6.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.6.0, at cache/file-based-page-cache-functions.php

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