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

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