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/class-wpo-page-cache.php +740 -190 3.2.6 → 4.7.0 View file →
@@ -21,19 +21,10 @@
21 21 * Directory that stores config and related files
22 22 */
23 23 if (!defined('WPO_CACHE_CONFIG_DIR')) define('WPO_CACHE_CONFIG_DIR', WPO_CACHE_DIR.'/config');
24 24
25 -/**
26 - * Directory that stores the cache, including gzipped files and mobile specifc cache
27 - */
28 -if (!defined('WPO_CACHE_FILES_DIR')) define('WPO_CACHE_FILES_DIR', untrailingslashit(WP_CONTENT_DIR).'/cache/wpo-cache');
29 -
30 25 require_once dirname(__FILE__) . '/file-based-page-cache-functions.php';
31 26
32 -if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
33 - require_once dirname(__FILE__) . '/php-5.3-functions.php';
34 -}
35 -
36 27 wpo_cache_load_extensions();
37 28
38 29 if (!class_exists('WPO_Page_Cache')) :
39 30
@@ -53,11 +44,18 @@
53 44 */
54 45 public $logger;
55 46
56 47 /**
48 + * File Logger for this class
49 + *
50 + * @var mixed
51 + */
52 + private $file_logger;
53 +
54 + /**
57 55 * Instance of this class
58 56 *
59 - * @var mixed
57 + * @var WPO_Page_Cache | null
60 58 */
61 59 public static $instance;
62 60
63 61 /**
@@ -89,18 +87,43 @@
89 87 */
90 88 private $_minimum_advanced_cache_file_version = '3.0.17';
91 89
92 90 /**
91 + * Flag to decide whether to purge or not
92 + *
93 + * @var bool
94 + */
95 + public $should_purge;
96 +
97 + /**
98 + * Flag to decide whether to purge or not
99 + *
100 + * @var bool
101 + */
102 + private $files_to_ignore = array('index.php','.htaccess');
103 +
104 + /**
93 105 * Set everything up here
94 106 */
95 107 public function __construct() {
96 108 $this->config = WPO_Cache_Config::instance();
97 - $this->rules = WPO_Cache_Rules::instance();
109 + if (empty($GLOBALS['wpo_cache_config'])) {
110 + $config_file_path = $this->config->get_config_file_path();
111 + if (file_exists($config_file_path)) {
112 + include_once($config_file_path);
113 + }
114 + }
115 +
116 + WPO_Cache_Rules::instance();
117 + WP_Optimize_Page_Cache_Preloader::instance();
118 +
98 119 $this->logger = new Updraft_PHP_Logger();
120 + $this->file_logger = new Updraft_File_Logger(WP_Optimize_Utils::get_log_file_path('cache'));
99 121
100 122 add_action('activated_plugin', array($this, 'activate_deactivate_plugin'));
101 123 add_action('deactivate_plugin', array($this, 'activate_deactivate_plugin'));
102 124 add_action('wpo_purge_old_cache', array($this, 'purge_old'));
125 + add_action('wpo_prune_cache_logs', array($this, 'prune_cache_logs'));
103 126
104 127 /**
105 128 * Regenerate config file on cache flush.
106 129 */
@@ -105,8 +128,9 @@
105 128 * Regenerate config file on cache flush.
106 129 */
107 130 add_action('wpo_cache_flush', array($this, 'update_cache_config'));
108 131 add_action('wpo_cache_flush', array($this, 'delete_cache_size_information'));
132 + add_action('wpo_cache_flush', array($this, 'update_cache_cleared_time'));
109 133 add_action('update_option_permalink_structure', array($this, 'update_option_permalink_structure'), 10, 3);
110 134
111 135 // Add purge cache link to admin bar.
112 136 add_filter('wpo_cache_admin_bar_menu_items', array($this, 'admin_bar_purge_cache'), 20, 1);
@@ -115,25 +139,147 @@
115 139 add_action('wp_loaded', array($this, 'handle_purge_single_page_cache'));
116 140
117 141 add_action('admin_init', array($this, 'admin_init'));
118 142
143 + add_action('update_option_gmt_offset', array($this, 'update_gmt_offset_timezone_string_config'), 10, 3);
144 + add_action('update_option_timezone_string', array($this, 'update_gmt_offset_timezone_string_config'), 10, 3);
145 + add_action('update_option_date_format', array($this, 'update_option_date_format'), 10, 2);
146 + add_action('update_option_time_format', array($this, 'update_option_time_format'), 10, 2);
147 +
119 148 $this->check_compatibility_issues();
120 149
150 + $this->ensure_cron_schedules_filter_registered();
151 +
152 + add_action('wpo_save_images_settings', array($this, 'update_webp_images_option'));
153 +
154 + add_action('wpo_preload_url', array($this, 'maybe_preload_url'));
155 +
156 + // Setup filters for exceptions.
157 + add_filter('wpo_restricted_cache_page_type', 'wpo_restricted_cache_page_type');
158 + add_filter('wpo_url_in_conditional_tags_exceptions', 'wpo_url_in_conditional_tags_exceptions');
159 + }
160 +
161 + /**
162 + * Ensures that the cron-related filter is registered.
163 + *
164 + * If `init` has already run, the filter is registered immediately.
165 + * Otherwise, it schedules the registration to occur during `init`.
166 + *
167 + * @return void
168 + */
169 + private function ensure_cron_schedules_filter_registered() {
170 + if (did_action('init')) {
171 + $this->register_cron_schedules_filter();
172 + } else {
173 + add_action('init', array($this, 'register_cron_schedules_filter'));
174 + }
175 + }
176 +
177 + /**
178 + * Registers the custom cron schedule filter for WP-Cron.
179 + *
180 + * This method hooks the class's `cron_schedules()` method into WordPress's
181 + * `cron_schedules` filter, allowing the plugin to define custom cron intervals.
182 + *
183 + * It is recommended to call this method on or after the `init` action to ensure
184 + * that translations and other dependencies are properly loaded before the filter runs.
185 + *
186 + * @return void
187 + */
188 + public function register_cron_schedules_filter() {
121 189 add_filter('cron_schedules', array($this, 'cron_schedules'));
122 - add_action('wpo_save_images_settings', array($this, 'update_webp_images_option'));
123 190 }
124 191
125 192 /**
126 - * Activate cron job for deleting expired cache files
193 + * Determines whether the current page should be cached.
194 + *
195 + * This method checks cache rules to identify if caching is possible.
196 + * If caching is not allowed, it adds appropriate HTTP headers and debug messages.
197 + *
198 + * @return bool|array True if the page should be cached, false otherwise or not empty array with the reasons why the page isn't cached
127 199 */
200 + public function should_cache_page() {
201 +
202 + if (!$this->is_enabled()) return false;
203 +
204 + return wpo_can_serve_from_cache();
205 + }
206 +
207 + /**
208 + * Maybe add information about why the page is not cached to the headers and footer output
209 + *
210 + * @param array $no_cache_because
211 + * @return void
212 + */
213 + public function maybe_add_no_cache_because_info($no_cache_because) {
214 + // Add http header
215 + if (!wp_doing_cron() && !empty($no_cache_because)) {
216 + $no_cache_because_message = join(", ", $no_cache_because);
217 + wpo_cache_add_nocache_http_header_with_send_headers_action($no_cache_because_message);
218 +
219 + $not_cached_details = "";
220 +
221 + // Output the reason only when the user has turned on debugging
222 + if ((defined('WP_DEBUG') && WP_DEBUG) || !empty(TeamUpdraft\WP_Optimize\Includes\Fragments\fetch_superglobal('get', 'wpo_cache_debug'))) {
223 + $not_cached_details = "because: ".$no_cache_because_message;
224 + }
225 + wpo_cache_add_footer_output(sprintf("Page not served from cache %s", $not_cached_details));
226 + }
227 + }
228 +
229 + /**
230 + * Handle cache settings update
231 + *
232 + * @param array $new_settings
233 + * @return void
234 + */
235 + public function cache_settings_updated($new_settings) {
236 +
237 + $this->unschedule_purge_old_cache();
238 +
239 + if ($new_settings['enable_page_caching']) {
240 + $this->schedule_purge_old_cache();
241 + }
242 + }
243 +
244 + /**
245 + * Activate cron job when the cache is enabled for deleting expired cache files
246 + */
128 247 public function cron_activate() {
248 + if ($this->is_enabled()) {
249 + $this->schedule_purge_old_cache();
250 + } else {
251 + $this->unschedule_purge_old_cache();
252 + }
253 + }
254 +
255 + /**
256 + * Schedules events to purge old cache
257 + *
258 + * @return void
259 + */
260 + private function schedule_purge_old_cache() {
129 261 $page_cache_length = $this->config->get_option('page_cache_length');
262 +
130 263 if (!wp_next_scheduled('wpo_purge_old_cache')) {
131 - wp_schedule_event(time() + (false === $page_cache_length ? '86400' : $page_cache_length), 'wpo_purge_old_cache', 'wpo_purge_old_cache');
264 + wp_schedule_event(time() + (false === $page_cache_length ? 86400 : $page_cache_length), 'wpo_purge_old_cache', 'wpo_purge_old_cache');
132 265 }
266 + if (!wp_next_scheduled('wpo_prune_cache_logs')) {
267 + wp_schedule_event(time(), 'weekly', 'wpo_prune_cache_logs');
268 + }
133 269 }
134 270
135 271 /**
272 + * Unschedules events to purge old cache
273 + *
274 + * @return void
275 + */
276 + private function unschedule_purge_old_cache() {
277 + wp_clear_scheduled_hook('wpo_purge_old_cache');
278 + wp_clear_scheduled_hook('wpo_prune_cache_logs');
279 + }
280 +
281 + /**
136 282 * Do required actions on activate/deactivate any plugin.
137 283 */
138 284 public function activate_deactivate_plugin() {
139 285
@@ -140,14 +286,11 @@
140 286 wp_clear_scheduled_hook('wpo_purge_old_cache');
141 287
142 288 $this->update_cache_config();
143 289
144 - /**
145 - * Filters whether activating / deactivating a plugin will purge the cache.
146 - */
147 - if (apply_filters('wpo_purge_page_cache_on_activate_deactivate_plugin', true)) {
148 - $this->purge();
149 - }
290 + $is_cache_purged = $this->purge();
291 +
292 + if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: plugin activation/deactivation");
150 293 }
151 294
152 295 /**
153 296 * Check if current user can purge cache.
@@ -154,10 +297,11 @@
154 297 *
155 298 * @return bool
156 299 */
157 300 public function can_purge_cache() {
158 - if (is_multisite()) return $this->is_enabled() && (current_user_can('manage_network_options') || WP_Optimize()->can_purge_the_cache());
159 - return $this->is_enabled() && (current_user_can('manage_options') || WP_Optimize()->can_purge_the_cache());
301 + if (!$this->is_enabled()) return false;
302 +
303 + return WP_Optimize_Utils::current_user_can_purge_cache();
160 304 }
161 305
162 306 /**
163 307 * Add Purge from cache in admin bar.
@@ -168,12 +312,13 @@
168 312 public function admin_bar_purge_cache($menu_items) {
169 313 global $pagenow;
170 314 if (!$this->can_purge_cache()) return $menu_items;
171 315
172 - $act_url = remove_query_arg(array('wpo_single_page_cache_purged', 'wpo_all_pages_cache_purged'));
316 + $act_url = WP_Optimize_Utils::get_url_without_cache_purge_params();
173 317
174 318 $cache_size = $this->get_cache_size();
175 319 $cache_size_info = '<h4>'.__('Page cache', 'wp-optimize').'</h4>';
320 + // translators: %d is the number of files in the cache
176 321 $cache_size_info .= '<span>'.__('Cache size:', 'wp-optimize').' '. WP_Optimize()->format_size($cache_size['size']).' '.sprintf(__('(%d files)', 'wp-optimize'), $cache_size['file_count']).'</span>';
177 322
178 323 $menu_items[] = array(
179 324 'id' => 'wpo_cache_stats',
@@ -193,9 +338,9 @@
193 338 ),
194 339 'parent' => 'wpo_purge_cache',
195 340 );
196 341
197 - if (!is_admin() || 'post.php' == $pagenow) {
342 + if (!is_admin() || 'post.php' === $pagenow) {
198 343 $menu_items[] = array(
199 344 'id' => 'wpo_purge_this_page_cache',
200 345 'title' => __('Purge cache for this page', 'wp-optimize'),
201 346 'href' => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_single_page_cache'), $act_url),
@@ -217,22 +362,38 @@
217 362
218 363 if (!$this->can_purge_cache()) return;
219 364
220 365 if (isset($_GET['wpo_single_page_cache_purged']) || isset($_GET['wpo_all_pages_cache_purged'])) {
366 + // phpcs:disable
367 + // We are not using $_GET values, just checks if the variable is set and then use hard coded string
221 368 if (isset($_GET['wpo_single_page_cache_purged'])) {
222 - $notice_function = $_GET['wpo_single_page_cache_purged'] ? 'notice_purge_single_page_cache_success' : 'notice_purge_single_page_cache_error';
369 + $message = $_GET['wpo_single_page_cache_purged'] ? __('Cache purged for this page', 'wp-optimize') : __('Error purging cache for this page', 'wp-optimize');
370 + $type = $_GET['wpo_single_page_cache_purged'] ? 'success' : 'error';
223 371 } else {
224 - $notice_function = $_GET['wpo_all_pages_cache_purged'] ? 'notice_purge_all_pages_cache_success' : 'notice_purge_all_pages_cache_error';
372 + $message = $_GET['wpo_all_pages_cache_purged'] ? __('Cache purged for all pages', 'wp-optimize') : __('Error purging cache for all pages', 'wp-optimize');
373 + $type = $_GET['wpo_all_pages_cache_purged'] ? 'success' : 'error';
225 374 }
375 + // phpcs:enable
226 376
227 - add_action('admin_notices', array($this, $notice_function));
377 + if (is_admin()) {
378 + $admin_notices_hook = is_network_admin() ? 'network_admin_notices' : 'admin_notices';
228 379
380 + add_action($admin_notices_hook, function() use ($message, $type) {
381 + $this->show_notice($message, $type);
382 + });
383 +
384 + add_action('admin_head', array('WP_Optimize_Utils', 'script_to_remove_cache_purge_params_from_url'));
385 + } else {
386 + printf('<script>window.onload = function() {alert("%s");}</script>', esc_js($message));
387 + WP_Optimize_Utils::script_to_remove_cache_purge_params_from_url();
388 + }
389 +
229 390 return;
230 391 }
231 392
232 393 if (!isset($_GET['_wpo_purge'])) return;
233 394
234 - if (wp_verify_nonce($_GET['_wpo_purge'], 'wpo_purge_single_page_cache')) {
395 + if (wp_verify_nonce(sanitize_key($_GET['_wpo_purge']), 'wpo_purge_single_page_cache')) {
235 396 $success = false;
236 397
237 398 if (is_admin()) {
238 399 $post = isset($_GET['post']) ? (int) $_GET['post'] : 0;
@@ -237,55 +398,56 @@
237 398 if (is_admin()) {
238 399 $post = isset($_GET['post']) ? (int) $_GET['post'] : 0;
239 400 if ($post > 0) {
240 401 $success = self::delete_single_post_cache($post);
402 + if ($success) $this->file_log("Cache for URL: {{URL}} has been purged, triggered by: " . __METHOD__, $post);
241 403 }
242 404 } else {
243 - $success = self::delete_cache_by_url(wpo_current_url());
405 + $url = wpo_current_url();
406 + $success = self::delete_cache_by_url($url);
407 + if ($success) $this->file_log("Cache for URL: " . self::remove_query_params($url) . " has been purged, triggered by: " . __METHOD__);
244 408 }
245 409
246 - // remove nonce from url and reload page.
247 - wp_redirect(add_query_arg('wpo_single_page_cache_purged', $success, remove_query_arg('_wpo_purge')));
410 + // Remove all existing purge-related parameters and add the new one
411 + $redirect_url = WP_Optimize_Utils::get_url_without_cache_purge_params();
412 + $redirect_url = add_query_arg('wpo_single_page_cache_purged', $success, $redirect_url);
413 +
414 + wp_redirect($redirect_url);
248 415 exit;
249 416
250 - } elseif (wp_verify_nonce($_GET['_wpo_purge'], 'wpo_purge_all_pages_cache')) {
417 + } elseif (wp_verify_nonce(sanitize_key($_GET['_wpo_purge']), 'wpo_purge_all_pages_cache')) {
251 418 $success = self::purge();
419 + $this->maybe_set_preload_cron_job();
420 + if ($success) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
252 421
253 - // remove nonce from url and reload page.
254 - wp_redirect(add_query_arg('wpo_all_pages_cache_purged', $success, remove_query_arg('_wpo_purge')));
422 + // Remove all existing purge-related parameters and add the new one
423 + $redirect_url = WP_Optimize_Utils::get_url_without_cache_purge_params();
424 + $redirect_url = add_query_arg('wpo_all_pages_cache_purged', $success, $redirect_url);
425 +
426 + wp_redirect($redirect_url);
255 427 exit;
256 428 }
257 429 }
258 430
259 431 /**
260 - * Show notification when page cache purged successfully.
432 + * Sets a preload cron job to run after 30 seconds
433 + *
434 + * As preloading may take time for larger sites, we are opting for an immediate cron job instead
435 + * of instant preloading to prevent the current request from slowing down the page load
436 + *
437 + * @return void
261 438 */
262 - public function notice_purge_single_page_cache_success() {
263 - $this->show_notice(__('The page cache was successfully purged.', 'wp-optimize'), 'success');
264 - }
439 + private function maybe_set_preload_cron_job() {
440 + $wpo_page_cache_preloader = WP_Optimize_Page_Cache_Preloader::instance();
265 441
266 - /**
267 - * Show notification when page cache wasn't purged.
268 - */
269 - public function notice_purge_single_page_cache_error() {
270 - $this->show_notice(__('The page cache was not purged.', 'wp-optimize'), 'error');
442 + if ($wpo_page_cache_preloader->is_scheduled_preload_enabled() || $this->should_auto_preload_purged_contents()) {
443 + if (!wp_next_scheduled('wpo_page_cache_run_preload')) {
444 + wp_schedule_single_event(time() + 30, 'wpo_page_cache_run_preload');
445 + }
446 + }
271 447 }
272 -
448 +
273 449 /**
274 - * Show notification when all pages cache purged successfully.
275 - */
276 - public function notice_purge_all_pages_cache_success() {
277 - $this->show_notice(__('The page cache was successfully purged.', 'wp-optimize'), 'success');
278 - }
279 -
280 - /**
281 - * Show notification when all pages cache wasn't purged.
282 - */
283 - public function notice_purge_all_pages_cache_error() {
284 - $this->show_notice(__('The page cache was not purged.', 'wp-optimize'), 'error');
285 - }
286 -
287 - /**
288 450 * Show notification in WordPress admin.
289 451 *
290 452 * @param string $message HTML (no further escaping is performed)
291 453 * @param string $type error, warning, success, or info
@@ -298,10 +460,10 @@
298 460 window.addEventListener('load', function() {
299 461 (function(wp) {
300 462 if (window.wp && wp.hasOwnProperty('data') && 'function' == typeof wp.data.dispatch) {
301 463 wp.data.dispatch('core/notices').createNotice(
302 - '<?php echo $type; ?>',
303 - '<?php echo $message; ?>',
464 + '<?php echo esc_html($type); ?>',
465 + '<?php echo wp_kses_post($message); ?>',
304 466 {
305 467 isDismissible: true,
306 468 }
307 469 );
@@ -309,10 +471,10 @@
309 471 })(window.wp);
310 472 });
311 473 </script>
312 474 <?php else : ?>
313 - <div class="notice wpo-notice notice-<?php echo $type; ?> is-dismissible">
314 - <p><?php echo $message; ?></p>
475 + <div class="notice wpo-notice notice-<?php echo esc_attr($type); ?> is-dismissible">
476 + <p><?php echo wp_kses_post($message); ?></p>
315 477 </div>
316 478 <?php
317 479 endif;
318 480 }
@@ -324,8 +486,9 @@
324 486 *
325 487 * @return WP_Error|bool - true on success, error otherwise
326 488 */
327 489 public function enable($force_enable = false) {
490 + global $is_apache;
328 491 static $already_ran_enable = false;
329 492
330 493 if ($already_ran_enable) return $already_ran_enable;
331 494
@@ -334,16 +497,30 @@
334 497 $already_ran_enable = $folders_created;
335 498 return $already_ran_enable;
336 499 }
337 500
501 + if (!file_exists($this->config->get_config_file_path())) {
502 + $result = $this->update_cache_config();
503 + if (is_wp_error($result)) return $result;
504 + }
505 +
338 506 // if WPO_ADVANCED_CACHE isn't set, or environment doesn't contain the right constant, force regeneration
339 507 if (!defined('WPO_ADVANCED_CACHE') || !defined('WP_CACHE')) {
340 508 $force_enable = true;
341 509 }
342 510
511 + $this->maybe_regenerate_cache_config_file();
512 +
513 + // Enable `.htaccess` rules to serve cached files
514 + if ($is_apache && apply_filters('wpo_serve_cache_via_htaccess', false)) {
515 + wpo_disable_cache_directories_viewing();
516 + wpo_allow_access_to_index_cache_files();
517 + $this->update_serve_cache_rules_htaccess_section(true);
518 + }
519 +
343 520 if (!$force_enable) {
344 - $already_ran_enable = true;
345 - return true;
521 + $already_ran_enable = $this->update_page_cache_enabled_state(true);
522 + return $already_ran_enable;
346 523 }
347 524
348 525 if (!$this->write_advanced_cache() && version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<')) {
349 526 $message = sprintf("The request to write the file %s failed. ", htmlspecialchars($this->get_advanced_cache_filename()));
@@ -349,11 +526,12 @@
349 526 $message = sprintf("The request to write the file %s failed. ", htmlspecialchars($this->get_advanced_cache_filename()));
350 527 $message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');
351 528
352 529 if (!defined('WP_CLI') || !WP_CLI) {
353 - $message .= "\n\n".sprintf(__('1. Please navigate, via FTP, to the folder - %s', 'wp-optimize'), htmlspecialchars(dirname($this->get_advanced_cache_filename())));
354 - $message .= "\n".__('2. Edit or create a file with the name advanced-cache.php', 'wp-optimize');
355 - $message .= "\n".__('3. Copy and paste the following lines into the file:', 'wp-optimize');
530 + // translators: %s is the path to advanced-cache.php folder
531 + $message .= "\n\n1. ".sprintf(__('Please navigate, via FTP, to the folder - %s', 'wp-optimize'), htmlspecialchars(dirname($this->get_advanced_cache_filename())));
532 + $message .= "\n2. ".__('Edit or create a file with the name advanced-cache.php', 'wp-optimize');
533 + $message .= "\n3. ".__('Copy and paste the following lines into the file:', 'wp-optimize');
356 534 }
357 535
358 536 $already_ran_enable = new WP_Error("write_advanced_cache", $message);
359 537 return $already_ran_enable;
@@ -369,19 +547,21 @@
369 547 $already_ran_enable = new WP_Error("verify_cache", "Could not verify if the cache was enabled: \n".implode("\n- ", $errors));
370 548 return $already_ran_enable;
371 549 }
372 550
373 - $already_ran_enable = true;
374 -
375 - return true;
551 + $already_ran_enable = $this->update_page_cache_enabled_state(true);
552 + return $already_ran_enable;
376 553 }
377 554
378 555 /**
379 556 * Disables page cache
380 557 *
381 - * @return bool - true on success, false otherwise
558 + * @param bool $keep_option_enabled - Whether to keep the option `enable_page_caching` enabled or not. This is useful when we want to disable the cache temporarily, e.g. when deactivating the plugin, but we want to keep the option enabled so that it can be re-enabled when the plugin is reactivated.
559 + * @return bool|WP_Error - true on success or WP_Error on failure.
382 560 */
383 - public function disable() {
561 + public function disable($keep_option_enabled = false) {
562 + global $is_apache;
563 +
384 564 $ret = true;
385 565
386 566 $advanced_cache_file = $this->get_advanced_cache_filename();
387 567
@@ -387,15 +567,8 @@
387 567
388 568 // N.B. The only use of WP_CACHE in WP core is to include('advanced-cache.php') (and run a function if it's then defined); so, if the decision to leave it enable is, for some unexpected reason, technically incorrect, it still can't cause a problem.
389 569 $disabled_wp_config = $this->write_wp_config(false);
390 570 if (!$disabled_wp_config) {
391 - $plugin_basename = basename(WPO_PLUGIN_MAIN_PATH);
392 - $action = "deactivate_".$plugin_basename."/wp-optimize.php";
393 - if (current_action() === $action) {
394 - $cache_config = WPO_Cache_Config::instance()->config;
395 - $cache_config['enable_page_caching'] = false;
396 - WPO_Cache_Config::instance()->update($cache_config, true);
397 - }
398 571 $this->log("Could not turn off the WP_CACHE constant in wp-config.php");
399 572 $this->add_warning('error_disabling', __('Could not turn off the WP_CACHE constant in wp-config.php', 'wp-optimize'));
400 573 }
401 574
@@ -402,9 +575,9 @@
402 575 $disabled_advanced_cache = true;
403 576 // First try to remove (so that it doesn't look to any other plugin like the file is already 'claimed')
404 577 // We only touch advanched-cache.php and wp-config.php if it appears that we were in control of advanced-cache.php
405 578 if (!file_exists($advanced_cache_file) || false !== strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) {
406 - if (file_exists($advanced_cache_file) && (!unlink($advanced_cache_file) && false === file_put_contents($advanced_cache_file, "<?php\n// WP-Optimize: page cache disabled"))) {
579 + if (file_exists($advanced_cache_file) && (!WP_Optimize_Utils::wp_delete_file($advanced_cache_file) && false === file_put_contents($advanced_cache_file, "<?php\n// WP-Optimize: page cache disabled"))) {
407 580 $disabled_advanced_cache = false;
408 581 $this->log("The request to the filesystem to remove or empty advanced-cache.php failed");
409 582 $this->add_warning('error_disabling', __('The request to the filesystem to remove or empty advanced-cache.php failed', 'wp-optimize'));
410 583 }
@@ -415,14 +588,55 @@
415 588 $ret = new WP_Error('error_disabling_cache', __('The page caching could not be disabled: the WP_CACHE constant could not be removed from wp-config.php and the request to the filesystem to remove or empty advanced-cache.php failed.', 'wp-optimize'));
416 589 }
417 590
418 591 // Delete cache to avoid stale cache on next activation
419 - $this->purge();
592 + $is_cache_purged = $this->purge();
593 + if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
420 594
595 + // Delete `.htaccess` rules to serve cached files
596 + if ($is_apache && apply_filters('wpo_serve_cache_via_htaccess', false)) {
597 + $this->update_serve_cache_rules_htaccess_section(false);
598 + }
599 +
600 + // Unschedule cron job to purge old cache
601 + wp_clear_scheduled_hook('wpo_purge_old_cache');
602 +
603 + if (!is_wp_error($ret)) {
604 + wp_clear_scheduled_hook('wpo_prune_cache_logs');
605 +
606 + if (!$keep_option_enabled) {
607 + $ret = $this->update_page_cache_enabled_state(false);
608 + }
609 + }
610 +
611 + if (!is_wp_error($ret)) {
612 + do_action('wpo_page_cache_disabled');
613 + }
614 +
421 615 return $ret;
422 616 }
423 617
618 + /**
619 + * Update page cache enabled state.
620 + * Only updates `enable_page_caching` if the option exists and the value has changed.
621 + *
622 + * @param bool $enabled Whether page caching should be marked as enabled.
623 + *
624 + * @return true|WP_Error true on success, WP_Error on failure.
625 + */
626 + private function update_page_cache_enabled_state($enabled) {
627 + $config = WPO_Cache_Config::instance();
628 + $cache_config = $config->get();
424 629
630 + if (isset($cache_config['enable_page_caching']) && $cache_config['enable_page_caching'] === $enabled) {
631 + return true;
632 + }
633 +
634 + $cache_config['enable_page_caching'] = $enabled;
635 +
636 + return $config->update($cache_config, true);
637 + }
638 +
425 639 /**
426 640 * Purges the cache
427 641 *
428 642 * @return bool - true on success, false otherwise
@@ -427,8 +641,19 @@
427 641 *
428 642 * @return bool - true on success, false otherwise
429 643 */
430 644 public function purge() {
645 + /**
646 + * Filters whether activating / deactivating a plugin will purge the cache.
647 + */
648 + $this->should_purge = apply_filters('wpo_purge_page_cache_on_activate_deactivate_plugin', true);
649 + $purge_actions = array(
650 + 'deactivate_' . plugin_basename(WPO_PLUGIN_MAIN_PATH . 'wp-optimize.php'),
651 + 'deactivate_plugin',
652 + 'activated_plugin',
653 + );
654 + $is_deactivation_action = in_array(current_action(), $purge_actions, true);
655 + if ($is_deactivation_action && !$this->should_purge) return false;
431 656
432 657 if (!self::delete(WPO_CACHE_FILES_DIR)) {
433 658 $this->log("The request to the filesystem to delete the cache failed");
434 659 return false;
@@ -443,19 +668,22 @@
443 668 }
444 669
445 670 /**
446 671 * Purge cache files older than cache life span
672 + *
673 + * @return array Log messages.
447 674 */
448 675 public function purge_old() {
676 + $log = array();
449 677 $page_cache_length = $this->config->get_option('page_cache_length');
678 + if (0 === $page_cache_length) return $log;
450 679 $expires = time() - $page_cache_length;
451 - $log = array();
452 680 $cache_folder = WPO_CACHE_FILES_DIR . '/' . str_ireplace(array('http://', 'https://'), '', get_site_url());
453 681 // get all directories that are a direct child of current directory
454 - if (is_dir($cache_folder) && is_writable($cache_folder)) {
682 + if (is_dir($cache_folder) && wp_is_writable($cache_folder)) {
455 683 if ($handle = opendir($cache_folder)) {
456 684 while (false !== ($d = readdir($handle))) {
457 - if (0 == strcmp($d, '.') || 0 == strcmp($d, '..')) {
685 + if (0 === strcmp($d, '.') || 0 === strcmp($d, '..')) {
458 686 continue;
459 687 }
460 688
461 689 if ($this->is_front_page_cache($d)) {
@@ -460,9 +688,9 @@
460 688
461 689 if ($this->is_front_page_cache($d)) {
462 690 $modified_time = (int) filemtime("$cache_folder/$d");
463 691 if ($modified_time <= $expires) {
464 - unlink("$cache_folder/$d");
692 + wp_delete_file("$cache_folder/$d");
465 693 }
466 694 continue;
467 695 }
468 696
@@ -474,9 +702,9 @@
474 702 $log[] = "checking if cache has expired - $d";
475 703 if ($modified_time <= $expires) {
476 704 $log[] = "deleting cache in $dir";
477 705 wpo_delete_files($dir, true);
478 - if (file_exists($dir)) rmdir($dir);
706 + if (file_exists($dir)) rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- N/A
479 707 }
480 708 }
481 709 closedir($handle);
482 710 }
@@ -510,29 +738,39 @@
510 738 return true;
511 739 }
512 740
513 741 /**
514 - * Check if cache is enabled and working
742 + * Checks whether page caching is turned on in settings.
515 743 *
516 - * @return bool - true on success, false otherwise
744 + * @return bool true if the `enable_page_caching` option is set & environment is ready to cache, false otherwise.
517 745 */
518 746 public function is_enabled() {
747 + return $this->config->get_option('enable_page_caching') && $this->is_environment_functional();
748 + }
519 749
520 - if (!$this->config->get_option('enable_page_caching')) return false;
750 + /**
751 + * Checks whether the cache delivery chain is actually intact and able to serve cached pages.
752 + *
753 + * Verifies the WP_CACHE and WPO_ADVANCED_CACHE constants are defined and true, and that the cache config file exists on disk.
754 + *
755 + * @return bool True if the delivery chain is functional, false otherwise.
756 + */
757 + private function is_environment_functional() {
758 + $is_cli_or_ajax = (defined('WP_CLI') && WP_CLI) || (defined('DOING_AJAX') && DOING_AJAX);
521 759
522 - if (!defined('WP_CACHE') || !WP_CACHE) {
523 - $this->log("WP_CACHE constant is not present in wp-config.php");
760 + if (!$is_cli_or_ajax && (!defined('WP_CACHE') || !WP_CACHE)) {
761 + $this->file_log("WP_CACHE constant is not present in wp-config.php");
524 762 return false;
525 763 }
526 764
527 - if (!defined('WPO_ADVANCED_CACHE') || !WPO_ADVANCED_CACHE) {
528 - $this->log("WPO_ADVANCED_CACHE constant is not present in advanced-cache.php");
765 + if (!$is_cli_or_ajax && (!defined('WPO_ADVANCED_CACHE') || !WPO_ADVANCED_CACHE)) {
766 + $this->file_log("WPO_ADVANCED_CACHE constant is not present in advanced-cache.php");
529 767 return false;
530 768 }
531 769
532 770 $config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
533 - if (!file_exists($config_file)) {
534 - $this->log("$config_file is not present");
771 + if (!$is_cli_or_ajax && !file_exists($config_file)) {
772 + $this->file_log("Cache config file $config_file is not present");
535 773 return false;
536 774 }
537 775
538 776 return true;
@@ -540,23 +778,27 @@
540 778
541 779 /**
542 780 * Create the folder structure needed for cache to work
543 781 *
544 - * @return bool - true on success, false otherwise
782 + * @return bool | WP_Error - true on success, false otherwise
545 783 */
546 - private function create_folders() {
784 + public function create_folders() {
785 + $permissions_str = __('Please check your file permissions.', 'wp-optimize');
547 786
548 787 if (!is_dir(WPO_CACHE_DIR) && !wp_mkdir_p(WPO_CACHE_DIR)) {
549 - return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s. Please check your file permissions.'), str_ireplace(ABSPATH, '', WPO_CACHE_DIR)));
788 + // translators: %s is a directory name
789 + return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s.', 'wp-optimize'), str_ireplace(ABSPATH, '', WPO_CACHE_DIR)) .' '. $permissions_str);
550 790 }
551 791
552 792 if (!is_dir(WPO_CACHE_CONFIG_DIR) && !wp_mkdir_p(WPO_CACHE_CONFIG_DIR)) {
553 - return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s. Please check your file permissions.'), str_ireplace(ABSPATH, '', WPO_CACHE_CONFIG_DIR)));
793 + // translators: %s is a directory name
794 + return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s.', 'wp-optimize'), str_ireplace(ABSPATH, '', WPO_CACHE_CONFIG_DIR)) .' '. $permissions_str);
554 795 }
555 796
556 797 if (!is_dir(WPO_CACHE_FILES_DIR)) {
557 798 if (!wp_mkdir_p(WPO_CACHE_FILES_DIR)) {
558 - return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s. Please check your file permissions.'), str_ireplace(ABSPATH, '', WPO_CACHE_FILES_DIR)));
799 + // translators: %s is a directory name
800 + return new WP_Error('create_folders', sprintf(__('The request to the filesystem failed: unable to create directory %s.', 'wp-optimize'), str_ireplace(ABSPATH, '', WPO_CACHE_FILES_DIR)) .' '. $permissions_str);
559 801 } else {
560 802 wpo_disable_cache_directories_viewing();
561 803 }
562 804 }
@@ -586,9 +828,10 @@
586 828 $cache_path = '/wpo-cache';
587 829 $cache_files_path = '/cache/wpo-cache';
588 830 $cache_extensions_path = WPO_CACHE_EXT_DIR;
589 831 $wpo_version = WPO_VERSION;
590 - $wpo_home_url = trailingslashit(home_url());
832 + $wpo_home_url = trailingslashit(network_home_url());
833 + $abspath = ABSPATH;
591 834
592 835 // CS does not like heredoc
593 836 // phpcs:disable
594 837 $this->advanced_cache_file_content = <<<EOF
@@ -595,9 +838,9 @@
595 838 <?php
596 839
597 840 if (!defined('ABSPATH')) die('No direct access allowed');
598 841
599 -// WP-Optimize advanced-cache.php (written by version: $wpo_version) (do not change this line, it is used for correctness checks)
842 +// WP-Optimize advanced-cache.php (written by version: $wpo_version) (homeurl: $wpo_home_url) (abspath: $abspath) (do not change this line, it is used for correctness checks)
600 843
601 844 if (!defined('WPO_ADVANCED_CACHE')) define('WPO_ADVANCED_CACHE', true);
602 845
603 846 \$possible_plugin_locations = array(
@@ -646,10 +889,15 @@
646 889 if (empty(\$GLOBALS['wpo_cache_config'])) {
647 890 include_once(WPO_CACHE_CONFIG_DIR . '/$config_file_basename');
648 891 }
649 892
650 -if (empty(\$GLOBALS['wpo_cache_config']) || empty(\$GLOBALS['wpo_cache_config']['enable_page_caching'])) { return; }
893 +if (empty(\$GLOBALS['wpo_cache_config'])) {
894 + error_log('WP-Optimize: Caching failed because the configuration data could not be loaded from the config file.');
895 + return;
896 +}
651 897
898 +if (empty(\$GLOBALS['wpo_cache_config']['enable_page_caching'])) { return; }
899 +
652 900 if (false !== \$plugin_location) { include_once(\$plugin_location.'/file-based-page-cache.php'); }
653 901
654 902 EOF;
655 903
@@ -663,15 +911,15 @@
663 911 }
664 912
665 913 // check if we can't write the advanced cache file
666 914 // case 1: the directory is read-only and the file doesn't exist
667 - if (!is_file($advanced_cache_filename) && !is_writable(dirname($advanced_cache_filename))) {
915 + if (!is_file($advanced_cache_filename) && !wp_is_writable(dirname($advanced_cache_filename))) {
668 916 $this->advanced_cache_file_writing_error = true;
669 917 return false;
670 918 }
671 919
672 920 // case 2: the file already exists but it's read-only
673 - if (is_file($advanced_cache_filename) && !is_writable($advanced_cache_filename)) {
921 + if (is_file($advanced_cache_filename) && !wp_is_writable($advanced_cache_filename)) {
674 922 if (version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<') || $update_required) {
675 923 $this->advanced_cache_file_writing_error = true;
676 924 return false;
677 925 } else {
@@ -702,11 +950,20 @@
702 950 }
703 951 }
704 952
705 953 // from 3.0.17 we use more secure way to store cache config files and need update advanced-cache.php
954 + // if the site url or folder changed we also update advanced-cache.php
706 955 $advanced_cache_current_version = $this->get_advanced_cache_version();
707 - if ($advanced_cache_current_version && version_compare($advanced_cache_current_version, $this->_minimum_advanced_cache_file_version, '>=')) return;
956 + $is_the_site_url_or_folder_changed = $this->is_the_site_url_or_folder_changed();
708 957
958 + if ($advanced_cache_current_version && version_compare($advanced_cache_current_version, $this->_minimum_advanced_cache_file_version, '>=') && !$is_the_site_url_or_folder_changed) return;
959 +
960 + // Purge the cache when the site url or folder changed.
961 + if ($is_the_site_url_or_folder_changed) {
962 + $is_cache_purged = $this->purge();
963 + if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
964 + }
965 +
709 966 if (!$this->write_advanced_cache()) {
710 967 add_action('admin_notices', array($this, 'notice_advanced_cache_autoupdate_error'));
711 968 } else {
712 969 $this->update_cache_config();
@@ -713,8 +970,31 @@
713 970 }
714 971 }
715 972
716 973 /**
974 + * Check if the site url or folder doesn't equal to values from advanced-cache.php
975 + *
976 + * @return bool
977 + */
978 + private function is_the_site_url_or_folder_changed() {
979 + if (!is_file($this->get_advanced_cache_filename())) return false;
980 +
981 + $content = file_get_contents($this->get_advanced_cache_filename());
982 +
983 + if (false === $content) return false;
984 +
985 + if (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\) (\(homeurl: (.+)\)) (\(abspath: (.+)\)) \(do not change/Ui', $content, $match)) {
986 + $wpo_home_url = trailingslashit(network_home_url());
987 + $abspath = ABSPATH;
988 + return ($wpo_home_url !== $match[3] || $abspath !== $match[5]);
989 + } elseif (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\)/Ui', $content, $match)) {
990 + return true;
991 + }
992 +
993 + return false;
994 + }
995 +
996 + /**
717 997 * Show notification when advanced-cache.php could not be updated.
718 998 */
719 999 public function notice_advanced_cache_autoupdate_error() {
720 1000 $this->show_notice(__('The file advanced-cache.php needs to be updated, but the automatic process failed.', 'wp-optimize').
@@ -759,8 +1039,13 @@
759 1039 if (!$config_path) {
760 1040 return false;
761 1041 }
762 1042
1043 + // Bail early if wp-config.php is not writable, to avoid a PHP warning.
1044 + if (!wp_is_writable($config_path)) {
1045 + return false;
1046 + }
1047 +
763 1048 $config_file_string = file_get_contents($config_path);
764 1049
765 1050 // Config file is empty. Maybe couldn't read it?
766 1051 if (empty($config_file_string)) {
@@ -813,9 +1098,9 @@
813 1098 }
814 1099 $errors = 0;
815 1100
816 1101 // First check wp-config.php.
817 - if (!$this->_get_wp_config() && !is_writable($this->_get_wp_config())) {
1102 + if (!$this->_get_wp_config() && !wp_is_writable($this->_get_wp_config())) {
818 1103 $this->log("Unable to write to or find wp-config.php; please check file/folder permissions");
819 1104 $this->add_warning('verify_cache', __("Unable to write to or find wp-config.php; please check file/folder permissions.", 'wp-optimize'));
820 1105 }
821 1106
@@ -821,9 +1106,9 @@
821 1106
822 1107 $advanced_cache_file = untrailingslashit(WP_CONTENT_DIR).'/advanced-cache.php';
823 1108
824 1109 // Now check wp-content. We need to be able to create files of the same user as this file.
825 - if ((!file_exists($advanced_cache_file) || false === strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) && !is_writable($advanced_cache_file) && !is_writable(untrailingslashit(WP_CONTENT_DIR))) {
1110 + if ((!file_exists($advanced_cache_file) || false === strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) && !wp_is_writable($advanced_cache_file) && !wp_is_writable(untrailingslashit(WP_CONTENT_DIR))) {
826 1111 $this->log("Unable to write the file advanced-cache.php inside the wp-content folder; please check file/folder permissions");
827 1112 $this->add_error('verify_cache', __("Unable to write the file advanced-cache.php inside the wp-content folder; please check file/folder permissions", 'wp-optimize'));
828 1113 $errors++;
829 1114 }
@@ -828,16 +1113,17 @@
828 1113 $errors++;
829 1114 }
830 1115
831 1116 if (file_exists(WPO_CACHE_FILES_DIR)) {
832 - if (!is_writable(WPO_CACHE_FILES_DIR)) {
1117 + if (!wp_is_writable(WPO_CACHE_FILES_DIR)) {
833 1118 $this->log("Unable to write inside the cache files folder; please check file/folder permissions");
1119 + // translators: %s is cache folder path
834 1120 $this->add_warning('verify_cache', sprintf(__("Unable to write inside the cache files folder (%s); please check file/folder permissions (no cache files will be able to be created otherwise)", 'wp-optimize'), WPO_CACHE_FILES_DIR));
835 1121 }
836 1122 }
837 1123
838 1124 if (file_exists(WPO_CACHE_CONFIG_DIR)) {
839 - if (!is_writable(WPO_CACHE_CONFIG_DIR)) {
1125 + if (!wp_is_writable(WPO_CACHE_CONFIG_DIR)) {
840 1126 $this->log("Unable to write inside the cache configuration folder; please check file/folder permissions");
841 1127 // If the config exists, only send a warning. Otherwise send an error.
842 1128 $type = 'warning';
843 1129 if (!file_exists(WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename())) {
@@ -843,8 +1129,9 @@
843 1129 if (!file_exists(WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename())) {
844 1130 $type = 'error';
845 1131 $errors++;
846 1132 }
1133 + // translators: %s is cache config folder path
847 1134 $this->add_error('verify_cache', sprintf(__("Unable to write inside the cache configuration folder (%s); please check file/folder permissions", 'wp-optimize'), WPO_CACHE_CONFIG_DIR), $type);
848 1135 }
849 1136 }
850 1137
@@ -851,9 +1138,9 @@
851 1138 return !$errors;
852 1139 }
853 1140
854 1141 /**
855 - * Update permalink strucutre in cache config
1142 + * Update permalink structure in cache config
856 1143 *
857 1144 * @param string $old_value Old value of permalink_structure option
858 1145 * @param string $value New value of permalink_structure option
859 1146 * @param string $option Option name `permalink_structure`
@@ -859,9 +1146,9 @@
859 1146 * @param string $option Option name `permalink_structure`
860 1147 */
861 1148 public function update_option_permalink_structure($old_value, $value, $option) {
862 1149 $current_config = $this->config->get();
863 - if ($old_value != $value) {
1150 + if ($old_value !== $value) {
864 1151 $current_config[$option] = $value;
865 1152 $this->config->update($current_config, true);
866 1153 }
867 1154 }
@@ -867,14 +1154,16 @@
867 1154 }
868 1155
869 1156 /**
870 1157 * Update cache config. Used to support 3d party plugins.
1158 + *
1159 + * @return bool | WP_Error
871 1160 */
872 1161 public function update_cache_config() {
873 1162 // get current cache settings.
874 1163 $current_config = $this->config->get();
875 1164 // and call update to change if need cookies and query variable names.
876 - $this->config->update($current_config, true);
1165 + return $this->config->update($current_config);
877 1166 }
878 1167
879 1168 /**
880 1169 * Delete information about cache size.
@@ -879,18 +1168,29 @@
879 1168 /**
880 1169 * Delete information about cache size.
881 1170 */
882 1171 public function delete_cache_size_information() {
1172 + //not removing delete_transient so it clear this key from option table
883 1173 delete_transient('wpo_get_cache_size');
1174 + delete_site_transient('wpo_get_cache_size');
884 1175 }
885 1176
886 1177 /**
1178 + * Update cache cleared time
1179 + */
1180 + public function update_cache_cleared_time(): void {
1181 + $config = $this->config->get();
1182 + $config['last_cleared'] = time();
1183 + $this->config->update($config);
1184 + }
1185 +
1186 + /**
887 1187 * Get current cache size.
888 1188 *
889 1189 * @return array
890 1190 */
891 1191 public function get_cache_size() {
892 - $cache_size = get_transient('wpo_get_cache_size');
1192 + $cache_size = get_site_transient('wpo_get_cache_size');
893 1193
894 1194 if (!empty($cache_size)) return $cache_size;
895 1195
896 1196 $infos = $this->get_dir_infos(WPO_CACHE_FILES_DIR);
@@ -898,51 +1198,26 @@
898 1198 'size' => $infos['size'],
899 1199 'file_count' => $infos['file_count']
900 1200 );
901 1201
902 - set_transient('wpo_get_cache_size', $cache_size);
1202 + $expiration = apply_filters('wpo_cache_size_expiration', DAY_IN_SECONDS);
1203 + $expiration = is_numeric($expiration) ? (int) $expiration : DAY_IN_SECONDS;
1204 + set_site_transient('wpo_get_cache_size', $cache_size, $expiration);
903 1205
904 1206 return $cache_size;
905 1207 }
906 1208
907 1209 /**
908 - * Fetch directory informations.
1210 + * Fetch directory information.
909 1211 *
910 1212 * @param string $dir
911 1213 * @return array
912 1214 */
913 1215 private function get_dir_infos($dir) {
914 - $dir_size = 0;
915 - $file_count = 0;
1216 + $filtered = apply_filters('wpo_cache_size_files_to_ignore', $this->files_to_ignore);
1217 + $this->files_to_ignore = is_array($filtered) ? $filtered : array();
916 1218
917 - $handle = is_dir($dir) ? opendir($dir) : false;
918 -
919 - if (false === $handle) {
920 - return array('size' => 0, 'file_count' => 0);
921 - }
922 -
923 - $file = readdir($handle);
924 -
925 - while (false !== $file) {
926 -
927 - if ('.' != $file && '..' != $file) {
928 - $current_file = $dir.'/'.$file;
929 -
930 - if (is_dir($current_file)) {
931 - $sub_dir_infos = $this->get_dir_infos($current_file);
932 - $dir_size += $sub_dir_infos['size'];
933 - $file_count += $sub_dir_infos['file_count'];
934 - } elseif (is_file($current_file)) {
935 - $dir_size += filesize($current_file);
936 - $file_count++;
937 - }
938 - }
939 -
940 - $file = readdir($handle);
941 -
942 - }
943 -
944 - return array('size' => $dir_size, 'file_count' => $file_count);
1219 + return WP_Optimize_Utils::get_folder_stats($dir, $this->files_to_ignore);
945 1220 }
946 1221
947 1222 /**
948 1223 * Returns the path to wp-config
@@ -974,11 +1249,9 @@
974 1249 * @param string $src
975 1250 * @return boolean
976 1251 */
977 1252 public static function delete($src) {
978 -
979 1253 return wpo_delete_files($src);
980 -
981 1254 }
982 1255
983 1256 /**
984 1257 * Delete cached files for specific url.
@@ -988,18 +1261,49 @@
988 1261 *
989 1262 * @return bool
990 1263 */
991 1264 public static function delete_cache_by_url($url, $recursive = false) {
992 - if (!defined('WPO_CACHE_FILES_DIR') || '' == $url) return;
1265 + if (!defined('WPO_CACHE_FILES_DIR') || '' === $url) return false;
993 1266
994 1267 $path = self::get_full_path_from_url($url);
995 1268
996 1269 do_action('wpo_delete_cache_by_url', $url, $recursive);
997 1270
1271 + do_action('wpo_preload_url', $url);
1272 +
998 1273 return wpo_delete_files($path, $recursive);
999 1274 }
1000 1275
1001 1276 /**
1277 + * Adds the URL to the preload list and hooks the preload task to shutdown
1278 + *
1279 + * @param string $url
1280 + * @return void
1281 + */
1282 + public function maybe_preload_url($url) {
1283 + if (!$this->should_auto_preload_purged_contents()) return;
1284 +
1285 + static $urls_preloaded = array();
1286 +
1287 + // Remove the query string from the URL
1288 + $url = preg_replace('/\?.*/', '', $url);
1289 +
1290 + if (!in_array($url, $urls_preloaded)) {
1291 + $urls_preloaded[] = $url;
1292 +
1293 + $preloader = WP_Optimize_Page_Cache_Preloader::instance();
1294 + $preloader->add_url_to_preload_list($url);
1295 + $url_task_creator = array($preloader, 'create_tasks_for_auto_preload_urls');
1296 +
1297 + $preloader->maybe_dont_close_browser_connection();
1298 +
1299 + if (!has_action('shutdown', $url_task_creator)) {
1300 + add_action('shutdown', $url_task_creator);
1301 + }
1302 + }
1303 + }
1304 +
1305 + /**
1002 1306 * Delete cached files for single post.
1003 1307 *
1004 1308 * @param integer $post_id The post ID
1005 1309 *
@@ -1005,11 +1309,26 @@
1005 1309 *
1006 1310 * @return bool
1007 1311 */
1008 1312 public static function delete_single_post_cache($post_id) {
1009 -
1010 - if (!defined('WPO_CACHE_FILES_DIR')) return;
1313 + $is_cache_deleted = self::really_delete_single_post_cache($post_id);
1011 1314
1315 + do_action('wpo_single_post_cache_deleted', $post_id);
1316 +
1317 + return $is_cache_deleted;
1318 + }
1319 +
1320 + /**
1321 + * Really delete cached files for single post.
1322 + *
1323 + * @param integer $post_id The post ID
1324 + *
1325 + * @return bool
1326 + */
1327 + public static function really_delete_single_post_cache($post_id) {
1328 +
1329 + if (!defined('WPO_CACHE_FILES_DIR')) return false;
1330 +
1012 1331 $post_url = get_permalink($post_id);
1013 1332
1014 1333 $path = self::get_full_path_from_url($post_url);
1015 1334
@@ -1018,8 +1337,10 @@
1018 1337 $recursive = preg_match('/\<\!--nextpage--\>/', $post->post_content) ? true : false;
1019 1338
1020 1339 do_action('wpo_delete_cache_by_url', $post_url, $recursive);
1021 1340
1341 + do_action('wpo_preload_url', $post_url);
1342 +
1022 1343 return wpo_delete_files($path, $recursive);
1023 1344 }
1024 1345
1025 1346 /**
@@ -1032,42 +1353,15 @@
1032 1353 $homepage_url = get_home_url(get_current_blog_id());
1033 1354
1034 1355 $path = self::get_full_path_from_url($homepage_url);
1035 1356
1036 - do_action('wpo_delete_cache_by_url', $homepage_url, false);
1357 + $recursive = false;
1037 1358
1038 - wpo_delete_files($path, false);
1039 - }
1359 + do_action('wpo_delete_cache_by_url', $homepage_url, $recursive);
1040 1360
1041 - /**
1042 - * Delete sitemap cahche.
1043 - */
1044 - public static function delete_sitemap_cache() {
1045 - if (!defined('WPO_CACHE_FILES_DIR')) return;
1361 + do_action('wpo_preload_url', $homepage_url);
1046 1362
1047 - $homepage_url = get_home_url(get_current_blog_id());
1048 -
1049 - $path = trailingslashit(WPO_CACHE_FILES_DIR) . trailingslashit(wpo_get_url_path($homepage_url));
1050 -
1051 - if (!is_dir($path)) return;
1052 -
1053 - $handle = opendir($path);
1054 -
1055 - if (false !== $handle) {
1056 - $file = readdir($handle);
1057 -
1058 - while (false !== $file) {
1059 -
1060 - if ('.' != $file && '..' != $file && is_dir($path . $file) && preg_match('/.*sitemap.*\.xml/i', $file)) {
1061 - do_action('wpo_delete_cache_by_url', $path . $file, false);
1062 - wpo_delete_files($path . $file, true);
1063 - }
1064 -
1065 - $file = readdir($handle);
1066 - }
1067 - }
1068 -
1069 - closedir($handle);
1363 + wpo_delete_files($path, $recursive);
1070 1364 }
1071 1365
1072 1366 /**
1073 1367 * Delete feed from cache.
@@ -1085,10 +1379,23 @@
1085 1379 }
1086 1380
1087 1381 /**
1088 1382 * Delete post feed from cache.
1383 + *
1384 + * @param int $post_id Post ID
1089 1385 */
1090 1386 public static function delete_post_feed_cache($post_id) {
1387 + self::really_delete_post_feed_cache($post_id);
1388 +
1389 + do_action('wpo_single_post_feed_cache_deleted', $post_id);
1390 + }
1391 +
1392 + /**
1393 + * Really delete post feed from cache.
1394 + *
1395 + * @param int $post_id Post ID
1396 + */
1397 + public static function really_delete_post_feed_cache($post_id) {
1091 1398 if (!defined('WPO_CACHE_FILES_DIR')) return;
1092 1399
1093 1400 $post_url = get_permalink($post_id);
1094 1401
@@ -1115,9 +1422,9 @@
1115 1422
1116 1423 // delete empty comments dir from the cache
1117 1424 $comments_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/';
1118 1425 $path = self::get_full_path_from_url($comments_url);
1119 -
1426 +
1120 1427 if (wpo_is_empty_dir($path)) {
1121 1428 wpo_delete_files($path, true);
1122 1429 }
1123 1430 }
@@ -1127,9 +1434,9 @@
1127 1434 *
1128 1435 * @param string $url
1129 1436 * @return string
1130 1437 */
1131 - private static function get_full_path_from_url($url) {
1438 + public static function get_full_path_from_url($url) {
1132 1439 return trailingslashit(WPO_CACHE_FILES_DIR) . trailingslashit(wpo_get_url_path($url));
1133 1440 }
1134 1441
1135 1442 /**
@@ -1138,9 +1445,9 @@
1138 1445 * @return void
1139 1446 */
1140 1447 public function admin_init() {
1141 1448 // Maybe update the advanced cache.
1142 - if ((!defined('DOING_AJAX') || !DOING_AJAX) && current_user_can('update_plugins')) {
1449 + if ((!defined('DOING_AJAX') || !DOING_AJAX) && WP_Optimize()->current_user_can('update_plugins')) {
1143 1450 $this->maybe_update_advanced_cache();
1144 1451 $this->cron_activate();
1145 1452 }
1146 1453 }
@@ -1159,9 +1466,9 @@
1159 1466
1160 1467 /**
1161 1468 * Returns an instance of the current class, creates one if it doesn't exist
1162 1469 *
1163 - * @return object
1470 + * @return self
1164 1471 */
1165 1472 public static function instance() {
1166 1473 if (empty(self::$instance)) {
1167 1474 self::$instance = new self();
@@ -1168,9 +1475,64 @@
1168 1475 }
1169 1476 return self::$instance;
1170 1477 }
1171 1478
1479 +
1172 1480 /**
1481 + * Update gmt_offset and timezone_string cache config value, used with hook `update_gmt_offset_timezone_string_config`.
1482 + *
1483 + * @param float | string $old_value GMT offset as a float value or timezone value supported by PHP as a string (https://www.php.net/manual/en/timezones.php)
1484 + * @param float | string $new_value
1485 + * @param string $option gmt_offset|timezone_string
1486 + *
1487 + * @return null
1488 + */
1489 + public function update_gmt_offset_timezone_string_config($old_value, $new_value, $option) {
1490 + if ('' === $new_value) return;
1491 +
1492 + $current_config = $this->config->get();
1493 + if ('timezone_string' === $option) {
1494 + $timeZone = new DateTimeZone($new_value);
1495 + $dateTime = new DateTime("now");
1496 + $gmt_offset = $timeZone->getOffset($dateTime);
1497 + $current_config['gmt_offset'] = round($gmt_offset/3600, 1);
1498 + $current_config['timezone_string'] = $new_value;
1499 + } elseif ('' !== $new_value) {
1500 + $current_config['gmt_offset'] = $new_value;
1501 + $current_config['timezone_string'] = '';
1502 + }
1503 + $this->config->update($current_config, true);
1504 + }
1505 +
1506 + /**
1507 + * Update `date_format` cache config value, used with hook `update_option_date_format`.
1508 + *
1509 + * @param string $old_value Old date format
1510 + * @param string $new_value New date format
1511 + *
1512 + * @return void
1513 + */
1514 + public function update_option_date_format($old_value, $new_value) {
1515 + $current_config = $this->config->get();
1516 + $current_config['date_format'] = $new_value;
1517 + $this->config->update($current_config, true);
1518 + }
1519 +
1520 + /**
1521 + * Update `time_format` cache config value, used with hook `update_option_time_format`.
1522 + *
1523 + * @param string $old_value Old time format
1524 + * @param string $new_value New time format
1525 + *
1526 + * @return void
1527 + */
1528 + public function update_option_time_format($old_value, $new_value) {
1529 + $current_config = $this->config->get();
1530 + $current_config['time_format'] = $new_value;
1531 + $this->config->update($current_config, true);
1532 + }
1533 +
1534 + /**
1173 1535 * Adds an error to the error store
1174 1536 *
1175 1537 * @param string $code - The error code
1176 1538 * @param string $message - The error's message
@@ -1249,9 +1611,9 @@
1249 1611 public function is_pagespeedninja_gzip_active() {
1250 1612 if (!class_exists('PagespeedNinja')) return false;
1251 1613
1252 1614 $options = get_option('pagespeedninja_config');
1253 - $gzip = !empty($options) ? (bool) $options['psi_EnableGzipCompression'] && (bool) $options['html_gzip'] : false;
1615 + $gzip = isset($options['psi_EnableGzipCompression']) && isset($options['html_gzip']) && (bool) $options['psi_EnableGzipCompression'] && (bool) $options['html_gzip'];
1254 1616
1255 1617 return $gzip;
1256 1618 }
1257 1619
@@ -1258,9 +1620,9 @@
1258 1620 /**
1259 1621 * Output PageSpeed Ninja Gzip notice.
1260 1622 */
1261 1623 public function show_pagespeedninja_gzip_notice() {
1262 - echo '<div id="wp-optimize-pagespeedninja-gzip-notice" class="error wpo-notice"><p><b>'.__('WP-Optimize:', 'wp-optimize').'</b> '.__('Please disable the feature "Gzip compression" in PageSpeed Ninja to prevent conflicts.', 'wp-optimize').'</p></div>';
1624 + echo '<div id="wp-optimize-pagespeedninja-gzip-notice" class="error wpo-notice"><p><b>'.esc_html__('WP-Optimize:', 'wp-optimize').'</b> '.esc_html__('Please disable the feature "Gzip compression" in PageSpeed Ninja to prevent conflicts.', 'wp-optimize').'</p></div>';
1263 1625 }
1264 1626
1265 1627 /**
1266 1628 * Check if Far Future Expiration is active and GZIP compression option is enabled.
@@ -1270,9 +1632,9 @@
1270 1632 public function is_farfutureexpiration_gzip_active() {
1271 1633 if (!class_exists('farFutureExpiration')) return false;
1272 1634
1273 1635 $options = get_option('far_future_expiration_settings');
1274 - $gzip = !empty($options) ? (bool) $options['enable_gzip'] : false;
1636 + $gzip = !empty($options) && $options['enable_gzip'];
1275 1637
1276 1638 return $gzip;
1277 1639 }
1278 1640
@@ -1279,9 +1641,9 @@
1279 1641 /**
1280 1642 * Output Far Future Expiration Gzip notice.
1281 1643 */
1282 1644 public function show_farfutureexpiration_gzip_notice() {
1283 - echo '<div id="wp-optimize-pagespeedninja-gzip-notice" class="error wpo-notice"><p><b>'.__('WP-Optimize:', 'wp-optimize').'</b> '.__('Please disable the feature "Gzip compression" in Far Future Expiration to prevent conflicts.', 'wp-optimize').'</p></div>';
1645 + echo '<div id="wp-optimize-pagespeedninja-gzip-notice" class="error wpo-notice"><p><b>'.esc_html__('WP-Optimize:', 'wp-optimize').'</b> '.esc_html__('Please disable the feature "Gzip compression" in Far Future Expiration to prevent conflicts.', 'wp-optimize').'</p></div>';
1284 1646 }
1285 1647
1286 1648 /**
1287 1649 * This is a notice to show users that writing `advanced-cache.php` failed
@@ -1286,8 +1648,9 @@
1286 1648 /**
1287 1649 * This is a notice to show users that writing `advanced-cache.php` failed
1288 1650 */
1289 1651 public function show_admin_notice_advanced_cache() {
1652 + // translators: %s is advanced-cache.php file path
1290 1653 $message = sprintf(__('The request to write the file %s failed.', 'wp-optimize'), htmlspecialchars($this->get_advanced_cache_filename()));
1291 1654 $message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');
1292 1655 WP_Optimize()->include_template('notices/cache-notice.php', false, array('message' => $message));
1293 1656 }
@@ -1299,9 +1662,30 @@
1299 1662 * @return array An array of schedules being returned.
1300 1663 */
1301 1664 public function cron_schedules($schedules) {
1302 1665 $page_cache_length = $this->config->get_option('page_cache_length');
1666 + // When the interval is less than `WP_CRON_LOCK_TIMEOUT` error messages are triggered
1667 + // So, instead of `0` (now) we are making it equal to the constant value.
1668 + // https://github.com/WordPress/WordPress/commit/84d19848666a81584e0007a6ab7f7ad3c990d71a
1669 + if (defined('WP_CRON_LOCK_TIMEOUT') && $page_cache_length < WP_CRON_LOCK_TIMEOUT) {
1670 + $page_cache_length = WP_CRON_LOCK_TIMEOUT;
1671 + }
1672 +
1303 1673 $schedules['wpo_purge_old_cache'] = array('interval' => false === $page_cache_length ? 86400 : $page_cache_length, 'display' => __('Every time after the cache has expired', 'wp-optimize'));
1674 +
1675 + $interval = WP_Optimize_Page_Cache_Preloader::instance()->get_continue_preload_cron_interval();
1676 + $schedules['wpo_page_cache_preload_continue_interval'] = array(
1677 + 'interval' => $interval,
1678 + // translators: %d is the number of minutes
1679 + 'display' => sprintf(__('%d minutes', 'wp-optimize'), round($interval / 60, 1))
1680 + );
1681 +
1682 + $schedules['wpo_use_cache_lifespan'] = array(
1683 + 'interval' => $page_cache_length,
1684 + // translators: %s is the cache lifespan
1685 + 'display' => sprintf(__('Same as cache lifespan: %s', 'wp-optimize'), WPO_Cache_Config::instance()->get_option('page_cache_length_value').' '.WPO_Cache_Config::instance()->get_option('page_cache_length_unit'))
1686 + );
1687 +
1304 1688 return $schedules;
1305 1689 }
1306 1690
1307 1691 /**
@@ -1312,8 +1696,174 @@
1312 1696 $cache_settings = WPO_Cache_Config::instance()->get();
1313 1697 $cache_settings['use_webp_images'] = WP_Optimize()->get_options()->get_option('webp_conversion');
1314 1698 WPO_Cache_Config::instance()->update($cache_settings);
1315 1699 }
1700 + }
1701 +
1702 + /**
1703 + * Maybe regenerate cache config file, in case of migrations
1704 + */
1705 + public function maybe_regenerate_cache_config_file() {
1706 + $config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
1707 + if (!file_exists($config_file)) {
1708 + $config = $this->config->get();
1709 + $this->config->write($config);
1710 + }
1711 + }
1712 +
1713 + /**
1714 + * Checks if the given cache directory is empty
1715 + *
1716 + * @param string $path The path to the cache directory.
1717 + * @return bool Returns true if the cache directory is empty, false otherwise.
1718 + */
1719 + public static function is_cache_empty($path) {
1720 + if (!is_dir($path)) return true;
1721 +
1722 + if ($handle = opendir($path)) {
1723 + while (false !== ($entry = readdir($handle))) {
1724 + if ("." === $entry || ".." === $entry) {
1725 + continue;
1726 + }
1727 +
1728 + $full_path = $path . '/' . $entry;
1729 + if (is_file($full_path)) {
1730 + closedir($handle);
1731 + return false;
1732 + }
1733 + }
1734 + closedir($handle);
1735 + return true;
1736 + }
1737 + return true;
1738 + }
1739 +
1740 + /**
1741 + * Remove query params from request url
1742 + *
1743 + * @param string $url The request url.
1744 + * @return string Returns url without query params.
1745 + */
1746 + private static function remove_query_params($url) {
1747 + $parsed_url = wp_parse_url($url);
1748 + if (!$parsed_url) return '';
1749 + return $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'];
1750 + }
1751 +
1752 + /**
1753 + * Check configuration for auto preloading after cache deletion
1754 + *
1755 + * @return bool
1756 + */
1757 + public function should_auto_preload_purged_contents() {
1758 +
1759 + if (!$this->is_enabled()) return false; // as Page Cache is not enabled we should not auto preload purged content.
1760 +
1761 + $wpo_cache = WP_Optimize()->get_page_cache();
1762 + $wpo_cache_options = $wpo_cache->config->get();
1763 +
1764 + return $wpo_cache_options['auto_preload_purged_contents'];
1765 + }
1766 +
1767 + /**
1768 + * Logging of interesting messages related to Cache Purging
1769 + *
1770 + * @param string $message
1771 + * @param int $post_id
1772 + */
1773 + public function file_log($message, $post_id = 0) {
1774 + if (!empty($post_id)) {
1775 + if (false !== strpos($message, '{{URL}}')) {
1776 + $message = str_replace('{{URL}}', get_permalink($post_id), $message);
1777 + }
1778 + if (false !== strpos($message, '{{title}}')) {
1779 + $message = str_replace('{{title}}', '"' . get_the_title($post_id) . '"', $message);
1780 + }
1781 + }
1782 + $this->file_logger->info($message);
1783 + }
1784 +
1785 + /**
1786 + * Prunes the log file
1787 + */
1788 + public function prune_cache_logs() {
1789 + $this->file_log("Pruning the Cache log file");
1790 + $this->file_logger->prune_logs();
1791 + }
1792 +
1793 + /**
1794 + * Update .htaccess file to serve cached files directly.
1795 + *
1796 + * @param bool $enable
1797 + */
1798 + public function update_serve_cache_rules_htaccess_section($enable) {
1799 + $htaccess = new WP_Optimize_Htaccess();
1800 +
1801 + $section_title = 'WP-Optimize rules to serve cached files';
1802 +
1803 + if ($enable) {
1804 + if (!$htaccess->is_commented_section_exists($section_title)) {
1805 + $htaccess->update_commented_section($this->prepare_apache_cache_handle_section(), $section_title, true);
1806 + $htaccess->write_file();
1807 + }
1808 + } elseif ($htaccess->is_commented_section_exists($section_title)) {
1809 + $htaccess->remove_commented_section($section_title);
1810 + $htaccess->write_file();
1811 + }
1812 +
1813 + }
1814 +
1815 + /**
1816 + * Get the .htaccess section with rules to serve cached files
1817 + *
1818 + * @return array
1819 + */
1820 + private function prepare_apache_cache_handle_section() {
1821 +
1822 + $site_root = $this->get_site_root_path();
1823 +
1824 + return array(
1825 + array(
1826 + '<IfModule mod_rewrite.c>',
1827 + 'RewriteEngine On',
1828 + 'RewriteCond %{REQUEST_METHOD} GET|HEAD',
1829 + 'RewriteCond %{QUERY_STRING} =""',
1830 + 'RewriteCond %{HTTP:Cookie} =""',
1831 + 'RewriteCond %{REQUEST_URI} !^/(wp-(?:admin|login|register|comments-post|cron|json))/ [NC]',
1832 + 'RewriteCond %{HTTP_USER_AGENT} !(android|blackberry|ipad|iphone|ipod|windows.phone|iEMobile|opera.mini|opera.mobile|webos|symbian|windows.mobile|kindle|googlebot.mobile) [NC]',
1833 + 'RewriteCond %{DOCUMENT_ROOT}'.$site_root.'wp-content/cache/wpo-cache/%{HTTP_HOST}%{REQUEST_URI}index.html -f',
1834 + 'RewriteRule ^(.*)$ '.$site_root.'wp-content/cache/wpo-cache/%{HTTP_HOST}%{REQUEST_URI}index.html [L]',
1835 + array(
1836 + '<IfModule mod_setenvif.c>',
1837 + 'SetEnvIf Request_URI "/wp-content/cache/wpo-cache/" FROM_CACHE=1',
1838 + '</IfModule>',
1839 + ),
1840 + array(
1841 + '<IfModule mod_headers.c>',
1842 + 'Header always set X-WP-Optimize-Cache-Header "Loaded from WP-Optimize cache" env=FROM_CACHE',
1843 + '</IfModule>',
1844 + ),
1845 + '</IfModule>',
1846 + ),
1847 + );
1848 + }
1849 +
1850 + /**
1851 + * Get relative site root path, if site is placed in the subdirectory then it returns /subdirectory otherwise /
1852 + *
1853 + * @param string $default
1854 + *
1855 + * @return string
1856 + */
1857 + private function get_site_root_path($default = '/') {
1858 + $site_root = wp_parse_url(site_url());
1859 + if (isset($site_root['path'])) {
1860 + $site_root = trailingslashit($site_root['path']);
1861 + } else {
1862 + $site_root = $default;
1863 + }
1864 +
1865 + return $site_root;
1316 1866 }
1317 1867 }
1318 1868
1319 1869 endif;