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