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

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