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 +1551 -870 3.4.2 → 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,259 +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 caching 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();
30 -
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 - }
34 -
35 - // Don't cache pages for logged in users.
36 - if (!function_exists('is_user_logged_in') || (function_exists('wp_get_current_user') && is_user_logged_in())) {
37 - if (!wpo_cache_loggedin_users()) {
38 - $no_cache_because[] = __('User is logged in', 'wp-optimize');
39 - } elseif (!empty($GLOBALS['wpo_cache_config']['enable_user_caching'])) {
40 - // Will only run when "Serve cached pages to logged in users" is checked
41 - $no_cache_because[] = __('User is logged in, this works only when the cache is preloaded', 'wp-optimize');
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);
42 44 }
43 - }
44 45
45 - $restricted_page_type_cache = apply_filters('wpo_restricted_cache_page_type', false);
46 - if ($restricted_page_type_cache) {
47 - $no_cache_because[] = $restricted_page_type_cache;
48 - }
49 -
50 - $conditional_tag_exceptions = apply_filters('wpo_url_in_conditional_tags_exceptions', false);
51 - if ($conditional_tag_exceptions) {
52 - $no_cache_because[] = $conditional_tag_exceptions;
53 - }
54 -
55 - // No root cache folder, so short-circuit here
56 - if (!file_exists(WPO_CACHE_DIR)) {
57 - $no_cache_because[] = __('WP-O cache parent directory was not found', 'wp-optimize').' ('.WPO_CACHE_DIR.')';
58 - } elseif (!file_exists(WPO_CACHE_FILES_DIR)) {
59 - // Try creating a folder for cached files, if it was flushed recently
60 - if (!mkdir(WPO_CACHE_FILES_DIR)) {
61 - $no_cache_because[] = __('WP-O cache directory was not found', 'wp-optimize').' ('.WPO_CACHE_FILES_DIR.')';
62 - } else {
63 - wpo_disable_cache_directories_viewing();
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 + }
64 53 }
65 - }
54 +
55 + $restricted_page_type_cache = apply_filters('wpo_restricted_cache_page_type', false);
66 56
67 - // If comments are opened and the user has saved his information.
68 - if (function_exists('comments_open') && function_exists('get_post') && get_post() && comments_open()) {
69 - $commenter = wp_get_current_commenter();
70 - // if any of the fields contain something, do not save to cache
71 - if ('' != $commenter['comment_author'] || '' != $commenter['comment_author_email'] || '' != $commenter['comment_author_url']) {
72 - $no_cache_because[] = __('Comments are opened and the visitor saved his information.', 'wp-optimize');
57 + if ($restricted_page_type_cache) {
58 + $no_cache_because[] = $restricted_page_type_cache;
73 59 }
74 - }
60 +
61 + $conditional_tag_exceptions = apply_filters('wpo_url_in_conditional_tags_exceptions', false);
75 62
76 - $can_cache_page = true;
77 -
78 - if (defined('DONOTCACHEPAGE') && DONOTCACHEPAGE) {
79 - $can_cache_page = false;
80 - }
81 -
82 - /**
83 - * Defines if the page can be cached or not
84 - *
85 - * @param boolean $can_cache_page
86 - */
87 - $can_cache_page_filter = apply_filters('wpo_can_cache_page', $can_cache_page);
88 -
89 - if (!$can_cache_page_filter) {
90 - if ($can_cache_page) {
91 - $can_cache_page = false;
92 - $no_cache_because[] = __('wpo_can_cache_page filter forbade it', 'wp-optimize');
93 - } else {
94 - $no_cache_because[] = __('DONOTCACHEPAGE constant forbade it and wpo_can_cache_page filter did not over-ride it', 'wp-optimize');
63 + if ($conditional_tag_exceptions) {
64 + $no_cache_because[] = $conditional_tag_exceptions;
95 65 }
96 - }
97 -
98 - if (defined('REST_REQUEST') && REST_REQUEST) {
99 - $no_cache_because[] = __('This is a REST API request (identified by REST_REQUEST constant)', 'wp-optimize');
100 - }
101 -
102 - // Don't cache with fatal error pages.
103 - $last_error = error_get_last();
104 - if (is_array($last_error) && E_ERROR == $last_error['type']) {
105 - $no_cache_because[] = __('This page has a fatal error', 'wp-optimize');
106 - }
107 -
108 - if (http_response_code() >= 500) {
109 - $no_cache_because[] = sprintf(__('This page has a critical error (HTTP code %s)', 'wp-optimize'), http_response_code());
110 - } elseif (http_response_code() >= 400) {
111 - $no_cache_because[] = sprintf(__('This page returned an HTTP unauthorised response code (%s)', 'wp-optimize'), http_response_code());
112 - }
113 -
114 - if (empty($no_cache_because)) {
115 -
116 - $buffer = apply_filters('wpo_pre_cache_buffer', $buffer, $flags);
117 -
118 - $url_path = wpo_get_url_path();
119 -
120 - $dirs = explode('/', $url_path);
121 -
122 - $path = WPO_CACHE_FILES_DIR;
123 -
124 - foreach ($dirs as $dir) {
125 - if (!empty($dir)) {
126 - $path .= '/' . $dir;
127 -
128 - if (!file_exists($path)) {
129 - if (!mkdir($path)) {
130 - $no_cache_because[] = __('Attempt to create subfolder within cache directory failed', 'wp-optimize')." ($path)";
131 - break;
132 - }
133 - }
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');
134 74 }
135 75 }
136 - }
137 -
138 - if (!empty($no_cache_because)) {
139 -
140 - $message = implode(', ', $no_cache_because);
141 -
142 - // Add http headers
143 - wpo_cache_add_nocache_http_header($message);
144 -
145 - if ((!defined('DOING_CRON') || !DOING_CRON) && (!defined('REST_REQUEST') || !REST_REQUEST)) {
146 - $not_cached_details = "";
147 -
148 - // Output the reason only when the user has turned on debugging
149 - if (((defined('WP_DEBUG') && WP_DEBUG) || isset($_GET['wpo_cache_debug']))) {
150 - $not_cached_details = "because: ".htmlspecialchars($message) . " ";
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();
151 86 }
152 -
153 - $buffer .= sprintf("\n<!-- WP Optimize page cache - https://getwpo.com - page NOT cached %s-->\n", $not_cached_details);
154 87 }
155 88
156 - return $buffer;
157 -
158 - } else {
159 -
160 - // Prevent mixed content when there's an http request but the site URL uses https.
161 - $home_url = get_home_url();
162 -
163 - if (!is_ssl() && 'https' === strtolower(parse_url($home_url, PHP_URL_SCHEME))) {
164 - $https_home_url = $home_url;
165 - $http_home_url = str_ireplace('https://', 'http://', $https_home_url);
166 - $buffer = str_replace(esc_url($http_home_url), esc_url($https_home_url), $buffer);
89 + // If comments are opened and the user has saved his information.
90 + if (function_exists('comments_open') && function_exists('get_post') && get_post() && comments_open()) {
91 + $commenter = wp_get_current_commenter();
92 + // if any of the fields contain something, do not save to cache
93 + if ('' !== $commenter['comment_author'] || '' !== $commenter['comment_author_email'] || '' !== $commenter['comment_author_url']) {
94 + $no_cache_because[] = __('Comments are opened and the visitor saved his information.', 'wp-optimize');
95 + }
167 96 }
168 -
169 - $modified_time = time(); // Take this as soon before writing as possible
170 - $timezone_string = '';
171 - $utc = isset($GLOBALS['wpo_cache_config']['gmt_offset']) ? (float) $GLOBALS['wpo_cache_config']['gmt_offset'] : 0;
172 - $modified_time += $utc * 3600;
173 97
174 - if (!empty($GLOBALS['wpo_cache_config']['timezone_string'])) {
175 - $timezone_string = 'UTC' !== $GLOBALS['wpo_cache_config']['timezone_string'] ? $GLOBALS['wpo_cache_config']['timezone_string'] : '';
98 + if (wpo_restricted_cache_page_with_cart_items()) {
99 + $no_cache_because[] = __('User has items in WooCommerce cart.', 'wp-optimize');
176 100 }
177 -
178 - if (!empty($timezone_string)) {
179 - $timezone_postfix = "(".$timezone_string." UTC:". $utc .")";
180 - } else {
181 - $timezone_postfix = "(UTC:" . $utc . ")";
101 +
102 + $can_cache_page = true;
103 +
104 + if (defined('DONOTCACHEPAGE') && DONOTCACHEPAGE) {
105 + $can_cache_page = false;
182 106 }
183 -
184 - $add_to_footer = '';
185 107
186 108 /**
187 - * Filter whether to display the html comment <!-- Cached by WP-Optimize ... -->
109 + * Defines if the page can be cached or not
188 110 *
189 - * @param boolean $show - Whether to display the html comment
190 - * @return boolean
111 + * @param boolean $can_cache_page
191 112 */
192 - if (preg_match('#</html>#i', $buffer) && (apply_filters('wpo_cache_show_cached_by_comment', true) || (defined('WP_DEBUG') && WP_DEBUG))) {
193 - $date_time_format = 'F j, Y g:i a';
194 - if (!empty($GLOBALS['wpo_cache_config']['date_format']) && !empty($GLOBALS['wpo_cache_config']['time_format'])) {
195 - $date_time_format = $GLOBALS['wpo_cache_config']['date_format'] . ' ' . $GLOBALS['wpo_cache_config']['time_format'];
196 - }
197 -
198 - if (!empty($GLOBALS['wpo_cache_config']['enable_mobile_caching']) && wpo_is_mobile()) {
199 - $add_to_footer .= "\n<!-- Cached by WP-Optimize - for mobile devices - https://getwpo.com - Last modified: " . gmdate($date_time_format, $modified_time) . " " . $timezone_postfix . " -->\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');
200 119 } else {
201 - $add_to_footer .= "\n<!-- Cached by WP-Optimize - https://getwpo.com - Last modified: " . gmdate($date_time_format, $modified_time) . " " . $timezone_postfix . " -->\n";
120 + $no_cache_because[] = __('DONOTCACHEPAGE constant forbade it and wpo_can_cache_page filter did not over-ride it', 'wp-optimize');
202 121 }
203 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 + }
204 137
205 - // Create an empty index.php file in the cache directory for disable directory viewing.
206 - if (!is_file($path . '/index.php')) file_put_contents($path . '/index.php', '');
207 -
208 - /**
209 - * Save $buffer into cache file.
210 - */
138 + // Get cache file name
211 139 $file_ext = '.html';
212 -
140 + $is_feed_cache = false;
141 +
213 142 if (wpo_feeds_caching_enabled()) {
214 143 if (is_feed()) {
215 144 $file_ext = '.rss-xml';
145 + $is_feed_cache = true;
216 146 }
217 147 }
148 +
149 + $cache_filename = wpo_cache_filename($file_ext);
218 150
219 - $cache_filename = wpo_cache_filename($file_ext);
220 - $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();
221 160
222 - if (defined('WPO_CACHE_FILENAME_DEBUG') && WPO_CACHE_FILENAME_DEBUG) {
223 - $add_to_footer .= "\n<!-- WP Optimize page cache debug information -->\n";
224 - if (!empty($GLOBALS['wpo_cache_filename_debug']) && is_array($GLOBALS['wpo_cache_filename_debug'])) {
225 - $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.')';
226 165 }
227 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 + */
228 246
229 - // if we can then cache gzipped content in .gz file.
230 - if (function_exists('gzencode') && apply_filters('wpo_allow_cache_gzip_files', true)) {
231 - // Only replace inside the addition, not inside the main buffer (e.g. post content)
232 - 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)));
233 - }
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 + }
234 257
235 - 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 + }
236 262
237 - if (is_callable('WP_Optimize')) {
238 - // delete cached information about cache size.
239 - WP_Optimize()->get_page_cache()->delete_cache_size_information();
240 - } else {
241 - // If the shutdown occurs before plugins are loaded,
242 - // then this will trigger a fatal error, so, we check first
243 - if (!doing_action('shutdown')) {
244 - error_log('[WPO_CACHE] WP_Optimize() is not callable.');
245 - $message = 'Please report this to WP-O support: ';
246 - if (function_exists('wp_debug_backtrace_summary')) {
247 - $message .= wp_debug_backtrace_summary();
248 - } else {
249 - $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 + }
250 275 }
251 - error_log($message);
252 276 }
253 - }
254 277
255 - header('Cache-Control: no-cache'); // Check back every time to see if re-download is necessary.
256 - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
257 - 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 + }
258 286
259 - if (wpo_cache_can_output_gzip_content()) {
260 -
261 - if (!wpo_cache_is_in_response_headers_list('Content-Encoding', 'gzip')) {
262 - 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
263 291 }
292 +
293 + file_put_contents($cache_file, $buffer); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
294 +
295 + if (is_callable('WP_Optimize')) {
296 + // delete cached information about cache size.
297 + WP_Optimize()->get_page_cache()->delete_cache_size_information();
298 + } else {
299 + // If the shutdown occurs before plugins are loaded,
300 + // then this will trigger a fatal error, so, we check first
301 + if (!doing_action('shutdown')) {
302 + // phpcs:disable
303 + // Edge case handling for debugging purpose
304 + error_log('[WPO_CACHE] WP_Optimize() is not callable.');
305 + $message = 'Please report this to WP-O support: ';
306 + if (function_exists('wp_debug_backtrace_summary')) {
307 + $message .= wp_debug_backtrace_summary();
308 + } else {
309 + $message .= wpo_debug_backtrace_summary();
310 + }
311 + error_log($message);
312 + // phpcs:enable
313 + }
314 + }
315 +
316 + header('Cache-Control: no-cache'); // Check back every time to see if re-download is necessary.
317 + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
318 + header('WPO-Cache-Status: saving to cache');
264 319
265 - // Disable php gzip to avoid double compression.
266 - ini_set('zlib.output_compression', 'Off');
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;
267 323
268 - return ob_gzhandler($buffer, $flags);
269 - } else {
270 - return $buffer;
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 + }
271 339 }
272 340 }
273 -}
274 341 endif;
275 342
276 343 /**
277 344 * Load files for support plugins.
@@ -276,52 +343,89 @@
276 343 /**
277 344 * Load files for support plugins.
278 345 */
279 346 if (!function_exists('wpo_cache_load_extensions')) :
280 -function wpo_cache_load_extensions() {
281 - $extensions = glob(WPO_CACHE_EXT_DIR . '/*.php');
282 -
283 - // Add external extensions
284 - if (defined('WPO_CACHE_CUSTOM_EXT_DIR') && is_dir(WPO_CACHE_CUSTOM_EXT_DIR)) {
285 - $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 + }
286 360 }
361 +endif;
287 362
288 - 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 + }
289 376
290 - foreach ($extensions as $extension) {
291 - 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
292 378 }
293 379 }
294 -endif;
295 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 + */
296 399 if (!function_exists('wpo_restricted_cache_page_type')) {
297 -function wpo_restricted_cache_page_type($restricted) {
298 - global $post;
299 -
300 - // Don't cache search or password protected.
301 - if ((function_exists('is_search') && is_search()) || (function_exists('is_404') && is_404()) || !empty($post->post_password)) {
302 - $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;
303 426 }
304 -
305 - // Don't cache the front page if option is set.
306 - if (in_array('/', wpo_get_url_exceptions()) && function_exists('is_front_page') && is_front_page()) {
307 -
308 - $restricted = __('In the settings, caching is disabled for the front page', 'wp-optimize');
309 - }
310 -
311 - // Don't cache htacesss. Remember to properly escape any output to prevent injection.
312 - if (strpos($_SERVER['REQUEST_URI'], '.htaccess') !== false) {
313 - $restricted = 'The file path is unsuitable for caching ('.$_SERVER['REQUEST_URI'].')';
314 - }
315 -
316 - // Don't cache feeds.
317 - if (function_exists('is_feed') && is_feed() && !wpo_feeds_caching_enabled()) {
318 - $restricted = __('We don\'t cache RSS feeds', 'wp-optimize');
319 - }
320 -
321 - return $restricted;
322 427 }
323 -}
324 428
325 429 /**
326 430 * Returns true if we need cache content for loggedin users.
327 431 *
@@ -327,11 +431,11 @@
327 431 *
328 432 * @return bool
329 433 */
330 434 if (!function_exists('wpo_cache_loggedin_users')) :
331 -function wpo_cache_loggedin_users() {
332 - 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());
333 -}
435 + function wpo_cache_loggedin_users() {
436 + return !empty($GLOBALS['wpo_cache_config']['enable_user_caching']) || !empty($GLOBALS['wpo_cache_config']['enable_user_specific_cache']) || (function_exists('wpo_we_cache_per_role') && wpo_we_cache_per_role());
437 + }
334 438 endif;
335 439
336 440 /**
337 441 * Get filename for store cache, depending on gzip, mobile and cookie settings.
@@ -339,88 +443,131 @@
339 443 * @param string $ext
340 444 * @return string
341 445 */
342 446 if (!function_exists('wpo_cache_filename')) :
343 -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 + }
344 456
345 - $wpo_cache_filename_debug = array();
346 -
347 - $filename = 'index';
348 -
349 - if (wpo_cache_mobile_caching_enabled() && wpo_is_mobile()) {
350 - $filename = 'mobile.' . $filename;
351 - }
352 -
353 - if (wpo_webp_images_enabled() && !wpo_is_using_webp_images_redirection() && wpo_is_using_alter_html()) {
354 - $filename = $filename . '.webp';
355 - }
356 -
357 - $cookies = wpo_cache_cookies();
358 -
359 - $cache_key = '';
360 -
361 - /**
362 - * Add cookie values to filename if need.
363 - * This section was inspired by things learned from WP-Rocket.
364 - */
365 - if (!empty($cookies)) {
366 - foreach ($cookies as $key => $cookie_name) {
367 - if (is_array($cookie_name) && isset($_COOKIE[$key])) {
368 - foreach ($cookie_name as $cookie_key) {
369 - if (isset($_COOKIE[$key][$cookie_key]) && '' !== $_COOKIE[$key][$cookie_key]) {
370 - $_cache_key = $cookie_key.'='.$_COOKIE[$key][$cookie_key];
371 - $_cache_key = preg_replace('/[^a-z0-9_\-\=]/i', '-', $_cache_key);
372 - $cache_key .= '-' . $_cache_key;
373 - $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 + }
374 480 }
481 + continue;
375 482 }
376 - 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 + }
377 490 }
378 -
379 - if (isset($_COOKIE[$cookie_name]) && '' !== $_COOKIE[$cookie_name]) {
380 - $_cache_key = $cookie_name.'='.$_COOKIE[$cookie_name];
381 - $_cache_key = preg_replace('/[^a-z0-9_\-\=]/i', '-', $_cache_key);
382 - $cache_key .= '-' . $_cache_key;
383 - $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 + }
384 507 }
385 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;
386 519 }
520 +endif;
387 521
388 - $query_variables = wpo_cache_query_variables();
389 522
390 - /**
391 - * Add GET variables to cache file name if need.
392 - */
393 - if (!empty($query_variables)) {
394 - foreach ($query_variables as $variable) {
395 - if (isset($_GET[$variable]) && !empty($_GET[$variable])) {
396 - $_cache_key = $variable.'='.$_GET[$variable];
397 - $_cache_key = preg_replace('/[^a-z0-9_\-\=]/i', '-', $_cache_key);
398 - $cache_key .= '-' . $_cache_key;
399 - $wpo_cache_filename_debug[] = 'GET parameter: name: ' . $variable . ', value:' . htmlentities($_GET[$variable]) . ', cache_key:' . $_cache_key;
400 - }
401 - }
402 - }
403 -
404 - // Adding cache key with queried cookies and variables to the cache file name.
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) {
405 533 if ('' !== $cache_key) {
406 534 // Add human-readable cache key to the filename
407 - $filename .= str_replace('--', '-', '-'.$cache_key);
535 + $filename .= preg_replace('/\-+/', '-', '-'.$cache_key);
408 536 }
409 537
410 - $filename = apply_filters('wpo_cache_filename', $filename);
411 -
412 538 // Trimming filename if it exceeds 240 characters due to filesystem limitations
413 539 if (strlen($filename) > 240) {
414 540 $filename = substr($filename, 0, 199) . '-' . sha1($filename);
415 541 }
416 542
417 - $wpo_cache_filename_debug[] = 'Extension: ' . $ext;
418 - $wpo_cache_filename_debug[] = 'Filename: ' . $filename.$ext;
543 + return $filename;
544 +}
545 +endif;
419 546
420 - $GLOBALS['wpo_cache_filename_debug'] = $wpo_cache_filename_debug;
421 547
422 - return $filename . $ext;
548 +if (!function_exists('wpo_rest_cache_filename')) :
549 +/**
550 + * Builds the rest cache filename, uses passed params.
551 + *
552 + * @param array $params
553 + * @return string
554 + */
555 +function wpo_rest_cache_filename($params) {
556 + $filename = 'index';
557 + $cache_key = '';
558 +
559 + if (!empty($params)) {
560 + ksort($params);
561 + foreach ($params as $key => $value) {
562 + if (is_array($value)) $value = serialize($value);
563 + $_cache_key = $key.'_'.$value;
564 + $_cache_key = preg_replace('/[^a-z0-9_\-]/i', '-', $_cache_key);
565 + $cache_key .= '-' . $_cache_key;
566 + }
567 + }
568 +
569 + return wpo_build_cache_filename($filename, $cache_key) . '.json';
423 570 }
424 571 endif;
425 572
426 573 /**
@@ -426,14 +573,14 @@
426 573 /**
427 574 * Returns site url from site_url() function or if it is not available from cache configuration.
428 575 */
429 576 if (!function_exists('wpo_site_url')) :
430 -function wpo_site_url() {
431 - if (is_callable('site_url')) return site_url('/');
432 -
433 - $site_url = empty($GLOBALS['wpo_cache_config']['site_url']) ? '' : $GLOBALS['wpo_cache_config']['site_url'];
434 - return $site_url;
435 -}
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 + }
436 583 endif;
437 584
438 585 /**
439 586 * Get cookie names which impact on cache file name.
@@ -440,12 +587,12 @@
440 587 *
441 588 * @return array
442 589 */
443 590 if (!function_exists('wpo_cache_cookies')) :
444 -function wpo_cache_cookies() {
445 - $cookies = empty($GLOBALS['wpo_cache_config']['wpo_cache_cookies']) ? array() : $GLOBALS['wpo_cache_config']['wpo_cache_cookies'];
446 - return $cookies;
447 -}
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 + }
448 595 endif;
449 596
450 597 /**
451 598 * Get GET variable names which impact on cache file name.
@@ -452,21 +599,21 @@
452 599 *
453 600 * @return array
454 601 */
455 602 if (!function_exists('wpo_cache_query_variables')) :
456 -function wpo_cache_query_variables() {
457 - if (defined('WPO_CACHE_URL_PARAMS') && WPO_CACHE_URL_PARAMS) {
458 - $variables = array_keys($_GET);
459 - } else {
460 - $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);
461 615 }
462 -
463 - if (!empty($variables)) {
464 - sort($variables);
465 - }
466 -
467 - return wpo_cache_maybe_ignore_query_variables($variables);
468 -}
469 616 endif;
470 617
471 618 /**
472 619 * Get list of all received HTTP headers.
@@ -473,37 +620,37 @@
473 620 *
474 621 * @return array
475 622 */
476 623 if (!function_exists('wpo_get_http_headers')) :
477 -function wpo_get_http_headers() {
478 -
479 - static $headers;
480 -
481 - if (!empty($headers)) return $headers;
482 -
483 - $headers = array();
484 -
485 - // if is apache server then use get allheaders() function.
486 - if (function_exists('getallheaders')) {
487 - $headers = getallheaders();
488 - } else {
489 - // https://www.php.net/manual/en/function.getallheaders.php
490 - foreach ($_SERVER as $key => $value) {
491 -
492 - $key = strtolower($key);
493 -
494 - if ('HTTP_' == substr($key, 0, 5)) {
495 - $headers[str_replace(' ', '-', ucwords(str_replace('_', ' ', substr($key, 5))))] = $value;
496 - } elseif ('content_type' == $key) {
497 - $headers["Content-Type"] = $value;
498 - } elseif ('content_length' == $key) {
499 - $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 + }
500 648 }
501 649 }
650 +
651 + return $headers;
502 652 }
503 -
504 - return $headers;
505 -}
506 653 endif;
507 654
508 655 /**
509 656 * Check if requested Accept-Encoding headers has gzip value.
@@ -510,15 +657,15 @@
510 657 *
511 658 * @return bool
512 659 */
513 660 if (!function_exists('wpo_cache_gzip_accepted')) :
514 -function wpo_cache_gzip_accepted() {
515 - $headers = wpo_get_http_headers();
516 -
517 - if (isset($headers['Accept-Encoding']) && preg_match('/gzip/i', $headers['Accept-Encoding'])) return true;
518 -
519 - return false;
520 -}
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 + }
521 668 endif;
522 669
523 670 /**
524 671 * Check if we can output gzip content in current answer, i.e. check Accept-Encoding headers has gzip value
@@ -526,11 +673,11 @@
526 673 *
527 674 * @return bool
528 675 */
529 676 if (!function_exists('wpo_cache_can_output_gzip_content')) :
530 -function wpo_cache_can_output_gzip_content() {
531 - return wpo_cache_gzip_accepted() && function_exists('ob_gzhandler');
532 -}
677 + function wpo_cache_can_output_gzip_content() {
678 + return wpo_cache_gzip_accepted() && function_exists('ob_gzhandler');
679 + }
533 680 endif;
534 681
535 682 /**
536 683 * Check if header with certain name exists in already prepared headers and has value comparable with $header_value.
@@ -540,29 +687,29 @@
540 687 *
541 688 * @return bool
542 689 */
543 690 if (!function_exists('wpo_cache_is_in_response_headers_list')) :
544 -function wpo_cache_is_in_response_headers_list($header_name, $header_value) {
545 - $headers_list = headers_list();
546 -
547 - if (!empty($headers_list)) {
548 - $header_name = strtolower($header_name);
549 -
550 - foreach ($headers_list as $value) {
551 - $value = explode(':', $value);
552 -
553 - if (strtolower($value[0]) == $header_name) {
554 - if (preg_match('/'.$header_value.'/', $value[1])) {
555 - return true;
556 - } else {
557 - 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 + }
558 706 }
559 707 }
560 708 }
709 +
710 + return false;
561 711 }
562 -
563 - return false;
564 -}
565 712 endif;
566 713
567 714 /**
568 715 * Check if mobile cache is enabled and current request is from moblile device.
@@ -569,12 +716,12 @@
569 716 *
570 717 * @return bool
571 718 */
572 719 if (!function_exists('wpo_cache_mobile_caching_enabled')) :
573 -function wpo_cache_mobile_caching_enabled() {
574 - if (!empty($GLOBALS['wpo_cache_config']['enable_mobile_caching'])) return true;
575 - return false;
576 -}
720 + function wpo_cache_mobile_caching_enabled() {
721 + if (!empty($GLOBALS['wpo_cache_config']['enable_mobile_caching'])) return true;
722 + return false;
723 + }
577 724 endif;
578 725
579 726 /**
580 727 * Check if webp images enabled
@@ -588,15 +735,27 @@
588 735 }
589 736 endif;
590 737
591 738 /**
592 - * Check whether webp images using alter html method or not
739 + * Check whether the requesting browser supports WebP images
593 740 *
594 741 * @return bool
595 742 */
596 -if (!function_exists('wpo_is_using_alter_html')) :
597 - function wpo_is_using_alter_html() {
598 - 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;
599 758 }
600 759 endif;
601 760
602 761 /**
@@ -606,15 +765,15 @@
606 765 */
607 766 if (!function_exists('wpo_is_using_webp_images_redirection')) :
608 767 function wpo_is_using_webp_images_redirection() {
609 768 if (empty($GLOBALS['wpo_cache_config']['uploads'])) return false;
610 -
769 +
611 770 $uploads_dir = $GLOBALS['wpo_cache_config']['uploads'];
612 771 $htaccess_file = $uploads_dir . '/.htaccess';
613 772 if (!file_exists($htaccess_file)) return false;
614 - $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
615 774 $comment_sections = array('Register webp mime type', 'WP-Optimize WebP Rules');
616 -
775 +
617 776 if (function_exists('str_contains')) {
618 777 return str_contains($htaccess_content, $comment_sections[0]) && str_contains($htaccess_content, $comment_sections[1]);
619 778 } else {
620 779 return strpos($htaccess_content, $comment_sections[0]) && strpos($htaccess_content, $comment_sections[1]);
@@ -628,9 +787,9 @@
628 787 * @return bool
629 788 */
630 789 if (!function_exists('wpo_is_activity_stream_requested')) :
631 790 function wpo_is_activity_stream_requested() {
632 - return (isset($_SERVER['HTTP_ACCEPT']) && false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/activity+json'));
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
633 792 }
634 793 endif;
635 794
636 795 /**
@@ -637,112 +796,358 @@
637 796 * Verify if the current request is robots.txt
638 797 */
639 798 if (!function_exists('wpo_is_robots_txt_requested')) :
640 799 function wpo_is_robots_txt_requested() {
641 - return (isset($_SERVER['REQUEST_URI']) && 'robots.txt' === basename($_SERVER['REQUEST_URI']));
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
642 801 }
643 802 endif;
644 803
645 804 /**
646 805 * Serves the cache and exits
806 + *
807 + * @return void
647 808 */
648 809 if (!function_exists('wpo_serve_cache')) :
649 -function wpo_serve_cache() {
650 - $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();
651 815
652 - $file_name_rss_xml = wpo_cache_filename('.rss-xml');
653 - $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 = '';
654 822
655 - $path_dir = WPO_CACHE_FILES_DIR . '/' . wpo_get_url_path() . '/';
656 - $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
657 836
658 - if (wpo_feeds_caching_enabled()) {
659 - // check for .xml cache file if .html cache file doesn't exist
660 - if (!file_exists($path_dir . $file_name) && file_exists($path_dir . $file_name_rss_xml)) {
661 - $path = $path_dir . $file_name_rss_xml;
662 - $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 + }
663 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 + }
664 928 }
929 +endif;
665 930
666 - $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;
667 944
668 - // if we can use gzip and gzipped file exist in cache we use it.
669 - // if headers already sent we don't use gzipped file content.
670 - if (!headers_sent() && wpo_cache_gzip_accepted() && file_exists($path . '.gz')) {
671 - $path .= '.gz';
672 - $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');
673 954 }
955 +endif;
674 956
675 - $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;
676 982
677 - // Cache has expired, purge and exit.
678 - if (!empty($GLOBALS['wpo_cache_config']['page_cache_length'])) {
679 - if (time() > ($GLOBALS['wpo_cache_config']['page_cache_length'] + $modified_time)) {
680 - wpo_delete_files($path);
681 - return;
682 - }
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;
683 995 }
684 996
685 - if ($use_gzip) {
686 - // Disable zlib output compression to avoid double content compression
687 - ini_set('zlib.output_compression', 'Off');
997 + if (wpo_is_activity_stream_requested()) {
998 + return false;
688 999 }
689 1000
690 - $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 + }
691 1005
692 - 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 + }
693 1011
694 - if (!empty($modified_time) && !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $modified_time) {
695 - if ($use_gzip && !$gzip_header_already_sent) {
696 - 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';
697 1025 }
698 -
699 - if ($send_as_feed) {
700 - 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)';
701 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 + }
702 1046
703 - header('WPO-Cache-Status: cached');
704 - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
705 - header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified', true, 304);
706 - exit;
1047 + if (wpo_restricted_cache_page_with_cart_items()) {
1048 + $no_cache_because[] = 'User has items in WooCommerce cart.';
707 1049 }
708 1050
709 - 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 + }
710 1055
711 - if (wpo_is_canonical_redirection_needed()) return;
1056 + $current_url = wpo_current_url();
712 1057
713 - if ($use_gzip && !$gzip_header_already_sent) {
714 - header('Content-Encoding: gzip');
715 - }
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 + }
716 1062
717 - // send correct headers for xml and txt files
718 - $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 + }
719 1067
720 - if (preg_match('/\.xml$/i', $filename)) {
721 - header('Content-type: text/xml');
722 - }
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
723 1072
724 - if (preg_match('/\.txt$/i', $filename)) {
725 - header('Content-type: text/plain');
726 - }
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);
727 1075
728 - if ($send_as_feed) {
729 - 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";
730 1078 }
1079 + }
731 1080
732 - header('WPO-Cache-Status: cached');
733 - if (!empty($modified_time)) {
734 - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified_time) . ' GMT');
735 - }
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));
736 1083
737 - 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;
738 1094
739 - 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']);
740 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;
741 1123 }
742 1124 endif;
743 1125
744 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 +/**
745 1150 * Checks and does redirection, if needed
746 1151 *
747 1152 * @return bool
748 1153 */
@@ -749,21 +1154,22 @@
749 1154 if (!function_exists('wpo_is_canonical_redirection_needed')) :
750 1155 function wpo_is_canonical_redirection_needed() {
751 1156 $permalink_structure = isset($GLOBALS['wpo_cache_config']['permalink_structure']) ? $GLOBALS['wpo_cache_config']['permalink_structure'] : '';
752 1157 $site_url = wpo_site_url();
753 -
1158 +
754 1159 // Exit if server variables are not available.
755 1160 if (!isset($_SERVER['HTTP_HOST'])) return false;
756 1161
757 1162 $schema = isset($_SERVER['HTTPS']) && 'on' === $_SERVER['HTTPS'] ? "https" : "http";
758 - $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
759 1165 $requested_url = $schema . $url_part;
760 - $url_parts = parse_url($requested_url);
761 - $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) : '';
762 1168
763 - if (!empty($permalink_structure) && $requested_url != $site_url && ((isset($url_parts['path']) && '/' !== $url_parts['path']) || isset($url_parts['query']))) {
764 - $request_uri = rtrim($_SERVER['REQUEST_URI'], '?');
765 - if ('/' == substr($permalink_structure, -1) && empty($extension) && empty($url_parts['query']) && empty($url_parts['fragment'])) {
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'])) {
766 1172 $url = preg_replace('/(.+?)([\/]*)(\[\?\#][^\/]+|$)/', '$1/$3', $request_uri);
767 1173 if (0 !== strcmp($request_uri, $url)) return true;
768 1174 } else {
769 1175 $url = rtrim($request_uri, '/');
@@ -777,18 +1183,18 @@
777 1183 /**
778 1184 * Clears the cache
779 1185 */
780 1186 if (!function_exists('wpo_cache_flush')) :
781 -function wpo_cache_flush() {
782 -
783 - if (defined('WPO_CACHE_FILES_DIR') && '' != WPO_CACHE_FILES_DIR) wpo_delete_files(WPO_CACHE_FILES_DIR);
784 -
785 - if (function_exists('wp_cache_flush')) {
786 - 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');
787 1196 }
788 -
789 - do_action('wpo_cache_flush');
790 -}
791 1197 endif;
792 1198
793 1199 /**
794 1200 * Get URL path for caching
@@ -796,31 +1202,27 @@
796 1202 * @since 1.0
797 1203 * @return string
798 1204 */
799 1205 if (!function_exists('wpo_get_url_path')) :
800 -function wpo_get_url_path($url = '') {
801 - $url = '' == $url ? wpo_current_url() : $url;
802 - $url_parts = parse_url($url);
803 -
804 - if (isset($url_parts['path']) && false !== stripos($url_parts['path'], '/index.php')) {
805 - $url_parts['path'] = preg_replace('/(.*?)index\.php(\/.+)/i', '$1index-php$2', $url_parts['path']);
806 - }
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
807 1209
808 - /*
809 - * Convert the hexadecimal digits within the percent-encoded triplet to uppercase, to ensure that the path remains
810 - * consistent. For instance, "example.com/%e0%a6" will be converted to "example.com/%E0%A6".
811 - */
812 - if (isset($url_parts['path'])) {
813 - $url_parts['path'] = preg_replace_callback('/%[0-9A-F]{2}/i', function($matches) {
814 - return strtoupper($matches[0]);
815 - }, $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 + }
1214 +
1215 + if (isset($url_parts['path']) && false !== stripos($url_parts['path'], '/index.')) {
1216 + $url_parts['path'] = preg_replace('/(.*?)index\.(php|html)(\/.+)/i', '$1index-$2$3', $url_parts['path']);
1217 + $url_parts['path'] = preg_replace('/index\.(php|html)/i', 'index-$1', $url_parts['path']);
1218 + }
1219 +
1220 + if (!isset($url_parts['host'])) $url_parts['host'] = '';
1221 + if (!isset($url_parts['path'])) $url_parts['path'] = '';
1222 +
1223 + return $url_parts['host'].$url_parts['path'];
816 1224 }
817 -
818 - if (!isset($url_parts['host'])) $url_parts['host'] = '';
819 - if (!isset($url_parts['path'])) $url_parts['path'] = '';
820 -
821 - return $url_parts['host'].$url_parts['path'];
822 -}
823 1225 endif;
824 1226
825 1227 /**
826 1228 * Get requested url.
@@ -827,19 +1229,21 @@
827 1229 *
828 1230 * @return string
829 1231 */
830 1232 if (!function_exists('wpo_current_url')) :
831 -function wpo_current_url() {
832 - // 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).
833 - // Otherwise this function would return a different URL at the beginning and end of the cache process.
834 - static $url = '';
835 - if ('' != $url) return $url;
836 - $http_host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
837 - $url = rtrim('http' . ((isset($_SERVER['HTTPS']) && ('on' == $_SERVER['HTTPS'] || 1 == $_SERVER['HTTPS']) ||
838 - isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO']) ? 's' : '' )
839 - . '://' . $http_host.$_SERVER['REQUEST_URI'], '/');
840 - return $url;
841 -}
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 + }
842 1246 endif;
843 1247
844 1248 /**
845 1249 * Return list of conditional tag exceptions.
@@ -846,42 +1250,42 @@
846 1250 *
847 1251 * @return array
848 1252 */
849 1253 if (!function_exists('wpo_get_conditional_tags_exceptions')) :
850 -function wpo_get_conditional_tags_exceptions() {
851 - static $exceptions = null;
852 -
853 - if (null !== $exceptions) return $exceptions;
854 -
855 - if (!empty($GLOBALS['wpo_cache_config'])) {
856 - if (empty($GLOBALS['wpo_cache_config']['cache_exception_conditional_tags'])) {
857 - $exceptions = array();
858 -
859 - } 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 + }
860 1268
861 - $exceptions = $GLOBALS['wpo_cache_config']['cache_exception_conditional_tags'];
862 -
863 - }
864 -
865 - } elseif (class_exists('WPO_Page_Cache')) {
866 -
867 - $config = WPO_Page_Cache::instance()->config->get();
868 -
869 - if (is_array($config) && array_key_exists('cache_exception_conditional_tags', $config)) {
870 - $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 +
871 1282 } else {
872 1283 $exceptions = array();
873 1284 }
874 -
875 - $exceptions = is_array($exceptions) ? $exceptions : preg_split('#(\n|\r|\r\n)#', $exceptions);
876 - $exceptions = array_filter($exceptions, 'trim');
877 1285
878 - } else {
879 - $exceptions = array();
1286 + return $exceptions;
880 1287 }
881 -
882 - return $exceptions;
883 -}
884 1288 endif;
885 1289
886 1290 /**
887 1291 * Return list of url exceptions.
@@ -888,38 +1292,66 @@
888 1292 *
889 1293 * @return array
890 1294 */
891 1295 if (!function_exists('wpo_get_url_exceptions')) :
892 -function wpo_get_url_exceptions() {
893 - static $exceptions = null;
894 -
895 - if (null !== $exceptions) return $exceptions;
896 -
897 - // if called from file-based-page-cache.php when WP loading
898 - // and cache settings exists then use it otherwise get settings from database.
899 - if (!empty($GLOBALS['wpo_cache_config'])) {
900 - 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 {
901 1321 $exceptions = array();
902 - } else {
903 - $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']);
904 1322 }
905 - } elseif (class_exists('WPO_Page_Cache')) {
906 - $config = WPO_Page_Cache::instance()->config->get();
1323 +
1324 + return apply_filters('wpo_get_url_exceptions', $exceptions);
1325 + }
1326 +endif;
907 1327
908 - if (is_array($config) && array_key_exists('cache_exception_urls', $config)) {
909 - $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 + }
910 1348 } else {
911 - $exceptions = array();
1349 + $cache_include_urls = array();
912 1350 }
913 -
914 - $exceptions = is_array($exceptions) ? $exceptions : preg_split('#(\n|\r)#', $exceptions);
915 - $exceptions = array_filter($exceptions, 'trim');
916 - } else {
917 - $exceptions = array();
1351 +
1352 + return apply_filters('wpo_get_cache_include_urls', $cache_include_urls);
918 1353 }
919 -
920 - return apply_filters('wpo_get_url_exceptions', $exceptions);
921 -}
922 1354 endif;
923 1355
924 1356 /**
925 1357 * Return true of exception url matches current url
@@ -928,12 +1360,12 @@
928 1360 * @param bool $regex Whether to check with regex or not.
929 1361 * @return bool true if matched, false otherwise
930 1362 */
931 1363 if (!function_exists('wpo_current_url_exception_match')) :
932 -function wpo_current_url_exception_match($exception) {
933 -
934 - return wpo_url_exception_match(wpo_current_url(), $exception);
935 -}
1364 + function wpo_current_url_exception_match($exception) {
1365 +
1366 + return wpo_url_exception_match(wpo_current_url(), $exception);
1367 + }
936 1368 endif;
937 1369
938 1370 /**
939 1371 * Check if url in conditional tags exceptions list.
@@ -940,31 +1372,32 @@
940 1372 *
941 1373 * @return string
942 1374 */
943 1375 if (!function_exists('wpo_url_in_conditional_tags_exceptions')) :
944 -function wpo_url_in_conditional_tags_exceptions() {
945 -
946 - $exceptions = wpo_get_conditional_tags_exceptions();
947 - $restricted = '';
948 - $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');
949 - //Filter for add more conditional tags to whitelist in the exceptions list.
950 - $allowed_functions = apply_filters('wpo_allowed_conditional_tags_exceptions', $allowed_functions);
951 - if (!empty($exceptions)) {
952 - foreach ($exceptions as $exception) {
953 - if (false !== strpos($exception, 'is_')) {
954 - $exception_function = $exception;
955 - if ('()' == substr($exception, -2)) {
956 - $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 + }
957 1395 }
958 -
959 - if (in_array($exception_function, $allowed_functions) && function_exists($exception_function) && call_user_func($exception_function)) {
960 - $restricted = sprintf(__('In the settings, caching is disabled for %s', 'wp-optimize'), $exception_function);
961 - }
962 1396 }
963 1397 }
1398 + return $restricted;
964 1399 }
965 - return $restricted;
966 -}
967 1400 endif;
968 1401
969 1402
970 1403 /**
@@ -974,26 +1407,72 @@
974 1407 *
975 1408 * @return bool
976 1409 */
977 1410 if (!function_exists('wpo_url_in_exceptions')) :
978 -function wpo_url_in_exceptions($url) {
979 - $exceptions = wpo_get_url_exceptions();
1411 + function wpo_url_in_exceptions($url) {
1412 + $exceptions = wpo_get_url_exceptions();
1413 +
1414 + if (!empty($exceptions)) {
1415 + foreach ($exceptions as $exception) {
1416 +
1417 + // don't check / - front page using regexp, we handle it in wpo_restricted_cache_page_type()
1418 + if ('/' === $exception) continue;
1419 +
1420 + if (wpo_url_exception_match($url, $exception)) {
1421 + // Exception match.
1422 + return true;
1423 + }
1424 + }
1425 + }
1426 +
1427 + return false;
1428 + }
1429 +endif;
980 1430
981 - if (!empty($exceptions)) {
982 - foreach ($exceptions as $exception) {
1431 +/**
1432 + * Checks if URL matches against listed include.
1433 + *
1434 + * Supports:
1435 + * - Root-based paths (e.g., /page)
1436 + * - Wildcards (*) in the last segment
1437 + * - One optional parent directory (sub-dir multisite)
1438 + *
1439 + * @param string $url
1440 + * @return bool
1441 + */
1442 +if (!function_exists('wpo_url_in_cache_include')) :
1443 + function wpo_url_in_cache_include($url): bool {
1444 + $url = preg_replace('/\?.*/', '', $url); // Remove query string
1445 + $url = rtrim($url, '/'); // normalize URL (remove trailing slash)
983 1446
984 - // don't check / - front page using regexp, we handle it in wpo_restricted_cache_page_type()
985 - if ('/' == $exception) continue;
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, '/');
986 1451
987 - if (wpo_url_exception_match($url, $exception)) {
988 - // Exception match.
989 - return true;
1452 + $cache_include_urls = wpo_get_cache_include_urls();
1453 +
1454 + if (!empty($cache_include_urls) && is_array($cache_include_urls)) {
1455 + foreach ($cache_include_urls as $include_url) {
1456 + $include_url = rtrim($include_url, '/'); // normalize include URL (remove trailing slash)
1457 + if (strpos($include_url, '/') === 0) {
1458 + // Support wildcards and allow one subdirectory max
1459 + $pattern = preg_quote($include_url, '#');
1460 + $pattern = str_replace('\*', '.*', $pattern);
1461 +
1462 + if (preg_match('#^(/[^/]+)?' . $pattern . '$#i', $path)) {
1463 + return true;
1464 + }
1465 + }
1466 +
1467 + if (wpo_url_exception_match($url, $include_url)) {
1468 + return true;
1469 + }
990 1470 }
991 1471 }
1472 +
1473 + return false;
992 1474 }
993 -
994 - return false;
995 -}
996 1475 endif;
997 1476
998 1477 /**
999 1478 * Checks if an URL matches against listed exceptions.
@@ -1003,51 +1482,39 @@
1003 1482 *
1004 1483 * @return bool
1005 1484 */
1006 1485 if (!function_exists('wpo_url_exception_match')) :
1007 -function wpo_url_exception_match($url, $exception) {
1008 - if (preg_match('#^[\s]*$#', $exception)) {
1009 - return false;
1010 - }
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);
1011 1510
1012 - $exception = str_replace('*', '.*', $exception);
1511 + if (!$exception) return false;
1013 1512
1014 - $exception = trim($exception);
1015 -
1016 - // Used to test websites placed in subdirectories.
1017 - $sub_dir = '';
1018 -
1019 - // If exception defined from root i.e. /page1 then remove domain part in url.
1020 - if (preg_match('/^\//', $exception)) {
1021 - // get site sub directory.
1022 - $sub_dir = preg_replace('#^(http|https):\/\/.*\/#Ui', '', wpo_site_url());
1023 - // add prefix slash and remove slash.
1024 - $sub_dir = ('' == $sub_dir) ? '' : '/' . rtrim($sub_dir, '/');
1025 - // get relative path
1026 - $url = preg_replace('#^(http|https):\/\/.*\/#Ui', '/', $url);
1513 + $exception = urldecode($exception);
1514 +
1515 + return (preg_match('#^'.$exception.'$#i', $url) || preg_match('#^'.$sub_dir.$exception.'$#i', $url));
1027 1516 }
1028 -
1029 - $url = urldecode(rtrim($url, '/')) . '/';
1030 - $exception = rtrim($exception, '/');
1031 -
1032 - // if we have no wildcard in the end of exception then add slash.
1033 - if (!preg_match('#\(\.\*\)$#', $exception)) $exception .= '/';
1034 -
1035 - $exception = preg_quote($exception);
1036 -
1037 - // fix - unescape some possibly escaped mask characters
1038 - $search = array(
1039 - '\\.\\*',
1040 - '\\-',
1041 - );
1042 - $replace = array(
1043 - '.*',
1044 - '-',
1045 - );
1046 - $exception = urldecode(str_replace($search, $replace, $exception));
1047 -
1048 - return (preg_match('#^'.$exception.'$#i', $url) || preg_match('#^'.$sub_dir.$exception.'$#i', $url));
1049 -}
1050 1517 endif;
1051 1518
1052 1519 /**
1053 1520 * Checks if its a mobile device
@@ -1054,114 +1521,194 @@
1054 1521 *
1055 1522 * @see https://developer.wordpress.org/reference/functions/wp_is_mobile/
1056 1523 */
1057 1524 if (!function_exists('wpo_is_mobile')) :
1058 -function wpo_is_mobile() {
1059 - if (empty($_SERVER['HTTP_USER_AGENT'])) {
1060 - $is_mobile = false;
1061 - // many mobile devices (all iPhone, iPad, etc.)
1062 - } elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') !== false
1063 - || strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false
1064 - || strpos($_SERVER['HTTP_USER_AGENT'], 'Silk/') !== false
1065 - || strpos($_SERVER['HTTP_USER_AGENT'], 'Kindle') !== false
1066 - || strpos($_SERVER['HTTP_USER_AGENT'], 'BlackBerry') !== false
1067 - || strpos($_SERVER['HTTP_USER_AGENT'], 'Opera Mini') !== false
1068 - || strpos($_SERVER['HTTP_USER_AGENT'], 'Opera Mobi') !== false
1069 - ) {
1070 - $is_mobile = true;
1071 - } else {
1072 - $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;
1073 1544 }
1074 -
1075 - return $is_mobile;
1076 -}
1077 1545 endif;
1078 1546
1079 1547 /**
1080 - * Check if current browser agent is not disabled in options.
1548 + * Converts a wildcard mask to a regular expression pattern.
1081 1549 *
1082 - * @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.
1083 1553 */
1084 -if (!function_exists('wpo_is_accepted_user_agent')) :
1085 -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);
1086 1557
1087 - $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 + }
1088 1561
1089 - if (!empty($exceptions)) {
1090 - foreach ($exceptions as $exception) {
1091 - if ('' == trim($exception)) continue;
1562 + // Convert wildcard to regex
1563 + $mask = str_replace('*', '.*', $mask);
1092 1564
1093 - 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, '/') . '/';
1094 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;
1095 1581 }
1582 +endif;
1096 1583
1097 - return true;
1098 -}
1099 -endif;
1100 1584
1101 -if (!function_exists('wpo_delete_files')) :
1102 1585 /**
1103 - * Deletes a specified source file or directory.
1586 + * Check if current browser agent is not disabled in options.
1104 1587 *
1105 - * If $src is a file, only that file will be deleted. If $src is a directory, the behavior depends on the
1106 - * $recursive parameter. When $recursive is true, the directory and its contents (including files and subdirectories)
1107 - * will be deleted. When $recursive is false, only the files in the top-level directory(eg. $src directory) will be deleted,
1108 - * while the $src directory itself and its subdirectories will remain untouched.
1588 + * @param string $user_agent
1109 1589 *
1110 - * @param string $src The path to the source file or directory to delete.
1111 - * @param bool $recursive (Optional) When set to true, the directory and its contents (including files and subdirectories)
1112 - * will be deleted. If false, only the files in the top-level directory will be deleted while
1113 - * its subdirectories will be preserved. Defaults to true.
1114 - *
1115 - * @return bool Returns true if the specified file or all files within the specified directory (and its subdirectories,
1116 - * when $recursive is true) are successfully deleted. Returns false if any file(s) could not be deleted
1117 - * due to file permissions or other reasons.
1590 + * @return bool
1118 1591 */
1119 -function wpo_delete_files($src, $recursive = true) {
1120 - // If the source doesn't exist, consider it deleted and return true
1121 - if (!file_exists($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 +
1122 1608 return true;
1123 1609 }
1610 +endif;
1124 1611
1125 - /*
1126 - * If the source is a file, delete it and return the result.
1127 - * If `unlink()` fails, we also verify if the file still exists before returning the result, as another
1128 - * PHP process may have already deleted the file between the execution of `is_file()` and `unlink()` operations.
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.
1129 1629 */
1130 - if (is_file($src)) {
1131 - if (!@unlink($src) && file_exists($src)) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1132 - return false;
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;
1133 1634 }
1134 - return true;
1135 - }
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 + }
1136 1669
1137 - $success = true;
1138 -
1139 - // If recursive is false, delete only the top-level files and return the result
1140 - if (!$recursive) {
1670 + $full_path = rtrim($src, '/\\') . DIRECTORY_SEPARATOR . $file;
1671 +
1672 + // If it's a file, delete it
1673 + if (is_file($full_path)) {
1674 + if (!wpo_delete_files($full_path)) {
1675 + $success = false;
1676 + }
1677 + }
1678 + }
1679 +
1680 + closedir($dir_handle);
1681 + } else {
1682 + if (file_exists($src)) {
1683 + $success = false;
1684 + }
1685 + }
1686 +
1687 + return $success;
1688 + }
1689 +
1690 + // If recursive is true, delete all files and directories recursively
1141 1691 $dir_handle = @opendir($src); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1142 -
1692 +
1143 1693 /*
1144 1694 * If opendir() is successful, process directory contents. If not, check if the directory exists.
1145 1695 * If it exists, return false (failure). Otherwise, assume it's already deleted and return true (success).
1146 1696 */
1147 1697 if (false !== $dir_handle) {
1148 -
1698 +
1149 1699 while (false !== ($file = readdir($dir_handle))) {
1150 - if ('.' == $file || '..' == $file) {
1700 + if ('.' === $file || '..' === $file) {
1151 1701 continue;
1152 1702 }
1153 1703
1154 - $full_path = $src . '/' . $file;
1155 -
1156 - // If it's a file, delete it
1157 - if (is_file($full_path)) {
1158 - if (!wpo_delete_files($full_path)) {
1159 - $success = false;
1160 - }
1704 + $full_path = rtrim($src, '/\\') . DIRECTORY_SEPARATOR . $file;
1705 +
1706 + if (!wpo_delete_files($full_path)) {
1707 + $success = false;
1161 1708 }
1162 1709 }
1163 -
1710 +
1164 1711 closedir($dir_handle);
1165 1712 } else {
1166 1713 if (file_exists($src)) {
1167 1714 $success = false;
@@ -1167,111 +1714,83 @@
1167 1714 $success = false;
1168 1715 }
1169 1716 }
1170 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 +
1171 1734 return $success;
1172 1735 }
1736 +endif;
1173 1737
1174 - // If recursive is true, delete all files and directories recursively
1175 - $dir_handle = @opendir($src); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1176 -
1177 - /*
1178 - * If opendir() is successful, process directory contents. If not, check if the directory exists.
1179 - * If it exists, return false (failure). Otherwise, assume it's already deleted and return true (success).
1738 +if (!function_exists('wpo_is_empty_dir')) :
1739 + /**
1740 + * Check if selected directory is empty or has only index.php which we added for security reasons.
1741 + *
1742 + * @param string $dir
1743 + *
1744 + * @return bool
1180 1745 */
1181 - if (false !== $dir_handle) {
1182 -
1183 - while (false !== ($file = readdir($dir_handle))) {
1184 - if ('.' == $file || '..' == $file) {
1185 - continue;
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;
1186 1761 }
1187 1762
1188 - $full_path = $src . '/' . $file;
1189 -
1190 - if (!wpo_delete_files($full_path)) {
1191 - $success = false;
1192 - }
1763 + $file = readdir($handle);
1193 1764 }
1194 -
1195 - closedir($dir_handle);
1196 - } else {
1197 - if (file_exists($src)) {
1198 - $success = false;
1199 - }
1765 +
1766 + closedir($handle);
1767 + return $is_empty;
1200 1768 }
1201 -
1202 - /*
1203 - * Delete the source directory itself.
1204 - * Success of `rmdir` operation is not recorded; we only ultimately care about emptying, not removing
1205 - * entirely (empty folders in our context are harmless)
1206 - */
1207 - if ($success) {
1208 - @rmdir($src); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
1209 - }
1210 -
1211 - // Delete cached information about cache size
1212 - WP_Optimize()->get_page_cache()->delete_cache_size_information();
1213 -
1214 - return $success;
1215 -}
1216 1769 endif;
1217 1770
1218 -if (!function_exists('wpo_is_empty_dir')) :
1219 1771 /**
1220 - * Check if selected directory is empty or has only index.php which we added for security reasons.
1221 - *
1222 - * @param string $dir
1223 - *
1224 - * @return bool
1225 - */
1226 -function wpo_is_empty_dir($dir) {
1227 - if (!file_exists($dir) || !is_dir($dir)) return false;
1228 -
1229 - $handle = opendir($dir);
1230 -
1231 - if (false === $handle) return false;
1232 -
1233 - $is_empty = true;
1234 - $file = readdir($handle);
1235 -
1236 - while (false !== $file) {
1237 -
1238 - if ('.' != $file && '..' != $file && 'index.php' != $file) {
1239 - $is_empty = false;
1240 - break;
1241 - }
1242 -
1243 - $file = readdir($handle);
1244 - }
1245 -
1246 - closedir($handle);
1247 - return $is_empty;
1248 -}
1249 -endif;
1250 -
1251 -/**
1252 1772 * Either store for later output, or output now. Only the most-recent call will be effective.
1253 1773 *
1254 1774 * @param String|Null $output - if not null, then the string to use when called by the shutdown action.
1255 1775 */
1256 1776 if (!function_exists('wpo_cache_add_footer_output')) :
1257 -function wpo_cache_add_footer_output($output = null) {
1258 -
1259 - static $buffered = null;
1260 -
1261 - if (function_exists('current_filter') && 'shutdown' == current_filter()) {
1262 - // Only add the line if it was a page, not something else (e.g. REST response)
1263 - if (function_exists('did_action') && did_action('wp_footer')) {
1264 - echo "\n<!-- WP Optimize page cache - https://getwpo.com - ".$buffered." -->\n";
1265 - } elseif (defined('WPO_CACHE_DEBUG') && WPO_CACHE_DEBUG && (!defined('REST_REQUEST') || !REST_REQUEST)) {
1266 - 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 + }
1267 1791 }
1268 - } else {
1269 - if (null == $buffered && function_exists('add_action')) add_action('shutdown', 'wpo_cache_add_footer_output', 11);
1270 - $buffered = $output;
1271 1792 }
1272 -
1273 -}
1274 1793 endif;
1275 1794
1276 1795 /**
1277 1796 * Remove variable names that shouldn't influence cache.
@@ -1280,46 +1799,53 @@
1280 1799 *
1281 1800 * @return array
1282 1801 */
1283 1802 if (!function_exists('wpo_cache_maybe_ignore_query_variables')) :
1284 -function wpo_cache_maybe_ignore_query_variables($variables) {
1285 -
1286 - /**
1287 - * Filters the current $_GET variables that will be used when caching or excluding from cache.
1288 - * Currently:
1289 - * - 'wpo_cache_debug' (Shows the reason for not being cached even when WP_DEBUG isn't set)
1290 - * - 'doing_wp_cron' (alternative cron)
1291 - * - 'aiosp_sitemap_path', 'aiosp_sitemap_page' (All in one SEO sitemap)
1292 - * - 'xml_sitemap', 'seopress_sitemap', 'seopress_news', 'seopress_video', 'seopress_cpt', 'seopress_paged' (SEOPress sitemap)
1293 - * - 'sitemap', 'sitemap_n' (YOAST SEO sitemap)
1294 - */
1295 - $exclude_variables = array(
1296 - 'wpo_cache_debug', // Shows the reason for not being cached even when WP_DEBUG isn't set
1297 - 'doing_wp_cron', // alternative cron
1298 - 'aiosp_sitemap_path', // All in one SEO sitemap
1299 - 'aiosp_sitemap_page',
1300 - 'xml_sitemap', // SEOPress sitemap
1301 - 'seopress_sitemap',
1302 - 'seopress_news',
1303 - 'seopress_video',
1304 - 'seopress_cpt',
1305 - 'seopress_paged',
1306 - 'sitemap', // YOAST SEO sitemap
1307 - 'sitemap_n',
1308 - );
1309 - $exclude_variables = function_exists('apply_filters') ? apply_filters('wpo_cache_ignore_query_variables', $exclude_variables) : $exclude_variables;
1310 -
1311 - if (empty($exclude_variables)) return $variables;
1312 -
1313 - foreach ($exclude_variables as $variable) {
1314 - $exclude = array_search($variable, $variables);
1315 - if (false !== $exclude) {
1316 - 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);
1317 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;
1318 1847 }
1319 -
1320 - return $variables;
1321 -}
1322 1848 endif;
1323 1849
1324 1850 /**
1325 1851 * Get cache config
@@ -1329,35 +1855,99 @@
1329 1855 *
1330 1856 * @return mixed
1331 1857 */
1332 1858 if (!function_exists('wpo_cache_config_get')) :
1333 -function wpo_cache_config_get($key, $default = false) {
1334 - $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;
1335 1871
1336 - 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;
1337 1882
1338 - if (isset($config[$key])) {
1339 - return $config[$key];
1340 - } else {
1341 - 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
1342 1892 }
1343 -}
1344 1893 endif;
1345 1894
1346 -if (!function_exists('wpo_disable_cache_directories_viewing')) :
1347 -function wpo_disable_cache_directories_viewing() {
1348 - 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;
1349 1912
1350 - 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;
1351 1929
1352 - // Create .htaccess file for apache server.
1353 - if ($is_apache) {
1354 - $htaccess_filename = WPO_CACHE_FILES_DIR . '/.htaccess';
1355 1930
1356 - // CS does not like heredoc
1357 - // phpcs:disable
1358 - $htaccess_content = <<<EOF
1359 -# 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
1360 1950 Options -Indexes
1361 1951
1362 1952 # Disable access to any files
1363 1953 <FilesMatch ".*">
@@ -1362,26 +1952,34 @@
1362 1952 # Disable access to any files
1363 1953 <FilesMatch ".*">
1364 1954 Order allow,deny
1365 1955 Deny from all
1366 -</FilesMatch>
1956 +</FilesMatch>
1367 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', '');
1368 1980 // phpcs:enable
1369 -
1370 - if (!is_file($htaccess_filename)) @file_put_contents($htaccess_filename, $htaccess_content); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress the error when there is file permission issues
1371 1981 }
1372 -
1373 - // Create web.config file for IIS servers.
1374 - if ($is_IIS || $is_iis7) {
1375 - $webconfig_filename = WPO_CACHE_FILES_DIR . '/web.config';
1376 - $webconfig_content = "<configuration>\n<system.webServer>\n<authorization>\n<deny users=\"*\" />\n</authorization>\n</system.webServer>\n</configuration>\n";
1377 -
1378 - if (!is_file($webconfig_filename)) @file_put_contents($webconfig_filename, $webconfig_content); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress the error when there is file permission issues
1379 - }
1380 -
1381 - // Create empty index.php file for all servers.
1382 - if (!is_file(WPO_CACHE_FILES_DIR . '/index.php')) @file_put_contents(WPO_CACHE_FILES_DIR . '/index.php', '');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress the error when there is file permission issues
1383 -}
1384 1982 endif;
1385 1983
1386 1984 /**
1387 1985 * Add the headers indicating why the page is not cached or served from cache
@@ -1393,9 +1991,9 @@
1393 1991 if (!function_exists('wpo_cache_add_nocache_http_header')) :
1394 1992 function wpo_cache_add_nocache_http_header($message = '') {
1395 1993 if (!headers_sent()) {
1396 1994 header('WPO-Cache-Status: not cached');
1397 - header('WPO-Cache-Message: '. trim(str_replace(array("\r", "\n", ':'), ' ', strip_tags($message))));
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
1398 1996 }
1399 1997 }
1400 1998 endif;
1401 1999
@@ -1406,16 +2004,11 @@
1406 2004 *
1407 2005 * @return void
1408 2006 */
1409 2007 if (!function_exists('wpo_cache_add_nocache_http_header_with_send_headers_action')) :
1410 - function wpo_cache_add_nocache_http_header_with_send_headers_action($message = '') {
1411 - static $buffered_message = null;
1412 -
1413 - if (function_exists('current_filter') && 'send_headers' === current_filter() && $buffered_message && !headers_sent()) {
1414 - wpo_cache_add_nocache_http_header($buffered_message);
1415 - } else {
1416 - if (!$buffered_message && function_exists('add_action')) add_action('send_headers', 'wpo_cache_add_nocache_http_header_with_send_headers_action', 11);
1417 - $buffered_message = $message;
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);
1418 2011 }
1419 2012 }
1420 2013 endif;
1421 2014
@@ -1429,17 +2022,28 @@
1429 2022 return apply_filters('wpo_feeds_caching_enabled', true);
1430 2023 }
1431 2024 endif;
1432 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 +
1433 2037 if (!function_exists('wpo_debug_backtrace_summary')) {
1434 2038 function wpo_debug_backtrace_summary($ignore_class = null, $skip_frames = 0, $pretty = true) {
1435 2039 static $truncate_paths;
1436 -
1437 - $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
1438 2042 $caller = array();
1439 2043 $check_class = !is_null($ignore_class);
1440 2044 $skip_frames++; // Skip this function.
1441 -
2045 +
1442 2046 if (!isset($truncate_paths)) {
1443 2047 $truncate_paths = array(
1444 2048 wpo_normalize_path(WP_CONTENT_DIR),
1445 2049 wpo_normalize_path(ABSPATH),
@@ -1444,17 +2048,17 @@
1444 2048 wpo_normalize_path(WP_CONTENT_DIR),
1445 2049 wpo_normalize_path(ABSPATH),
1446 2050 );
1447 2051 }
1448 -
2052 +
1449 2053 foreach ($trace as $call) {
1450 2054 if ($skip_frames > 0) {
1451 2055 $skip_frames--;
1452 2056 } elseif (isset($call['class'])) {
1453 - if ($check_class && $ignore_class == $call['class']) {
2057 + if ($check_class && $ignore_class === $call['class']) {
1454 2058 continue; // Filter out calls.
1455 2059 }
1456 -
2060 +
1457 2061 $caller[] = "{$call['class']}{$call['type']}{$call['function']}";
1458 2062 } else {
1459 2063 if (in_array($call['function'], array('do_action', 'apply_filters', 'do_action_ref_array', 'apply_filters_ref_array'), true)) {
1460 2064 $caller[] = "{$call['function']}('{$call['args'][0]}')";
@@ -1477,16 +2081,93 @@
1477 2081 if (!function_exists('wpo_normalize_path')) {
1478 2082 function wpo_normalize_path($path) {
1479 2083 // Standardise all paths to use '/'.
1480 2084 $path = str_replace('\\', '/', $path);
1481 -
2085 +
1482 2086 // Replace multiple slashes down to a singular, allowing for network shares having two slashes.
1483 2087 $path = preg_replace('|(?<=.)/+|', '/', $path);
1484 -
2088 +
1485 2089 // Windows paths should uppercase the drive letter.
1486 2090 if (':' === substr($path, 1, 1)) {
1487 2091 $path = ucfirst($path);
1488 2092 }
2093 +
2094 + return $path;
2095 + }
2096 +}
1489 2097
1490 - 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;
1491 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');
1492 2172 }
2173 +endif;