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
wp-optimize / cache / class-wpo-page-cache.php

class-wpo-page-cache.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at cache/class-wpo-page-cache.php

1,870 lines 59.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Page caching functionality
4 *
5 * Acknowledgement: The page cache functionality was loosely based on the simple cache plugin - https://github.com/tlovett1/simple-cache
6 */
7
8 if (!defined('ABSPATH')) die('No direct access allowed');
9
10 /**
11 * Base cache directory, everything else goes under here
12 */
13 if (!defined('WPO_CACHE_DIR')) define('WPO_CACHE_DIR', untrailingslashit(WP_CONTENT_DIR).'/wpo-cache');
14
15 /**
16 * Extensions directory.
17 */
18 if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', dirname(__FILE__).'/extensions');
19
20 /**
21 * Directory that stores config and related files
22 */
23 if (!defined('WPO_CACHE_CONFIG_DIR')) define('WPO_CACHE_CONFIG_DIR', WPO_CACHE_DIR.'/config');
24
25 require_once dirname(__FILE__) . '/file-based-page-cache-functions.php';
26
27 wpo_cache_load_extensions();
28
29 if (!class_exists('WPO_Page_Cache')) :
30
31 class WPO_Page_Cache {
32
33 /**
34 * Cache config object
35 *
36 * @var mixed
37 */
38 public $config;
39
40 /**
41 * Logger for this class
42 *
43 * @var mixed
44 */
45 public $logger;
46
47 /**
48 * File Logger for this class
49 *
50 * @var mixed
51 */
52 private $file_logger;
53
54 /**
55 * Instance of this class
56 *
57 * @var WPO_Page_Cache | null
58 */
59 public static $instance;
60
61 /**
62 * Store last advanced cache file writing status
63 * If true then last writing finished with error
64 *
65 * @var bool
66 */
67 public $advanced_cache_file_writing_error;
68
69 /**
70 * Store errors
71 *
72 * @var array
73 */
74 private $_errors = array();
75
76 /**
77 * Last advanced cache file content
78 *
79 * @var string
80 */
81 public $advanced_cache_file_content;
82
83 /**
84 * Store the latest advanced-cache.php version required
85 *
86 * @var string
87 */
88 private $_minimum_advanced_cache_file_version = '3.0.17';
89
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 /**
105 * Set everything up here
106 */
107 public function __construct() {
108 $this->config = WPO_Cache_Config::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
119 $this->logger = new Updraft_PHP_Logger();
120 $this->file_logger = new Updraft_File_Logger(WP_Optimize_Utils::get_log_file_path('cache'));
121
122 add_action('activated_plugin', array($this, 'activate_deactivate_plugin'));
123 add_action('deactivate_plugin', array($this, 'activate_deactivate_plugin'));
124 add_action('wpo_purge_old_cache', array($this, 'purge_old'));
125 add_action('wpo_prune_cache_logs', array($this, 'prune_cache_logs'));
126
127 /**
128 * Regenerate config file on cache flush.
129 */
130 add_action('wpo_cache_flush', array($this, 'update_cache_config'));
131 add_action('wpo_cache_flush', array($this, 'delete_cache_size_information'));
132 add_action('wpo_cache_flush', array($this, 'update_cache_cleared_time'));
133 add_action('update_option_permalink_structure', array($this, 'update_option_permalink_structure'), 10, 3);
134
135 // Add purge cache link to admin bar.
136 add_filter('wpo_cache_admin_bar_menu_items', array($this, 'admin_bar_purge_cache'), 20, 1);
137
138 // Handle single page purge.
139 add_action('wp_loaded', array($this, 'handle_purge_single_page_cache'));
140
141 add_action('admin_init', array($this, 'admin_init'));
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
148 $this->check_compatibility_issues();
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() {
189 add_filter('cron_schedules', array($this, 'cron_schedules'));
190 }
191
192 /**
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
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 */
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() {
261 $page_cache_length = $this->config->get_option('page_cache_length');
262
263 if (!wp_next_scheduled('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');
265 }
266 if (!wp_next_scheduled('wpo_prune_cache_logs')) {
267 wp_schedule_event(time(), 'weekly', 'wpo_prune_cache_logs');
268 }
269 }
270
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 /**
282 * Do required actions on activate/deactivate any plugin.
283 */
284 public function activate_deactivate_plugin() {
285
286 wp_clear_scheduled_hook('wpo_purge_old_cache');
287
288 $this->update_cache_config();
289
290 $is_cache_purged = $this->purge();
291
292 if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: plugin activation/deactivation");
293 }
294
295 /**
296 * Check if current user can purge cache.
297 *
298 * @return bool
299 */
300 public function can_purge_cache() {
301 if (!$this->is_enabled()) return false;
302
303 return WP_Optimize_Utils::current_user_can_purge_cache();
304 }
305
306 /**
307 * Add Purge from cache in admin bar.
308 *
309 * @param array $menu_items
310 * @return array
311 */
312 public function admin_bar_purge_cache($menu_items) {
313 global $pagenow;
314 if (!$this->can_purge_cache()) return $menu_items;
315
316 $act_url = WP_Optimize_Utils::get_url_without_cache_purge_params();
317
318 $cache_size = $this->get_cache_size();
319 $cache_size_info = '<h4>'.__('Page cache', 'wp-optimize').'</h4>';
320 // translators: %d is the number of files in the cache
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>';
322
323 $menu_items[] = array(
324 'id' => 'wpo_cache_stats',
325 'title' => $cache_size_info,
326 'meta' => array(
327 'class' => 'wpo-cache-stats',
328 ),
329 'parent' => 'wpo_purge_cache',
330 );
331
332 $menu_items[] = array(
333 'id' => 'wpo_purge_all_pages_cache',
334 'title' => __('Purge cache for all pages', 'wp-optimize'),
335 'href' => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_all_pages_cache'), $act_url),
336 'meta' => array(
337 'title' => __('Purge cache for all pages', 'wp-optimize'),
338 ),
339 'parent' => 'wpo_purge_cache',
340 );
341
342 if (!is_admin() || 'post.php' === $pagenow) {
343 $menu_items[] = array(
344 'id' => 'wpo_purge_this_page_cache',
345 'title' => __('Purge cache for this page', 'wp-optimize'),
346 'href' => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_single_page_cache'), $act_url),
347 'meta' => array(
348 'title' => __('Purge cache for this page', 'wp-optimize'),
349 ),
350 'parent' => 'wpo_purge_cache',
351 );
352 }
353
354 return $menu_items;
355
356 }
357
358 /**
359 * Check if purge single page action sent and purge cache.
360 */
361 public function handle_purge_single_page_cache() {
362
363 if (!$this->can_purge_cache()) return;
364
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
368 if (isset($_GET['wpo_single_page_cache_purged'])) {
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';
371 } else {
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';
374 }
375 // phpcs:enable
376
377 if (is_admin()) {
378 $admin_notices_hook = is_network_admin() ? 'network_admin_notices' : 'admin_notices';
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
390 return;
391 }
392
393 if (!isset($_GET['_wpo_purge'])) return;
394
395 if (wp_verify_nonce(sanitize_key($_GET['_wpo_purge']), 'wpo_purge_single_page_cache')) {
396 $success = false;
397
398 if (is_admin()) {
399 $post = isset($_GET['post']) ? (int) $_GET['post'] : 0;
400 if ($post > 0) {
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);
403 }
404 } else {
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__);
408 }
409
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);
415 exit;
416
417 } elseif (wp_verify_nonce(sanitize_key($_GET['_wpo_purge']), 'wpo_purge_all_pages_cache')) {
418 $success = self::purge();
419 $this->maybe_set_preload_cron_job();
420 if ($success) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
421
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);
427 exit;
428 }
429 }
430
431 /**
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
438 */
439 private function maybe_set_preload_cron_job() {
440 $wpo_page_cache_preloader = WP_Optimize_Page_Cache_Preloader::instance();
441
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 }
447 }
448
449 /**
450 * Show notification in WordPress admin.
451 *
452 * @param string $message HTML (no further escaping is performed)
453 * @param string $type error, warning, success, or info
454 */
455 public function show_notice($message, $type) {
456 global $current_screen;
457
458 if ($current_screen && is_callable(array($current_screen, 'is_block_editor')) && $current_screen->is_block_editor()) : ?>
459 <script>
460 window.addEventListener('load', function() {
461 (function(wp) {
462 if (window.wp && wp.hasOwnProperty('data') && 'function' == typeof wp.data.dispatch) {
463 wp.data.dispatch('core/notices').createNotice(
464 '<?php echo esc_html($type); ?>',
465 '<?php echo wp_kses_post($message); ?>',
466 {
467 isDismissible: true,
468 }
469 );
470 }
471 })(window.wp);
472 });
473 </script>
474 <?php else : ?>
475 <div class="notice wpo-notice notice-<?php echo esc_attr($type); ?> is-dismissible">
476 <p><?php echo wp_kses_post($message); ?></p>
477 </div>
478 <?php
479 endif;
480 }
481
482 /**
483 * Enables page cache
484 *
485 * @param bool $force_enable - Force regenerating everything. E.g. we want to do that when saving the settings
486 *
487 * @return WP_Error|bool - true on success, error otherwise
488 */
489 public function enable($force_enable = false) {
490 global $is_apache;
491 static $already_ran_enable = false;
492
493 if ($already_ran_enable) return $already_ran_enable;
494
495 $folders_created = $this->create_folders();
496 if (is_wp_error($folders_created)) {
497 $already_ran_enable = $folders_created;
498 return $already_ran_enable;
499 }
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
506 // if WPO_ADVANCED_CACHE isn't set, or environment doesn't contain the right constant, force regeneration
507 if (!defined('WPO_ADVANCED_CACHE') || !defined('WP_CACHE')) {
508 $force_enable = true;
509 }
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
520 if (!$force_enable) {
521 $already_ran_enable = $this->update_page_cache_enabled_state(true);
522 return $already_ran_enable;
523 }
524
525 if (!$this->write_advanced_cache() && version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<')) {
526 $message = sprintf("The request to write the file %s failed. ", htmlspecialchars($this->get_advanced_cache_filename()));
527 $message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');
528
529 if (!defined('WP_CLI') || !WP_CLI) {
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');
534 }
535
536 $already_ran_enable = new WP_Error("write_advanced_cache", $message);
537 return $already_ran_enable;
538 }
539
540 if (!$this->write_wp_config(true)) {
541 $already_ran_enable = new WP_Error("write_wp_config", "Could not turn on the WP_CACHE constant in wp-config.php. Check your permissions.");
542 return $already_ran_enable;
543 }
544
545 if (!$this->verify_cache()) {
546 $errors = $this->get_errors();
547 $already_ran_enable = new WP_Error("verify_cache", "Could not verify if the cache was enabled: \n".implode("\n- ", $errors));
548 return $already_ran_enable;
549 }
550
551 $already_ran_enable = $this->update_page_cache_enabled_state(true);
552 return $already_ran_enable;
553 }
554
555 /**
556 * Disables page cache
557 *
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.
560 */
561 public function disable($keep_option_enabled = false) {
562 global $is_apache;
563
564 $ret = true;
565
566 $advanced_cache_file = $this->get_advanced_cache_filename();
567
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.
569 $disabled_wp_config = $this->write_wp_config(false);
570 if (!$disabled_wp_config) {
571 $this->log("Could not turn off the WP_CACHE constant in wp-config.php");
572 $this->add_warning('error_disabling', __('Could not turn off the WP_CACHE constant in wp-config.php', 'wp-optimize'));
573 }
574
575 $disabled_advanced_cache = true;
576 // First try to remove (so that it doesn't look to any other plugin like the file is already 'claimed')
577 // We only touch advanched-cache.php and wp-config.php if it appears that we were in control of advanced-cache.php
578 if (!file_exists($advanced_cache_file) || false !== strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) {
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"))) {
580 $disabled_advanced_cache = false;
581 $this->log("The request to the filesystem to remove or empty advanced-cache.php failed");
582 $this->add_warning('error_disabling', __('The request to the filesystem to remove or empty advanced-cache.php failed', 'wp-optimize'));
583 }
584 }
585
586 // If both actions failed, the cache wasn't disabled. So we send an error. If only one succeeds, it will still be disabled.
587 if (!$disabled_wp_config && !$disabled_advanced_cache) {
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'));
589 }
590
591 // Delete cache to avoid stale cache on next activation
592 $is_cache_purged = $this->purge();
593 if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
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
615 return $ret;
616 }
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();
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
639 /**
640 * Purges the cache
641 *
642 * @return bool - true on success, false otherwise
643 */
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;
656
657 if (!self::delete(WPO_CACHE_FILES_DIR)) {
658 $this->log("The request to the filesystem to delete the cache failed");
659 return false;
660 }
661
662 /**
663 * Fires after purging the cache
664 */
665 do_action('wpo_cache_flush');
666
667 return true;
668 }
669
670 /**
671 * Purge cache files older than cache life span
672 *
673 * @return array Log messages.
674 */
675 public function purge_old() {
676 $log = array();
677 $page_cache_length = $this->config->get_option('page_cache_length');
678 if (0 === $page_cache_length) return $log;
679 $expires = time() - $page_cache_length;
680 $cache_folder = WPO_CACHE_FILES_DIR . '/' . str_ireplace(array('http://', 'https://'), '', get_site_url());
681 // get all directories that are a direct child of current directory
682 if (is_dir($cache_folder) && wp_is_writable($cache_folder)) {
683 if ($handle = opendir($cache_folder)) {
684 while (false !== ($d = readdir($handle))) {
685 if (0 === strcmp($d, '.') || 0 === strcmp($d, '..')) {
686 continue;
687 }
688
689 if ($this->is_front_page_cache($d)) {
690 $modified_time = (int) filemtime("$cache_folder/$d");
691 if ($modified_time <= $expires) {
692 wp_delete_file("$cache_folder/$d");
693 }
694 continue;
695 }
696
697 $dir = $cache_folder.'/'.$d;
698 if (!is_dir($dir)) continue;
699
700 $stat = stat($dir);
701 $modified_time = $stat['mtime'];
702 $log[] = "checking if cache has expired - $d";
703 if ($modified_time <= $expires) {
704 $log[] = "deleting cache in $dir";
705 wpo_delete_files($dir, true);
706 if (file_exists($dir)) rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- N/A
707 }
708 }
709 closedir($handle);
710 }
711 }
712 return $log;
713 }
714
715 /**
716 * Finds out given cache file is of front page or not
717 *
718 * @return bool
719 */
720 private function is_front_page_cache($d) {
721 return in_array($d, array('index.html', 'index.html.gz'));
722 }
723
724 /**
725 * Purges the cache
726 *
727 * @return bool - true on success, false otherwise
728 */
729 public function clean_up() {
730
731 $this->disable();
732
733 if (!self::delete(WPO_CACHE_DIR, true)) {
734 $this->log("The request to the filesystem to clean up the cache failed");
735 return false;
736 }
737
738 return true;
739 }
740
741 /**
742 * Checks whether page caching is turned on in settings.
743 *
744 * @return bool true if the `enable_page_caching` option is set & environment is ready to cache, false otherwise.
745 */
746 public function is_enabled() {
747 return $this->config->get_option('enable_page_caching') && $this->is_environment_functional();
748 }
749
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);
759
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");
762 return false;
763 }
764
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");
767 return false;
768 }
769
770 $config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
771 if (!$is_cli_or_ajax && !file_exists($config_file)) {
772 $this->file_log("Cache config file $config_file is not present");
773 return false;
774 }
775
776 return true;
777 }
778
779 /**
780 * Create the folder structure needed for cache to work
781 *
782 * @return bool | WP_Error - true on success, false otherwise
783 */
784 public function create_folders() {
785 $permissions_str = __('Please check your file permissions.', 'wp-optimize');
786
787 if (!is_dir(WPO_CACHE_DIR) && !wp_mkdir_p(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);
790 }
791
792 if (!is_dir(WPO_CACHE_CONFIG_DIR) && !wp_mkdir_p(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);
795 }
796
797 if (!is_dir(WPO_CACHE_FILES_DIR)) {
798 if (!wp_mkdir_p(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);
801 } else {
802 wpo_disable_cache_directories_viewing();
803 }
804 }
805
806 return true;
807 }
808
809 /**
810 * Get advanced-cache.php file name with full path.
811 *
812 * @return string
813 */
814 public function get_advanced_cache_filename() {
815 return untrailingslashit(WP_CONTENT_DIR) . '/advanced-cache.php';
816 }
817
818 /**
819 * Writes advanced-cache.php
820 *
821 * @param boolean $update_required - Whether the update is required or not.
822 * @return bool
823 */
824 private function write_advanced_cache($update_required = false) {
825 $config_file_basename = $this->config->get_cache_config_filename();
826 $cache_file_basename = untrailingslashit(plugin_dir_path(__FILE__));
827 $plugin_basename = basename(WPO_PLUGIN_MAIN_PATH);
828 $cache_path = '/wpo-cache';
829 $cache_files_path = '/cache/wpo-cache';
830 $cache_extensions_path = WPO_CACHE_EXT_DIR;
831 $wpo_version = WPO_VERSION;
832 $wpo_home_url = trailingslashit(network_home_url());
833 $abspath = ABSPATH;
834
835 // CS does not like heredoc
836 // phpcs:disable
837 $this->advanced_cache_file_content = <<<EOF
838 <?php
839
840 if (!defined('ABSPATH')) die('No direct access allowed');
841
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)
843
844 if (!defined('WPO_ADVANCED_CACHE')) define('WPO_ADVANCED_CACHE', true);
845
846 \$possible_plugin_locations = array(
847 defined('WP_PLUGIN_DIR') ? WP_PLUGIN_DIR.'/$plugin_basename/cache' : false,
848 defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR.'/plugins/$plugin_basename/cache' : false,
849 dirname(__FILE__).'/plugins/$plugin_basename/cache',
850 '$cache_file_basename',
851 );
852
853 \$plugin_location = false;
854
855 foreach (\$possible_plugin_locations as \$possible_location) {
856 if (false !== \$possible_location && @file_exists(\$possible_location.'/file-based-page-cache.php')) {
857 \$plugin_location = \$possible_location;
858 break;
859 }
860 }
861
862 if (false === \$plugin_location) {
863 if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND')) define('WPO_PLUGIN_LOCATION_NOT_FOUND', true);
864 \$protocol = \$_SERVER['REQUEST_SCHEME'];
865 \$host = \$_SERVER['HTTP_HOST'];
866 \$request_uri = \$_SERVER['REQUEST_URI'];
867 if (strcasecmp('$wpo_home_url', \$protocol . '://' . \$host . \$request_uri) === 0) {
868 error_log('WP-Optimize: No caching took place, because the plugin location could not be found');
869 }
870 } else {
871 if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND')) define('WPO_PLUGIN_LOCATION_NOT_FOUND', false);
872 }
873
874 if (is_admin()) { return; }
875
876 if (!defined('WPO_CACHE_DIR')) define('WPO_CACHE_DIR', WP_CONTENT_DIR.'$cache_path');
877 if (!defined('WPO_CACHE_CONFIG_DIR')) define('WPO_CACHE_CONFIG_DIR', WPO_CACHE_DIR.'/config');
878 if (!defined('WPO_CACHE_FILES_DIR')) define('WPO_CACHE_FILES_DIR', WP_CONTENT_DIR.'$cache_files_path');
879 if (false !== \$plugin_location) {
880 if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', \$plugin_location.'/extensions');
881 } else {
882 if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', '$cache_extensions_path');
883 }
884
885 if (!@file_exists(WPO_CACHE_CONFIG_DIR . '/$config_file_basename')) { return; }
886
887 \$GLOBALS['wpo_cache_config'] = @json_decode(file_get_contents(WPO_CACHE_CONFIG_DIR . '/$config_file_basename'), true);
888
889 if (empty(\$GLOBALS['wpo_cache_config'])) {
890 include_once(WPO_CACHE_CONFIG_DIR . '/$config_file_basename');
891 }
892
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 }
897
898 if (empty(\$GLOBALS['wpo_cache_config']['enable_page_caching'])) { return; }
899
900 if (false !== \$plugin_location) { include_once(\$plugin_location.'/file-based-page-cache.php'); }
901
902 EOF;
903
904 // phpcs:enable
905 $advanced_cache_filename = $this->get_advanced_cache_filename();
906
907 // If the file content is already up to date, success
908 if (is_file($advanced_cache_filename) && file_get_contents($advanced_cache_filename) === $this->advanced_cache_file_content) {
909 $this->advanced_cache_file_writing_error = false;
910 return true;
911 }
912
913 // check if we can't write the advanced cache file
914 // case 1: the directory is read-only and the file doesn't exist
915 if (!is_file($advanced_cache_filename) && !wp_is_writable(dirname($advanced_cache_filename))) {
916 $this->advanced_cache_file_writing_error = true;
917 return false;
918 }
919
920 // case 2: the file already exists but it's read-only
921 if (is_file($advanced_cache_filename) && !wp_is_writable($advanced_cache_filename)) {
922 if (version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<') || $update_required) {
923 $this->advanced_cache_file_writing_error = true;
924 return false;
925 } else {
926 $this->advanced_cache_file_writing_error = false;
927 return true;
928 }
929 }
930
931 if (!file_put_contents($this->get_advanced_cache_filename(), $this->advanced_cache_file_content)) {
932 $this->advanced_cache_file_writing_error = true;
933 return false;
934 }
935
936 $this->advanced_cache_file_writing_error = false;
937 return true;
938 }
939
940 /**
941 * Update advanced cache version if needed.
942 */
943 public function maybe_update_advanced_cache() {
944
945 if (!$this->is_enabled()) return;
946
947 if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND') || (defined('WPO_PLUGIN_LOCATION_NOT_FOUND') && true === WPO_PLUGIN_LOCATION_NOT_FOUND)) {
948 if (!$this->write_advanced_cache(true)) {
949 add_action('admin_notices', array($this, 'show_admin_notice_advanced_cache'));
950 }
951 }
952
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
955 $advanced_cache_current_version = $this->get_advanced_cache_version();
956 $is_the_site_url_or_folder_changed = $this->is_the_site_url_or_folder_changed();
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
966 if (!$this->write_advanced_cache()) {
967 add_action('admin_notices', array($this, 'notice_advanced_cache_autoupdate_error'));
968 } else {
969 $this->update_cache_config();
970 }
971 }
972
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 /**
997 * Show notification when advanced-cache.php could not be updated.
998 */
999 public function notice_advanced_cache_autoupdate_error() {
1000 $this->show_notice(__('The file advanced-cache.php needs to be updated, but the automatic process failed.', 'wp-optimize').
1001 ' <a href="'.admin_url('admin.php?page=wpo_cache').'">'.__('Please try to disable and then re-enable the WP-Optimize cache manually.', 'wp-optimize').'</a>', 'error');
1002 }
1003
1004 /**
1005 * Get WPO version number from advanced-cache.php file.
1006 *
1007 * @return bool|mixed
1008 */
1009 public function get_advanced_cache_version() {
1010 if (!is_file($this->get_advanced_cache_filename())) return false;
1011
1012 $version = false;
1013 $content = file_get_contents($this->get_advanced_cache_filename());
1014
1015 if (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\)/Ui', $content, $match)) {
1016 $version = $match[1];
1017 }
1018
1019 return $version;
1020 }
1021
1022 /**
1023 * Set WP_CACHE on or off in wp-config.php
1024 *
1025 * @param boolean $status value of WP_CACHE.
1026 * @return boolean true if the value was set, false otherwise
1027 */
1028 private function write_wp_config($status = true) {
1029 // If we changed the value in wp-config, save it, in case we need to change it again in the same run.
1030 static $changed = false;
1031
1032 if (defined('WP_CACHE') && WP_CACHE === $status && !$changed) {
1033 return true;
1034 }
1035
1036 $config_path = $this->_get_wp_config();
1037
1038 // Couldn't find wp-config.php.
1039 if (!$config_path) {
1040 return false;
1041 }
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
1048 $config_file_string = file_get_contents($config_path);
1049
1050 // Config file is empty. Maybe couldn't read it?
1051 if (empty($config_file_string)) {
1052 return false;
1053 }
1054
1055 $config_file = preg_split("#(\n|\r\n)#", $config_file_string);
1056 $line_key = false;
1057
1058 foreach ($config_file as $key => $line) {
1059 if (!preg_match('/^\s*define\(\s*(\'|")([A-Z_]+)(\'|")(.*)/i', $line, $match)) {
1060 continue;
1061 }
1062
1063 if ('WP_CACHE' === $match[2]) {
1064 $line_key = $key;
1065 }
1066 }
1067
1068 if (false !== $line_key) {
1069 unset($config_file[$line_key]);
1070 }
1071
1072
1073 if ($status) {
1074 array_shift($config_file);
1075 array_unshift($config_file, '<?php', "define('WP_CACHE', true); // WP-Optimize Cache");
1076 }
1077
1078 foreach ($config_file as $key => $line) {
1079 if ('' === $line) {
1080 unset($config_file[$key]);
1081 }
1082 }
1083 if (!file_put_contents($config_path, implode(PHP_EOL, $config_file))) {
1084 return false;
1085 }
1086 $changed = true;
1087 return true;
1088 }
1089
1090 /**
1091 * Verify we can write to the file system
1092 *
1093 * @return boolean
1094 */
1095 private function verify_cache() {
1096 if (function_exists('clearstatcache')) {
1097 clearstatcache();
1098 }
1099 $errors = 0;
1100
1101 // First check wp-config.php.
1102 if (!$this->_get_wp_config() && !wp_is_writable($this->_get_wp_config())) {
1103 $this->log("Unable to write to or find wp-config.php; please check file/folder permissions");
1104 $this->add_warning('verify_cache', __("Unable to write to or find wp-config.php; please check file/folder permissions.", 'wp-optimize'));
1105 }
1106
1107 $advanced_cache_file = untrailingslashit(WP_CONTENT_DIR).'/advanced-cache.php';
1108
1109 // Now check wp-content. We need to be able to create files of the same user as this file.
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))) {
1111 $this->log("Unable to write the file advanced-cache.php inside the wp-content folder; please check file/folder permissions");
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'));
1113 $errors++;
1114 }
1115
1116 if (file_exists(WPO_CACHE_FILES_DIR)) {
1117 if (!wp_is_writable(WPO_CACHE_FILES_DIR)) {
1118 $this->log("Unable to write inside the cache files folder; please check file/folder permissions");
1119 // translators: %s is cache folder path
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));
1121 }
1122 }
1123
1124 if (file_exists(WPO_CACHE_CONFIG_DIR)) {
1125 if (!wp_is_writable(WPO_CACHE_CONFIG_DIR)) {
1126 $this->log("Unable to write inside the cache configuration folder; please check file/folder permissions");
1127 // If the config exists, only send a warning. Otherwise send an error.
1128 $type = 'warning';
1129 if (!file_exists(WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename())) {
1130 $type = 'error';
1131 $errors++;
1132 }
1133 // translators: %s is cache config folder path
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);
1135 }
1136 }
1137
1138 return !$errors;
1139 }
1140
1141 /**
1142 * Update permalink structure in cache config
1143 *
1144 * @param string $old_value Old value of permalink_structure option
1145 * @param string $value New value of permalink_structure option
1146 * @param string $option Option name `permalink_structure`
1147 */
1148 public function update_option_permalink_structure($old_value, $value, $option) {
1149 $current_config = $this->config->get();
1150 if ($old_value !== $value) {
1151 $current_config[$option] = $value;
1152 $this->config->update($current_config, true);
1153 }
1154 }
1155
1156 /**
1157 * Update cache config. Used to support 3d party plugins.
1158 *
1159 * @return bool | WP_Error
1160 */
1161 public function update_cache_config() {
1162 // get current cache settings.
1163 $current_config = $this->config->get();
1164 // and call update to change if need cookies and query variable names.
1165 return $this->config->update($current_config);
1166 }
1167
1168 /**
1169 * Delete information about cache size.
1170 */
1171 public function delete_cache_size_information() {
1172 //not removing delete_transient so it clear this key from option table
1173 delete_transient('wpo_get_cache_size');
1174 delete_site_transient('wpo_get_cache_size');
1175 }
1176
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 /**
1187 * Get current cache size.
1188 *
1189 * @return array
1190 */
1191 public function get_cache_size() {
1192 $cache_size = get_site_transient('wpo_get_cache_size');
1193
1194 if (!empty($cache_size)) return $cache_size;
1195
1196 $infos = $this->get_dir_infos(WPO_CACHE_FILES_DIR);
1197 $cache_size = array(
1198 'size' => $infos['size'],
1199 'file_count' => $infos['file_count']
1200 );
1201
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);
1205
1206 return $cache_size;
1207 }
1208
1209 /**
1210 * Fetch directory information.
1211 *
1212 * @param string $dir
1213 * @return array
1214 */
1215 private function get_dir_infos($dir) {
1216 $filtered = apply_filters('wpo_cache_size_files_to_ignore', $this->files_to_ignore);
1217 $this->files_to_ignore = is_array($filtered) ? $filtered : array();
1218
1219 return WP_Optimize_Utils::get_folder_stats($dir, $this->files_to_ignore);
1220 }
1221
1222 /**
1223 * Returns the path to wp-config
1224 *
1225 * @return string|boolean wp-config.php path.
1226 */
1227 private function _get_wp_config() {
1228
1229 $config_path = false;
1230
1231 foreach (get_included_files() as $filename) {
1232 if (preg_match('/(\\\\|\/)wp-config\.php$/i', $filename)) {
1233 $config_path = $filename;
1234 break;
1235 }
1236 }
1237
1238 // WP-CLI doesn't include wp-config.php that's why we use function from WP-CLI to locate config file.
1239 if (!$config_path && is_callable('wpo_wp_cli_locate_wp_config')) {
1240 $config_path = wpo_wp_cli_locate_wp_config();
1241 }
1242
1243 return $config_path;
1244 }
1245
1246 /**
1247 * Util to delete folders and/or files
1248 *
1249 * @param string $src
1250 * @return boolean
1251 */
1252 public static function delete($src) {
1253 return wpo_delete_files($src);
1254 }
1255
1256 /**
1257 * Delete cached files for specific url.
1258 *
1259 * @param string $url
1260 * @param bool $recursive If true child elements will deleted too
1261 *
1262 * @return bool
1263 */
1264 public static function delete_cache_by_url($url, $recursive = false) {
1265 if (!defined('WPO_CACHE_FILES_DIR') || '' === $url) return false;
1266
1267 $path = self::get_full_path_from_url($url);
1268
1269 do_action('wpo_delete_cache_by_url', $url, $recursive);
1270
1271 do_action('wpo_preload_url', $url);
1272
1273 return wpo_delete_files($path, $recursive);
1274 }
1275
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 /**
1306 * Delete cached files for single post.
1307 *
1308 * @param integer $post_id The post ID
1309 *
1310 * @return bool
1311 */
1312 public static function delete_single_post_cache($post_id) {
1313 $is_cache_deleted = self::really_delete_single_post_cache($post_id);
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
1331 $post_url = get_permalink($post_id);
1332
1333 $path = self::get_full_path_from_url($post_url);
1334
1335 // for posts with pagination run purging cache recursively.
1336 $post = get_post($post_id);
1337 $recursive = preg_match('/\<\!--nextpage--\>/', $post->post_content) ? true : false;
1338
1339 do_action('wpo_delete_cache_by_url', $post_url, $recursive);
1340
1341 do_action('wpo_preload_url', $post_url);
1342
1343 return wpo_delete_files($path, $recursive);
1344 }
1345
1346 /**
1347 * Delete cached home page files.
1348 */
1349 public static function delete_homepage_cache() {
1350
1351 if (!defined('WPO_CACHE_FILES_DIR')) return;
1352
1353 $homepage_url = get_home_url(get_current_blog_id());
1354
1355 $path = self::get_full_path_from_url($homepage_url);
1356
1357 $recursive = false;
1358
1359 do_action('wpo_delete_cache_by_url', $homepage_url, $recursive);
1360
1361 do_action('wpo_preload_url', $homepage_url);
1362
1363 wpo_delete_files($path, $recursive);
1364 }
1365
1366 /**
1367 * Delete feed from cache.
1368 */
1369 public static function delete_feed_cache() {
1370 if (!defined('WPO_CACHE_FILES_DIR')) return;
1371
1372 $homepage_url = get_home_url(get_current_blog_id());
1373
1374 $path = self::get_full_path_from_url($homepage_url) . 'feed/';
1375
1376 do_action('wpo_delete_cache_by_url', $path, true);
1377
1378 wpo_delete_files($path, true);
1379 }
1380
1381 /**
1382 * Delete post feed from cache.
1383 *
1384 * @param int $post_id Post ID
1385 */
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) {
1398 if (!defined('WPO_CACHE_FILES_DIR')) return;
1399
1400 $post_url = get_permalink($post_id);
1401
1402 $path = self::get_full_path_from_url($post_url) . 'feed/';
1403
1404 do_action('wpo_delete_cache_by_url', $path, true);
1405
1406 wpo_delete_files($path, true);
1407 }
1408
1409 /**
1410 * Delete comments feed from cache.
1411 */
1412 public static function delete_comments_feed() {
1413 if (!defined('WPO_CACHE_FILES_DIR')) return;
1414
1415 $comments_feed_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/feed/';
1416
1417 $path = self::get_full_path_from_url($comments_feed_url);
1418
1419 do_action('wpo_delete_cache_by_url', $comments_feed_url, true);
1420
1421 wpo_delete_files($path, true);
1422
1423 // delete empty comments dir from the cache
1424 $comments_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/';
1425 $path = self::get_full_path_from_url($comments_url);
1426
1427 if (wpo_is_empty_dir($path)) {
1428 wpo_delete_files($path, true);
1429 }
1430 }
1431
1432 /**
1433 * Returns full path to the cache folder by url.
1434 *
1435 * @param string $url
1436 * @return string
1437 */
1438 public static function get_full_path_from_url($url) {
1439 return trailingslashit(WPO_CACHE_FILES_DIR) . trailingslashit(wpo_get_url_path($url));
1440 }
1441
1442 /**
1443 * Admin actions
1444 *
1445 * @return void
1446 */
1447 public function admin_init() {
1448 // Maybe update the advanced cache.
1449 if ((!defined('DOING_AJAX') || !DOING_AJAX) && WP_Optimize()->current_user_can('update_plugins')) {
1450 $this->maybe_update_advanced_cache();
1451 $this->cron_activate();
1452 }
1453 }
1454
1455 /**
1456 * Logs error messages
1457 *
1458 * @param string $message
1459 * @return null|void
1460 */
1461 public function log($message) {
1462 if (isset($this->logger)) {
1463 $this->logger->log($message, 'error');
1464 }
1465 }
1466
1467 /**
1468 * Returns an instance of the current class, creates one if it doesn't exist
1469 *
1470 * @return self
1471 */
1472 public static function instance() {
1473 if (empty(self::$instance)) {
1474 self::$instance = new self();
1475 }
1476 return self::$instance;
1477 }
1478
1479
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 /**
1535 * Adds an error to the error store
1536 *
1537 * @param string $code - The error code
1538 * @param string $message - The error's message
1539 * @param string $type - The error's type (error, warning)
1540 * @return void
1541 */
1542 public function add_error($code, $message, $type = 'error') {
1543 if (!isset($this->_errors[$type])) {
1544 $this->_errors[$type] = new WP_Error($code, $message);
1545 } else {
1546 $this->_errors[$type]->add($code, $message);
1547 }
1548 }
1549
1550 /**
1551 * Adds a warning to the error store
1552 *
1553 * @param string $code - The error code
1554 * @param string $message - The error's message
1555 * @return void
1556 */
1557 public function add_warning($code, $message) {
1558 $this->add_error($code, $message, 'warning');
1559 }
1560
1561 /**
1562 * Get all recorded errors
1563 *
1564 * @param string $type - The error type
1565 * @param boolean $get_messages_only - Whether to get only the messages, or the full WP_Error object
1566 * @return boolean|array|WP_Error
1567 */
1568 public function get_errors($type = 'error', $get_messages_only = true) {
1569 if (!$this->has_errors($type)) return false;
1570 $errors = $this->_errors[$type];
1571 if ($get_messages_only) {
1572 return $errors->get_error_messages();
1573 }
1574 return $errors;
1575 }
1576
1577 /**
1578 * Check if any errors were recorded
1579 *
1580 * @param string $type - The error type
1581 * @return boolean
1582 */
1583 public function has_errors($type = 'error') {
1584 return isset($this->_errors[$type]) && !empty($this->_errors[$type]) && $this->_errors[$type]->has_errors();
1585 }
1586
1587 /**
1588 * Check if any warnings were recorded
1589 *
1590 * @return boolean
1591 */
1592 public function has_warnings() {
1593 return $this->has_errors('warning');
1594 }
1595
1596 /**
1597 * Check the cache compatibility issues.
1598 */
1599 public function check_compatibility_issues() {
1600 if (!$this->is_enabled()) return;
1601
1602 if ($this->is_pagespeedninja_gzip_active()) add_action('admin_notices', array($this, 'show_pagespeedninja_gzip_notice'));
1603 if ($this->is_farfutureexpiration_gzip_active()) add_action('admin_notices', array($this, 'show_farfutureexpiration_gzip_notice'));
1604 }
1605
1606 /**
1607 * Check if PageSpeed Ninja is active and GZIP compression option is enabled.
1608 *
1609 * @return bool
1610 */
1611 public function is_pagespeedninja_gzip_active() {
1612 if (!class_exists('PagespeedNinja')) return false;
1613
1614 $options = get_option('pagespeedninja_config');
1615 $gzip = isset($options['psi_EnableGzipCompression']) && isset($options['html_gzip']) && (bool) $options['psi_EnableGzipCompression'] && (bool) $options['html_gzip'];
1616
1617 return $gzip;
1618 }
1619
1620 /**
1621 * Output PageSpeed Ninja Gzip notice.
1622 */
1623 public function show_pagespeedninja_gzip_notice() {
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>';
1625 }
1626
1627 /**
1628 * Check if Far Future Expiration is active and GZIP compression option is enabled.
1629 *
1630 * @return bool
1631 */
1632 public function is_farfutureexpiration_gzip_active() {
1633 if (!class_exists('farFutureExpiration')) return false;
1634
1635 $options = get_option('far_future_expiration_settings');
1636 $gzip = !empty($options) && $options['enable_gzip'];
1637
1638 return $gzip;
1639 }
1640
1641 /**
1642 * Output Far Future Expiration Gzip notice.
1643 */
1644 public function show_farfutureexpiration_gzip_notice() {
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>';
1646 }
1647
1648 /**
1649 * This is a notice to show users that writing `advanced-cache.php` failed
1650 */
1651 public function show_admin_notice_advanced_cache() {
1652 // translators: %s is advanced-cache.php file path
1653 $message = sprintf(__('The request to write the file %s failed.', 'wp-optimize'), htmlspecialchars($this->get_advanced_cache_filename()));
1654 $message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');
1655 WP_Optimize()->include_template('notices/cache-notice.php', false, array('message' => $message));
1656 }
1657
1658 /**
1659 * Scheduler public functions to update schedulers
1660 *
1661 * @param array $schedules An array of schedules being passed.
1662 * @return array An array of schedules being returned.
1663 */
1664 public function cron_schedules($schedules) {
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
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
1688 return $schedules;
1689 }
1690
1691 /**
1692 * Update cache config file to reflect the webp images option
1693 */
1694 public function update_webp_images_option() {
1695 if ($this->is_enabled()) {
1696 $cache_settings = WPO_Cache_Config::instance()->get();
1697 $cache_settings['use_webp_images'] = WP_Optimize()->get_options()->get_option('webp_conversion');
1698 WPO_Cache_Config::instance()->update($cache_settings);
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;
1866 }
1867 }
1868
1869 endif;
1870