PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.6.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.6.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.6.0, at cache/class-wpo-page-cache.php

1,847 lines 57.7 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('update_option_permalink_structure', array($this, 'update_option_permalink_structure'), 10, 3);
133
134 // Add purge cache link to admin bar.
135 add_filter('wpo_cache_admin_bar_menu_items', array($this, 'admin_bar_purge_cache'), 20, 1);
136
137 // Handle single page purge.
138 add_action('wp_loaded', array($this, 'handle_purge_single_page_cache'));
139
140 add_action('admin_init', array($this, 'admin_init'));
141
142 add_action('update_option_gmt_offset', array($this, 'update_gmt_offset_timezone_string_config'), 10, 3);
143 add_action('update_option_timezone_string', array($this, 'update_gmt_offset_timezone_string_config'), 10, 3);
144 add_action('update_option_date_format', array($this, 'update_option_date_format'), 10, 2);
145 add_action('update_option_time_format', array($this, 'update_option_time_format'), 10, 2);
146
147 $this->check_compatibility_issues();
148
149 $this->ensure_cron_schedules_filter_registered();
150
151 add_action('wpo_save_images_settings', array($this, 'update_webp_images_option'));
152
153 add_action('wpo_preload_url', array($this, 'maybe_preload_url'));
154
155 // Setup filters for exceptions.
156 add_filter('wpo_restricted_cache_page_type', 'wpo_restricted_cache_page_type');
157 add_filter('wpo_url_in_conditional_tags_exceptions', 'wpo_url_in_conditional_tags_exceptions');
158 }
159
160 /**
161 * Ensures that the cron-related filter is registered.
162 *
163 * If `init` has already run, the filter is registered immediately.
164 * Otherwise, it schedules the registration to occur during `init`.
165 *
166 * @return void
167 */
168 private function ensure_cron_schedules_filter_registered() {
169 if (did_action('init')) {
170 $this->register_cron_schedules_filter();
171 } else {
172 add_action('init', array($this, 'register_cron_schedules_filter'));
173 }
174 }
175
176 /**
177 * Registers the custom cron schedule filter for WP-Cron.
178 *
179 * This method hooks the class's `cron_schedules()` method into WordPress's
180 * `cron_schedules` filter, allowing the plugin to define custom cron intervals.
181 *
182 * It is recommended to call this method on or after the `init` action to ensure
183 * that translations and other dependencies are properly loaded before the filter runs.
184 *
185 * @return void
186 */
187 public function register_cron_schedules_filter() {
188 add_filter('cron_schedules', array($this, 'cron_schedules'));
189 }
190
191 /**
192 * Determines whether the current page should be cached.
193 *
194 * This method checks cache rules to identify if caching is possible.
195 * If caching is not allowed, it adds appropriate HTTP headers and debug messages.
196 *
197 * @return bool True if the page should be cached, false otherwise.
198 */
199 public function should_cache_page() {
200
201 if (!$this->is_enabled()) return false;
202
203 $no_cache_because = array();
204
205 // Check serve cache rules, to identify if we need to cache the page.
206 $can_serve_from_cache = wpo_can_serve_from_cache();
207
208 if (false === $can_serve_from_cache) return false;
209
210 if (is_array($can_serve_from_cache)) $no_cache_because = $can_serve_from_cache;
211
212 if (!empty($no_cache_because)) {
213 do_action('wpo_page_not_cached', $no_cache_because);
214
215 // Add http header
216 if (!wp_doing_cron()) {
217 $no_cache_because_message = join(", ", $no_cache_because);
218 wpo_cache_add_nocache_http_header_with_send_headers_action($no_cache_because_message);
219
220 $not_cached_details = "";
221
222 // Output the reason only when the user has turned on debugging
223 if ((defined('WP_DEBUG') && WP_DEBUG) || !empty(TeamUpdraft\WP_Optimize\Includes\Fragments\fetch_superglobal('get', 'wpo_cache_debug'))) {
224 $not_cached_details = "because: ".$no_cache_because_message;
225 }
226 wpo_cache_add_footer_output(sprintf("Page not served from cache %s", $not_cached_details));
227 }
228
229 return false;
230 }
231
232 return true;
233 }
234
235 /**
236 * Handle cache settings update
237 *
238 * @param array $new_settings
239 * @return void
240 */
241 public function cache_settings_updated($new_settings) {
242
243 $this->unschedule_purge_old_cache();
244
245 if ($new_settings['enable_page_caching']) {
246 $this->schedule_purge_old_cache();
247 }
248 }
249
250 /**
251 * Activate cron job when the cache is enabled for deleting expired cache files
252 */
253 public function cron_activate() {
254 if ($this->is_enabled()) {
255 $this->schedule_purge_old_cache();
256 } else {
257 $this->unschedule_purge_old_cache();
258 }
259 }
260
261 /**
262 * Schedules events to purge old cache
263 *
264 * @return void
265 */
266 private function schedule_purge_old_cache() {
267 $page_cache_length = $this->config->get_option('page_cache_length');
268
269 if (!wp_next_scheduled('wpo_purge_old_cache')) {
270 wp_schedule_event(time() + (false === $page_cache_length ? 86400 : $page_cache_length), 'wpo_purge_old_cache', 'wpo_purge_old_cache');
271 }
272 if (!wp_next_scheduled('wpo_prune_cache_logs')) {
273 wp_schedule_event(time(), 'weekly', 'wpo_prune_cache_logs');
274 }
275 }
276
277 /**
278 * Unschedules events to purge old cache
279 *
280 * @return void
281 */
282 private function unschedule_purge_old_cache() {
283 wp_clear_scheduled_hook('wpo_purge_old_cache');
284 wp_clear_scheduled_hook('wpo_prune_cache_logs');
285 }
286
287 /**
288 * Do required actions on activate/deactivate any plugin.
289 */
290 public function activate_deactivate_plugin() {
291
292 wp_clear_scheduled_hook('wpo_purge_old_cache');
293
294 $this->update_cache_config();
295
296 $is_cache_purged = $this->purge();
297
298 if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: plugin activation/deactivation");
299 }
300
301 /**
302 * Check if current user can purge cache.
303 *
304 * @return bool
305 */
306 public function can_purge_cache() {
307 if (!$this->is_enabled()) return false;
308
309 return WP_Optimize_Utils::current_user_can_purge_cache();
310 }
311
312 /**
313 * Add Purge from cache in admin bar.
314 *
315 * @param array $menu_items
316 * @return array
317 */
318 public function admin_bar_purge_cache($menu_items) {
319 global $pagenow;
320 if (!$this->can_purge_cache()) return $menu_items;
321
322 $act_url = remove_query_arg(array('wpo_single_page_cache_purged', 'wpo_all_pages_cache_purged'));
323
324 $cache_size = $this->get_cache_size();
325 $cache_size_info = '<h4>'.__('Page cache', 'wp-optimize').'</h4>';
326 // translators: %d is the number of files in the cache
327 $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>';
328
329 $menu_items[] = array(
330 'id' => 'wpo_cache_stats',
331 'title' => $cache_size_info,
332 'meta' => array(
333 'class' => 'wpo-cache-stats',
334 ),
335 'parent' => 'wpo_purge_cache',
336 );
337
338 $menu_items[] = array(
339 'id' => 'wpo_purge_all_pages_cache',
340 'title' => __('Purge cache for all pages', 'wp-optimize'),
341 'href' => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_all_pages_cache'), $act_url),
342 'meta' => array(
343 'title' => __('Purge cache for all pages', 'wp-optimize'),
344 ),
345 'parent' => 'wpo_purge_cache',
346 );
347
348 if (!is_admin() || 'post.php' === $pagenow) {
349 $menu_items[] = array(
350 'id' => 'wpo_purge_this_page_cache',
351 'title' => __('Purge cache for this page', 'wp-optimize'),
352 'href' => add_query_arg('_wpo_purge', wp_create_nonce('wpo_purge_single_page_cache'), $act_url),
353 'meta' => array(
354 'title' => __('Purge cache for this page', 'wp-optimize'),
355 ),
356 'parent' => 'wpo_purge_cache',
357 );
358 }
359
360 return $menu_items;
361
362 }
363
364 /**
365 * Check if purge single page action sent and purge cache.
366 */
367 public function handle_purge_single_page_cache() {
368
369 if (!$this->can_purge_cache()) return;
370
371 if (isset($_GET['wpo_single_page_cache_purged']) || isset($_GET['wpo_all_pages_cache_purged'])) {
372 // phpcs:disable
373 // We are not using $_GET values, just checks if the variable is set and then use hard coded string
374 if (isset($_GET['wpo_single_page_cache_purged'])) {
375 $message = $_GET['wpo_single_page_cache_purged'] ? __('Cache purged for this page', 'wp-optimize') : __('Error purging cache for this page', 'wp-optimize');
376 $type = $_GET['wpo_single_page_cache_purged'] ? 'success' : 'error';
377 } else {
378 $message = $_GET['wpo_all_pages_cache_purged'] ? __('Cache purged for all pages', 'wp-optimize') : __('Error purging cache for all pages', 'wp-optimize');
379 $type = $_GET['wpo_all_pages_cache_purged'] ? 'success' : 'error';
380 }
381 // phpcs:enable
382
383 if (is_admin()) {
384 add_action('admin_notices', function() use ($message, $type) {
385 $this->show_notice($message, $type);
386 });
387 } else {
388 printf('<script>window.onload = function() {alert("%s");}</script>', esc_js($message));
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 * @return bool|WP_Error - true on success or WP_Error on failure.
559 */
560 public function disable() {
561 global $is_apache;
562
563 $ret = true;
564
565 $advanced_cache_file = $this->get_advanced_cache_filename();
566
567 // 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.
568 $disabled_wp_config = $this->write_wp_config(false);
569 if (!$disabled_wp_config) {
570 $this->log("Could not turn off the WP_CACHE constant in wp-config.php");
571 $this->add_warning('error_disabling', __('Could not turn off the WP_CACHE constant in wp-config.php', 'wp-optimize'));
572 }
573
574 $disabled_advanced_cache = true;
575 // First try to remove (so that it doesn't look to any other plugin like the file is already 'claimed')
576 // We only touch advanched-cache.php and wp-config.php if it appears that we were in control of advanced-cache.php
577 if (!file_exists($advanced_cache_file) || false !== strpos(file_get_contents($advanced_cache_file), 'WP-Optimize advanced-cache.php')) {
578 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"))) {
579 $disabled_advanced_cache = false;
580 $this->log("The request to the filesystem to remove or empty advanced-cache.php failed");
581 $this->add_warning('error_disabling', __('The request to the filesystem to remove or empty advanced-cache.php failed', 'wp-optimize'));
582 }
583 }
584
585 // If both actions failed, the cache wasn't disabled. So we send an error. If only one succeeds, it will still be disabled.
586 if (!$disabled_wp_config && !$disabled_advanced_cache) {
587 $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'));
588 }
589
590 // Delete cache to avoid stale cache on next activation
591 $is_cache_purged = $this->purge();
592 if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
593
594 // Delete `.htaccess` rules to serve cached files
595 if ($is_apache && apply_filters('wpo_serve_cache_via_htaccess', false)) {
596 $this->update_serve_cache_rules_htaccess_section(false);
597 }
598
599 // Unschedule cron job to purge old cache
600 wp_clear_scheduled_hook('wpo_purge_old_cache');
601
602 if (!is_wp_error($ret)) {
603 wp_clear_scheduled_hook('wpo_prune_cache_logs');
604 $ret = $this->update_page_cache_enabled_state(false);
605 }
606
607 if (!is_wp_error($ret)) {
608 do_action('wpo_page_cache_disabled');
609 }
610
611 return $ret;
612 }
613
614 /**
615 * Update page cache enabled state.
616 * Only updates `enable_page_caching` if the option exists and the value has changed.
617 *
618 * @param bool $enabled Whether page caching should be marked as enabled.
619 *
620 * @return true|WP_Error true on success, WP_Error on failure.
621 */
622 private function update_page_cache_enabled_state($enabled) {
623 $config = WPO_Cache_Config::instance();
624 $cache_config = $config->get();
625
626 if (isset($cache_config['enable_page_caching']) && $cache_config['enable_page_caching'] === $enabled) {
627 return true;
628 }
629
630 $cache_config['enable_page_caching'] = $enabled;
631 return $config->update($cache_config, true);
632 }
633
634 /**
635 * Purges the cache
636 *
637 * @return bool - true on success, false otherwise
638 */
639 public function purge() {
640 /**
641 * Filters whether activating / deactivating a plugin will purge the cache.
642 */
643 $this->should_purge = apply_filters('wpo_purge_page_cache_on_activate_deactivate_plugin', true);
644 $purge_actions = array(
645 'deactivate_' . plugin_basename(WPO_PLUGIN_MAIN_PATH . 'wp-optimize.php'),
646 'deactivate_plugin',
647 'activated_plugin',
648 );
649 $is_deactivation_action = in_array(current_action(), $purge_actions, true);
650 if ($is_deactivation_action && !$this->should_purge) return false;
651
652 if (!self::delete(WPO_CACHE_FILES_DIR)) {
653 $this->log("The request to the filesystem to delete the cache failed");
654 return false;
655 }
656
657 /**
658 * Fires after purging the cache
659 */
660 do_action('wpo_cache_flush');
661
662 return true;
663 }
664
665 /**
666 * Purge cache files older than cache life span
667 *
668 * @return array Log messages.
669 */
670 public function purge_old() {
671 $log = array();
672 $page_cache_length = $this->config->get_option('page_cache_length');
673 if (0 === $page_cache_length) return $log;
674 $expires = time() - $page_cache_length;
675 $cache_folder = WPO_CACHE_FILES_DIR . '/' . str_ireplace(array('http://', 'https://'), '', get_site_url());
676 // get all directories that are a direct child of current directory
677 if (is_dir($cache_folder) && wp_is_writable($cache_folder)) {
678 if ($handle = opendir($cache_folder)) {
679 while (false !== ($d = readdir($handle))) {
680 if (0 === strcmp($d, '.') || 0 === strcmp($d, '..')) {
681 continue;
682 }
683
684 if ($this->is_front_page_cache($d)) {
685 $modified_time = (int) filemtime("$cache_folder/$d");
686 if ($modified_time <= $expires) {
687 wp_delete_file("$cache_folder/$d");
688 }
689 continue;
690 }
691
692 $dir = $cache_folder.'/'.$d;
693 if (!is_dir($dir)) continue;
694
695 $stat = stat($dir);
696 $modified_time = $stat['mtime'];
697 $log[] = "checking if cache has expired - $d";
698 if ($modified_time <= $expires) {
699 $log[] = "deleting cache in $dir";
700 wpo_delete_files($dir, true);
701 if (file_exists($dir)) rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- N/A
702 }
703 }
704 closedir($handle);
705 }
706 }
707 return $log;
708 }
709
710 /**
711 * Finds out given cache file is of front page or not
712 *
713 * @return bool
714 */
715 private function is_front_page_cache($d) {
716 return in_array($d, array('index.html', 'index.html.gz'));
717 }
718
719 /**
720 * Purges the cache
721 *
722 * @return bool - true on success, false otherwise
723 */
724 public function clean_up() {
725
726 $this->disable();
727
728 if (!self::delete(WPO_CACHE_DIR, true)) {
729 $this->log("The request to the filesystem to clean up the cache failed");
730 return false;
731 }
732
733 return true;
734 }
735
736 /**
737 * Check if cache is enabled and working
738 *
739 * @return bool - true on success, false otherwise
740 */
741 public function is_enabled() {
742
743 if (!$this->config->get_option('enable_page_caching')) return false;
744
745 if (!defined('WP_CACHE') || !WP_CACHE) {
746 if ((!(defined('WP_CLI') && WP_CLI)) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
747 $this->log("WP_CACHE constant is not present in wp-config.php");
748 return false;
749 }
750 }
751
752 if (!defined('WPO_ADVANCED_CACHE') || !WPO_ADVANCED_CACHE) {
753 if ((!(defined('WP_CLI') && WP_CLI)) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
754 $this->log("WPO_ADVANCED_CACHE constant is not present in advanced-cache.php");
755 return false;
756 }
757 }
758
759 $config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
760 if (!file_exists($config_file)) {
761 if ((!(defined('WP_CLI') && WP_CLI)) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
762 $this->log("Cache config file $config_file is not present");
763 return false;
764 }
765 }
766
767 return true;
768 }
769
770 /**
771 * Create the folder structure needed for cache to work
772 *
773 * @return bool | WP_Error - true on success, false otherwise
774 */
775 public function create_folders() {
776 $permissions_str = __('Please check your file permissions.', 'wp-optimize');
777
778 if (!is_dir(WPO_CACHE_DIR) && !wp_mkdir_p(WPO_CACHE_DIR)) {
779 // translators: %s is a directory name
780 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);
781 }
782
783 if (!is_dir(WPO_CACHE_CONFIG_DIR) && !wp_mkdir_p(WPO_CACHE_CONFIG_DIR)) {
784 // translators: %s is a directory name
785 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);
786 }
787
788 if (!is_dir(WPO_CACHE_FILES_DIR)) {
789 if (!wp_mkdir_p(WPO_CACHE_FILES_DIR)) {
790 // translators: %s is a directory name
791 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);
792 } else {
793 wpo_disable_cache_directories_viewing();
794 }
795 }
796
797 return true;
798 }
799
800 /**
801 * Get advanced-cache.php file name with full path.
802 *
803 * @return string
804 */
805 public function get_advanced_cache_filename() {
806 return untrailingslashit(WP_CONTENT_DIR) . '/advanced-cache.php';
807 }
808
809 /**
810 * Writes advanced-cache.php
811 *
812 * @param boolean $update_required - Whether the update is required or not.
813 * @return bool
814 */
815 private function write_advanced_cache($update_required = false) {
816 $config_file_basename = $this->config->get_cache_config_filename();
817 $cache_file_basename = untrailingslashit(plugin_dir_path(__FILE__));
818 $plugin_basename = basename(WPO_PLUGIN_MAIN_PATH);
819 $cache_path = '/wpo-cache';
820 $cache_files_path = '/cache/wpo-cache';
821 $cache_extensions_path = WPO_CACHE_EXT_DIR;
822 $wpo_version = WPO_VERSION;
823 $wpo_home_url = trailingslashit(network_home_url());
824 $abspath = ABSPATH;
825
826 // CS does not like heredoc
827 // phpcs:disable
828 $this->advanced_cache_file_content = <<<EOF
829 <?php
830
831 if (!defined('ABSPATH')) die('No direct access allowed');
832
833 // 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)
834
835 if (!defined('WPO_ADVANCED_CACHE')) define('WPO_ADVANCED_CACHE', true);
836
837 \$possible_plugin_locations = array(
838 defined('WP_PLUGIN_DIR') ? WP_PLUGIN_DIR.'/$plugin_basename/cache' : false,
839 defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR.'/plugins/$plugin_basename/cache' : false,
840 dirname(__FILE__).'/plugins/$plugin_basename/cache',
841 '$cache_file_basename',
842 );
843
844 \$plugin_location = false;
845
846 foreach (\$possible_plugin_locations as \$possible_location) {
847 if (false !== \$possible_location && @file_exists(\$possible_location.'/file-based-page-cache.php')) {
848 \$plugin_location = \$possible_location;
849 break;
850 }
851 }
852
853 if (false === \$plugin_location) {
854 if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND')) define('WPO_PLUGIN_LOCATION_NOT_FOUND', true);
855 \$protocol = \$_SERVER['REQUEST_SCHEME'];
856 \$host = \$_SERVER['HTTP_HOST'];
857 \$request_uri = \$_SERVER['REQUEST_URI'];
858 if (strcasecmp('$wpo_home_url', \$protocol . '://' . \$host . \$request_uri) === 0) {
859 error_log('WP-Optimize: No caching took place, because the plugin location could not be found');
860 }
861 } else {
862 if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND')) define('WPO_PLUGIN_LOCATION_NOT_FOUND', false);
863 }
864
865 if (is_admin()) { return; }
866
867 if (!defined('WPO_CACHE_DIR')) define('WPO_CACHE_DIR', WP_CONTENT_DIR.'$cache_path');
868 if (!defined('WPO_CACHE_CONFIG_DIR')) define('WPO_CACHE_CONFIG_DIR', WPO_CACHE_DIR.'/config');
869 if (!defined('WPO_CACHE_FILES_DIR')) define('WPO_CACHE_FILES_DIR', WP_CONTENT_DIR.'$cache_files_path');
870 if (false !== \$plugin_location) {
871 if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', \$plugin_location.'/extensions');
872 } else {
873 if (!defined('WPO_CACHE_EXT_DIR')) define('WPO_CACHE_EXT_DIR', '$cache_extensions_path');
874 }
875
876 if (!@file_exists(WPO_CACHE_CONFIG_DIR . '/$config_file_basename')) { return; }
877
878 \$GLOBALS['wpo_cache_config'] = @json_decode(file_get_contents(WPO_CACHE_CONFIG_DIR . '/$config_file_basename'), true);
879
880 if (empty(\$GLOBALS['wpo_cache_config'])) {
881 include_once(WPO_CACHE_CONFIG_DIR . '/$config_file_basename');
882 }
883
884 if (empty(\$GLOBALS['wpo_cache_config'])) {
885 error_log('WP-Optimize: Caching failed because the configuration data could not be loaded from the config file.');
886 return;
887 }
888
889 if (empty(\$GLOBALS['wpo_cache_config']['enable_page_caching'])) { return; }
890
891 if (false !== \$plugin_location) { include_once(\$plugin_location.'/file-based-page-cache.php'); }
892
893 EOF;
894
895 // phpcs:enable
896 $advanced_cache_filename = $this->get_advanced_cache_filename();
897
898 // If the file content is already up to date, success
899 if (is_file($advanced_cache_filename) && file_get_contents($advanced_cache_filename) === $this->advanced_cache_file_content) {
900 $this->advanced_cache_file_writing_error = false;
901 return true;
902 }
903
904 // check if we can't write the advanced cache file
905 // case 1: the directory is read-only and the file doesn't exist
906 if (!is_file($advanced_cache_filename) && !wp_is_writable(dirname($advanced_cache_filename))) {
907 $this->advanced_cache_file_writing_error = true;
908 return false;
909 }
910
911 // case 2: the file already exists but it's read-only
912 if (is_file($advanced_cache_filename) && !wp_is_writable($advanced_cache_filename)) {
913 if (version_compare($this->get_advanced_cache_version(), $this->_minimum_advanced_cache_file_version, '<') || $update_required) {
914 $this->advanced_cache_file_writing_error = true;
915 return false;
916 } else {
917 $this->advanced_cache_file_writing_error = false;
918 return true;
919 }
920 }
921
922 if (!file_put_contents($this->get_advanced_cache_filename(), $this->advanced_cache_file_content)) {
923 $this->advanced_cache_file_writing_error = true;
924 return false;
925 }
926
927 $this->advanced_cache_file_writing_error = false;
928 return true;
929 }
930
931 /**
932 * Update advanced cache version if needed.
933 */
934 public function maybe_update_advanced_cache() {
935
936 if (!$this->is_enabled()) return;
937
938 if (!defined('WPO_PLUGIN_LOCATION_NOT_FOUND') || (defined('WPO_PLUGIN_LOCATION_NOT_FOUND') && true === WPO_PLUGIN_LOCATION_NOT_FOUND)) {
939 if (!$this->write_advanced_cache(true)) {
940 add_action('admin_notices', array($this, 'show_admin_notice_advanced_cache'));
941 }
942 }
943
944 // from 3.0.17 we use more secure way to store cache config files and need update advanced-cache.php
945 // if the site url or folder changed we also update advanced-cache.php
946 $advanced_cache_current_version = $this->get_advanced_cache_version();
947 $is_the_site_url_or_folder_changed = $this->is_the_site_url_or_folder_changed();
948
949 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;
950
951 // Purge the cache when the site url or folder changed.
952 if ($is_the_site_url_or_folder_changed) {
953 $is_cache_purged = $this->purge();
954 if ($is_cache_purged) $this->file_log("Full Cache Purge triggered by: ". __METHOD__);
955 }
956
957 if (!$this->write_advanced_cache()) {
958 add_action('admin_notices', array($this, 'notice_advanced_cache_autoupdate_error'));
959 } else {
960 $this->update_cache_config();
961 }
962 }
963
964 /**
965 * Check if the site url or folder doesn't equal to values from advanced-cache.php
966 *
967 * @return bool
968 */
969 private function is_the_site_url_or_folder_changed() {
970 if (!is_file($this->get_advanced_cache_filename())) return false;
971
972 $content = file_get_contents($this->get_advanced_cache_filename());
973
974 if (false === $content) return false;
975
976 if (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\) (\(homeurl: (.+)\)) (\(abspath: (.+)\)) \(do not change/Ui', $content, $match)) {
977 $wpo_home_url = trailingslashit(network_home_url());
978 $abspath = ABSPATH;
979 return ($wpo_home_url !== $match[3] || $abspath !== $match[5]);
980 } elseif (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\)/Ui', $content, $match)) {
981 return true;
982 }
983
984 return false;
985 }
986
987 /**
988 * Show notification when advanced-cache.php could not be updated.
989 */
990 public function notice_advanced_cache_autoupdate_error() {
991 $this->show_notice(__('The file advanced-cache.php needs to be updated, but the automatic process failed.', 'wp-optimize').
992 ' <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');
993 }
994
995 /**
996 * Get WPO version number from advanced-cache.php file.
997 *
998 * @return bool|mixed
999 */
1000 public function get_advanced_cache_version() {
1001 if (!is_file($this->get_advanced_cache_filename())) return false;
1002
1003 $version = false;
1004 $content = file_get_contents($this->get_advanced_cache_filename());
1005
1006 if (preg_match('/WP\-Optimize advanced\-cache\.php \(written by version\: (.+)\)/Ui', $content, $match)) {
1007 $version = $match[1];
1008 }
1009
1010 return $version;
1011 }
1012
1013 /**
1014 * Set WP_CACHE on or off in wp-config.php
1015 *
1016 * @param boolean $status value of WP_CACHE.
1017 * @return boolean true if the value was set, false otherwise
1018 */
1019 private function write_wp_config($status = true) {
1020 // If we changed the value in wp-config, save it, in case we need to change it again in the same run.
1021 static $changed = false;
1022
1023 if (defined('WP_CACHE') && WP_CACHE === $status && !$changed) {
1024 return true;
1025 }
1026
1027 $config_path = $this->_get_wp_config();
1028
1029 // Couldn't find wp-config.php.
1030 if (!$config_path) {
1031 return false;
1032 }
1033
1034 $config_file_string = file_get_contents($config_path);
1035
1036 // Config file is empty. Maybe couldn't read it?
1037 if (empty($config_file_string)) {
1038 return false;
1039 }
1040
1041 $config_file = preg_split("#(\n|\r\n)#", $config_file_string);
1042 $line_key = false;
1043
1044 foreach ($config_file as $key => $line) {
1045 if (!preg_match('/^\s*define\(\s*(\'|")([A-Z_]+)(\'|")(.*)/i', $line, $match)) {
1046 continue;
1047 }
1048
1049 if ('WP_CACHE' === $match[2]) {
1050 $line_key = $key;
1051 }
1052 }
1053
1054 if (false !== $line_key) {
1055 unset($config_file[$line_key]);
1056 }
1057
1058
1059 if ($status) {
1060 array_shift($config_file);
1061 array_unshift($config_file, '<?php', "define('WP_CACHE', true); // WP-Optimize Cache");
1062 }
1063
1064 foreach ($config_file as $key => $line) {
1065 if ('' === $line) {
1066 unset($config_file[$key]);
1067 }
1068 }
1069 if (!file_put_contents($config_path, implode(PHP_EOL, $config_file))) {
1070 return false;
1071 }
1072 $changed = true;
1073 return true;
1074 }
1075
1076 /**
1077 * Verify we can write to the file system
1078 *
1079 * @return boolean
1080 */
1081 private function verify_cache() {
1082 if (function_exists('clearstatcache')) {
1083 clearstatcache();
1084 }
1085 $errors = 0;
1086
1087 // First check wp-config.php.
1088 if (!$this->_get_wp_config() && !wp_is_writable($this->_get_wp_config())) {
1089 $this->log("Unable to write to or find wp-config.php; please check file/folder permissions");
1090 $this->add_warning('verify_cache', __("Unable to write to or find wp-config.php; please check file/folder permissions.", 'wp-optimize'));
1091 }
1092
1093 $advanced_cache_file = untrailingslashit(WP_CONTENT_DIR).'/advanced-cache.php';
1094
1095 // Now check wp-content. We need to be able to create files of the same user as this file.
1096 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))) {
1097 $this->log("Unable to write the file advanced-cache.php inside the wp-content folder; please check file/folder permissions");
1098 $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'));
1099 $errors++;
1100 }
1101
1102 if (file_exists(WPO_CACHE_FILES_DIR)) {
1103 if (!wp_is_writable(WPO_CACHE_FILES_DIR)) {
1104 $this->log("Unable to write inside the cache files folder; please check file/folder permissions");
1105 // translators: %s is cache folder path
1106 $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));
1107 }
1108 }
1109
1110 if (file_exists(WPO_CACHE_CONFIG_DIR)) {
1111 if (!wp_is_writable(WPO_CACHE_CONFIG_DIR)) {
1112 $this->log("Unable to write inside the cache configuration folder; please check file/folder permissions");
1113 // If the config exists, only send a warning. Otherwise send an error.
1114 $type = 'warning';
1115 if (!file_exists(WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename())) {
1116 $type = 'error';
1117 $errors++;
1118 }
1119 // translators: %s is cache config folder path
1120 $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);
1121 }
1122 }
1123
1124 return !$errors;
1125 }
1126
1127 /**
1128 * Update permalink structure in cache config
1129 *
1130 * @param string $old_value Old value of permalink_structure option
1131 * @param string $value New value of permalink_structure option
1132 * @param string $option Option name `permalink_structure`
1133 */
1134 public function update_option_permalink_structure($old_value, $value, $option) {
1135 $current_config = $this->config->get();
1136 if ($old_value !== $value) {
1137 $current_config[$option] = $value;
1138 $this->config->update($current_config, true);
1139 }
1140 }
1141
1142 /**
1143 * Update cache config. Used to support 3d party plugins.
1144 *
1145 * @return bool | WP_Error
1146 */
1147 public function update_cache_config() {
1148 // get current cache settings.
1149 $current_config = $this->config->get();
1150 // and call update to change if need cookies and query variable names.
1151 return $this->config->update($current_config);
1152 }
1153
1154 /**
1155 * Delete information about cache size.
1156 */
1157 public function delete_cache_size_information() {
1158 //not removing delete_transient so it clear this key from option table
1159 delete_transient('wpo_get_cache_size');
1160 delete_site_transient('wpo_get_cache_size');
1161 }
1162
1163 /**
1164 * Get current cache size.
1165 *
1166 * @return array
1167 */
1168 public function get_cache_size() {
1169 $cache_size = get_site_transient('wpo_get_cache_size');
1170
1171 if (!empty($cache_size)) return $cache_size;
1172
1173 $infos = $this->get_dir_infos(WPO_CACHE_FILES_DIR);
1174 $cache_size = array(
1175 'size' => $infos['size'],
1176 'file_count' => $infos['file_count']
1177 );
1178
1179 $expiration = apply_filters('wpo_cache_size_expiration', DAY_IN_SECONDS);
1180 $expiration = is_numeric($expiration) ? (int) $expiration : DAY_IN_SECONDS;
1181 set_site_transient('wpo_get_cache_size', $cache_size, $expiration);
1182
1183 return $cache_size;
1184 }
1185
1186 /**
1187 * Fetch directory information.
1188 *
1189 * @param string $dir
1190 * @return array
1191 */
1192 private function get_dir_infos($dir) {
1193 $filtered = apply_filters('wpo_cache_size_files_to_ignore', $this->files_to_ignore);
1194 $this->files_to_ignore = is_array($filtered) ? $filtered : array();
1195
1196 return WP_Optimize_Utils::get_folder_stats($dir, $this->files_to_ignore);
1197 }
1198
1199 /**
1200 * Returns the path to wp-config
1201 *
1202 * @return string|boolean wp-config.php path.
1203 */
1204 private function _get_wp_config() {
1205
1206 $config_path = false;
1207
1208 foreach (get_included_files() as $filename) {
1209 if (preg_match('/(\\\\|\/)wp-config\.php$/i', $filename)) {
1210 $config_path = $filename;
1211 break;
1212 }
1213 }
1214
1215 // WP-CLI doesn't include wp-config.php that's why we use function from WP-CLI to locate config file.
1216 if (!$config_path && is_callable('wpo_wp_cli_locate_wp_config')) {
1217 $config_path = wpo_wp_cli_locate_wp_config();
1218 }
1219
1220 return $config_path;
1221 }
1222
1223 /**
1224 * Util to delete folders and/or files
1225 *
1226 * @param string $src
1227 * @return boolean
1228 */
1229 public static function delete($src) {
1230 return wpo_delete_files($src);
1231 }
1232
1233 /**
1234 * Delete cached files for specific url.
1235 *
1236 * @param string $url
1237 * @param bool $recursive If true child elements will deleted too
1238 *
1239 * @return bool
1240 */
1241 public static function delete_cache_by_url($url, $recursive = false) {
1242 if (!defined('WPO_CACHE_FILES_DIR') || '' === $url) return false;
1243
1244 $path = self::get_full_path_from_url($url);
1245
1246 do_action('wpo_delete_cache_by_url', $url, $recursive);
1247
1248 do_action('wpo_preload_url', $url);
1249
1250 return wpo_delete_files($path, $recursive);
1251 }
1252
1253 /**
1254 * Adds the URL to the preload list and hooks the preload task to shutdown
1255 *
1256 * @param string $url
1257 * @return void
1258 */
1259 public function maybe_preload_url($url) {
1260 if (!$this->should_auto_preload_purged_contents()) return;
1261
1262 static $urls_preloaded = array();
1263
1264 // Remove the query string from the URL
1265 $url = preg_replace('/\?.*/', '', $url);
1266
1267 if (!in_array($url, $urls_preloaded)) {
1268 $urls_preloaded[] = $url;
1269
1270 $preloader = WP_Optimize_Page_Cache_Preloader::instance();
1271 $preloader->add_url_to_preload_list($url);
1272 $url_task_creator = array($preloader, 'create_tasks_for_auto_preload_urls');
1273
1274 $preloader->maybe_dont_close_browser_connection();
1275
1276 if (!has_action('shutdown', $url_task_creator)) {
1277 add_action('shutdown', $url_task_creator);
1278 }
1279 }
1280 }
1281
1282 /**
1283 * Delete cached files for single post.
1284 *
1285 * @param integer $post_id The post ID
1286 *
1287 * @return bool
1288 */
1289 public static function delete_single_post_cache($post_id) {
1290 $is_cache_deleted = self::really_delete_single_post_cache($post_id);
1291
1292 do_action('wpo_single_post_cache_deleted', $post_id);
1293
1294 return $is_cache_deleted;
1295 }
1296
1297 /**
1298 * Really delete cached files for single post.
1299 *
1300 * @param integer $post_id The post ID
1301 *
1302 * @return bool
1303 */
1304 public static function really_delete_single_post_cache($post_id) {
1305
1306 if (!defined('WPO_CACHE_FILES_DIR')) return false;
1307
1308 $post_url = get_permalink($post_id);
1309
1310 $path = self::get_full_path_from_url($post_url);
1311
1312 // for posts with pagination run purging cache recursively.
1313 $post = get_post($post_id);
1314 $recursive = preg_match('/\<\!--nextpage--\>/', $post->post_content) ? true : false;
1315
1316 do_action('wpo_delete_cache_by_url', $post_url, $recursive);
1317
1318 do_action('wpo_preload_url', $post_url);
1319
1320 return wpo_delete_files($path, $recursive);
1321 }
1322
1323 /**
1324 * Delete cached home page files.
1325 */
1326 public static function delete_homepage_cache() {
1327
1328 if (!defined('WPO_CACHE_FILES_DIR')) return;
1329
1330 $homepage_url = get_home_url(get_current_blog_id());
1331
1332 $path = self::get_full_path_from_url($homepage_url);
1333
1334 $recursive = false;
1335
1336 do_action('wpo_delete_cache_by_url', $homepage_url, $recursive);
1337
1338 do_action('wpo_preload_url', $homepage_url);
1339
1340 wpo_delete_files($path, $recursive);
1341 }
1342
1343 /**
1344 * Delete feed from cache.
1345 */
1346 public static function delete_feed_cache() {
1347 if (!defined('WPO_CACHE_FILES_DIR')) return;
1348
1349 $homepage_url = get_home_url(get_current_blog_id());
1350
1351 $path = self::get_full_path_from_url($homepage_url) . 'feed/';
1352
1353 do_action('wpo_delete_cache_by_url', $path, true);
1354
1355 wpo_delete_files($path, true);
1356 }
1357
1358 /**
1359 * Delete post feed from cache.
1360 *
1361 * @param int $post_id Post ID
1362 */
1363 public static function delete_post_feed_cache($post_id) {
1364 self::really_delete_post_feed_cache($post_id);
1365
1366 do_action('wpo_single_post_feed_cache_deleted', $post_id);
1367 }
1368
1369 /**
1370 * Really delete post feed from cache.
1371 *
1372 * @param int $post_id Post ID
1373 */
1374 public static function really_delete_post_feed_cache($post_id) {
1375 if (!defined('WPO_CACHE_FILES_DIR')) return;
1376
1377 $post_url = get_permalink($post_id);
1378
1379 $path = self::get_full_path_from_url($post_url) . 'feed/';
1380
1381 do_action('wpo_delete_cache_by_url', $path, true);
1382
1383 wpo_delete_files($path, true);
1384 }
1385
1386 /**
1387 * Delete comments feed from cache.
1388 */
1389 public static function delete_comments_feed() {
1390 if (!defined('WPO_CACHE_FILES_DIR')) return;
1391
1392 $comments_feed_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/feed/';
1393
1394 $path = self::get_full_path_from_url($comments_feed_url);
1395
1396 do_action('wpo_delete_cache_by_url', $comments_feed_url, true);
1397
1398 wpo_delete_files($path, true);
1399
1400 // delete empty comments dir from the cache
1401 $comments_url = trailingslashit(get_home_url(get_current_blog_id())) . 'comments/';
1402 $path = self::get_full_path_from_url($comments_url);
1403
1404 if (wpo_is_empty_dir($path)) {
1405 wpo_delete_files($path, true);
1406 }
1407 }
1408
1409 /**
1410 * Returns full path to the cache folder by url.
1411 *
1412 * @param string $url
1413 * @return string
1414 */
1415 public static function get_full_path_from_url($url) {
1416 return trailingslashit(WPO_CACHE_FILES_DIR) . trailingslashit(wpo_get_url_path($url));
1417 }
1418
1419 /**
1420 * Admin actions
1421 *
1422 * @return void
1423 */
1424 public function admin_init() {
1425 // Maybe update the advanced cache.
1426 if ((!defined('DOING_AJAX') || !DOING_AJAX) && WP_Optimize()->current_user_can('update_plugins')) {
1427 $this->maybe_update_advanced_cache();
1428 $this->cron_activate();
1429 }
1430 }
1431
1432 /**
1433 * Logs error messages
1434 *
1435 * @param string $message
1436 * @return null|void
1437 */
1438 public function log($message) {
1439 if (isset($this->logger)) {
1440 $this->logger->log($message, 'error');
1441 }
1442 }
1443
1444 /**
1445 * Returns an instance of the current class, creates one if it doesn't exist
1446 *
1447 * @return self
1448 */
1449 public static function instance() {
1450 if (empty(self::$instance)) {
1451 self::$instance = new self();
1452 }
1453 return self::$instance;
1454 }
1455
1456
1457 /**
1458 * Update gmt_offset and timezone_string cache config value, used with hook `update_gmt_offset_timezone_string_config`.
1459 *
1460 * @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)
1461 * @param float | string $new_value
1462 * @param string $option gmt_offset|timezone_string
1463 *
1464 * @return null
1465 */
1466 public function update_gmt_offset_timezone_string_config($old_value, $new_value, $option) {
1467 if ('' === $new_value) return;
1468
1469 $current_config = $this->config->get();
1470 if ('timezone_string' === $option) {
1471 $timeZone = new DateTimeZone($new_value);
1472 $dateTime = new DateTime("now");
1473 $gmt_offset = $timeZone->getOffset($dateTime);
1474 $current_config['gmt_offset'] = round($gmt_offset/3600, 1);
1475 $current_config['timezone_string'] = $new_value;
1476 } elseif ('' !== $new_value) {
1477 $current_config['gmt_offset'] = $new_value;
1478 $current_config['timezone_string'] = '';
1479 }
1480 $this->config->update($current_config, true);
1481 }
1482
1483 /**
1484 * Update `date_format` cache config value, used with hook `update_option_date_format`.
1485 *
1486 * @param string $old_value Old date format
1487 * @param string $new_value New date format
1488 *
1489 * @return void
1490 */
1491 public function update_option_date_format($old_value, $new_value) {
1492 $current_config = $this->config->get();
1493 $current_config['date_format'] = $new_value;
1494 $this->config->update($current_config, true);
1495 }
1496
1497 /**
1498 * Update `time_format` cache config value, used with hook `update_option_time_format`.
1499 *
1500 * @param string $old_value Old time format
1501 * @param string $new_value New time format
1502 *
1503 * @return void
1504 */
1505 public function update_option_time_format($old_value, $new_value) {
1506 $current_config = $this->config->get();
1507 $current_config['time_format'] = $new_value;
1508 $this->config->update($current_config, true);
1509 }
1510
1511 /**
1512 * Adds an error to the error store
1513 *
1514 * @param string $code - The error code
1515 * @param string $message - The error's message
1516 * @param string $type - The error's type (error, warning)
1517 * @return void
1518 */
1519 public function add_error($code, $message, $type = 'error') {
1520 if (!isset($this->_errors[$type])) {
1521 $this->_errors[$type] = new WP_Error($code, $message);
1522 } else {
1523 $this->_errors[$type]->add($code, $message);
1524 }
1525 }
1526
1527 /**
1528 * Adds a warning to the error store
1529 *
1530 * @param string $code - The error code
1531 * @param string $message - The error's message
1532 * @return void
1533 */
1534 public function add_warning($code, $message) {
1535 $this->add_error($code, $message, 'warning');
1536 }
1537
1538 /**
1539 * Get all recorded errors
1540 *
1541 * @param string $type - The error type
1542 * @param boolean $get_messages_only - Whether to get only the messages, or the full WP_Error object
1543 * @return boolean|array|WP_Error
1544 */
1545 public function get_errors($type = 'error', $get_messages_only = true) {
1546 if (!$this->has_errors($type)) return false;
1547 $errors = $this->_errors[$type];
1548 if ($get_messages_only) {
1549 return $errors->get_error_messages();
1550 }
1551 return $errors;
1552 }
1553
1554 /**
1555 * Check if any errors were recorded
1556 *
1557 * @param string $type - The error type
1558 * @return boolean
1559 */
1560 public function has_errors($type = 'error') {
1561 return isset($this->_errors[$type]) && !empty($this->_errors[$type]) && $this->_errors[$type]->has_errors();
1562 }
1563
1564 /**
1565 * Check if any warnings were recorded
1566 *
1567 * @return boolean
1568 */
1569 public function has_warnings() {
1570 return $this->has_errors('warning');
1571 }
1572
1573 /**
1574 * Check the cache compatibility issues.
1575 */
1576 public function check_compatibility_issues() {
1577 if (!$this->is_enabled()) return;
1578
1579 if ($this->is_pagespeedninja_gzip_active()) add_action('admin_notices', array($this, 'show_pagespeedninja_gzip_notice'));
1580 if ($this->is_farfutureexpiration_gzip_active()) add_action('admin_notices', array($this, 'show_farfutureexpiration_gzip_notice'));
1581 }
1582
1583 /**
1584 * Check if PageSpeed Ninja is active and GZIP compression option is enabled.
1585 *
1586 * @return bool
1587 */
1588 public function is_pagespeedninja_gzip_active() {
1589 if (!class_exists('PagespeedNinja')) return false;
1590
1591 $options = get_option('pagespeedninja_config');
1592 $gzip = isset($options['psi_EnableGzipCompression']) && isset($options['html_gzip']) && (bool) $options['psi_EnableGzipCompression'] && (bool) $options['html_gzip'];
1593
1594 return $gzip;
1595 }
1596
1597 /**
1598 * Output PageSpeed Ninja Gzip notice.
1599 */
1600 public function show_pagespeedninja_gzip_notice() {
1601 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>';
1602 }
1603
1604 /**
1605 * Check if Far Future Expiration is active and GZIP compression option is enabled.
1606 *
1607 * @return bool
1608 */
1609 public function is_farfutureexpiration_gzip_active() {
1610 if (!class_exists('farFutureExpiration')) return false;
1611
1612 $options = get_option('far_future_expiration_settings');
1613 $gzip = !empty($options) && $options['enable_gzip'];
1614
1615 return $gzip;
1616 }
1617
1618 /**
1619 * Output Far Future Expiration Gzip notice.
1620 */
1621 public function show_farfutureexpiration_gzip_notice() {
1622 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>';
1623 }
1624
1625 /**
1626 * This is a notice to show users that writing `advanced-cache.php` failed
1627 */
1628 public function show_admin_notice_advanced_cache() {
1629 // translators: %s is advanced-cache.php file path
1630 $message = sprintf(__('The request to write the file %s failed.', 'wp-optimize'), htmlspecialchars($this->get_advanced_cache_filename()));
1631 $message .= ' '.__('Please check file and directory permissions on the file paths up to this point, and your PHP error log.', 'wp-optimize');
1632 WP_Optimize()->include_template('notices/cache-notice.php', false, array('message' => $message));
1633 }
1634
1635 /**
1636 * Scheduler public functions to update schedulers
1637 *
1638 * @param array $schedules An array of schedules being passed.
1639 * @return array An array of schedules being returned.
1640 */
1641 public function cron_schedules($schedules) {
1642 $page_cache_length = $this->config->get_option('page_cache_length');
1643 // When the interval is less than `WP_CRON_LOCK_TIMEOUT` error messages are triggered
1644 // So, instead of `0` (now) we are making it equal to the constant value.
1645 // https://github.com/WordPress/WordPress/commit/84d19848666a81584e0007a6ab7f7ad3c990d71a
1646 if (defined('WP_CRON_LOCK_TIMEOUT') && $page_cache_length < WP_CRON_LOCK_TIMEOUT) {
1647 $page_cache_length = WP_CRON_LOCK_TIMEOUT;
1648 }
1649
1650 $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'));
1651
1652 $interval = WP_Optimize_Page_Cache_Preloader::instance()->get_continue_preload_cron_interval();
1653 $schedules['wpo_page_cache_preload_continue_interval'] = array(
1654 'interval' => $interval,
1655 // translators: %d is the number of minutes
1656 'display' => sprintf(__('%d minutes', 'wp-optimize'), round($interval / 60, 1))
1657 );
1658
1659 $schedules['wpo_use_cache_lifespan'] = array(
1660 'interval' => $page_cache_length,
1661 // translators: %s is the cache lifespan
1662 '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'))
1663 );
1664
1665 return $schedules;
1666 }
1667
1668 /**
1669 * Update cache config file to reflect the webp images option
1670 */
1671 public function update_webp_images_option() {
1672 if ($this->is_enabled()) {
1673 $cache_settings = WPO_Cache_Config::instance()->get();
1674 $cache_settings['use_webp_images'] = WP_Optimize()->get_options()->get_option('webp_conversion');
1675 WPO_Cache_Config::instance()->update($cache_settings);
1676 }
1677 }
1678
1679 /**
1680 * Maybe regenerate cache config file, in case of migrations
1681 */
1682 public function maybe_regenerate_cache_config_file() {
1683 $config_file = WPO_CACHE_CONFIG_DIR . '/'.$this->config->get_cache_config_filename();
1684 if (!file_exists($config_file)) {
1685 $config = $this->config->get();
1686 $this->config->write($config);
1687 }
1688 }
1689
1690 /**
1691 * Checks if the given cache directory is empty
1692 *
1693 * @param string $path The path to the cache directory.
1694 * @return bool Returns true if the cache directory is empty, false otherwise.
1695 */
1696 public static function is_cache_empty($path) {
1697 if (!is_dir($path)) return true;
1698
1699 if ($handle = opendir($path)) {
1700 while (false !== ($entry = readdir($handle))) {
1701 if ("." === $entry || ".." === $entry) {
1702 continue;
1703 }
1704
1705 $full_path = $path . '/' . $entry;
1706 if (is_file($full_path)) {
1707 closedir($handle);
1708 return false;
1709 }
1710 }
1711 closedir($handle);
1712 return true;
1713 }
1714 return true;
1715 }
1716
1717 /**
1718 * Remove query params from request url
1719 *
1720 * @param string $url The request url.
1721 * @return string Returns url without query params.
1722 */
1723 private static function remove_query_params($url) {
1724 $parsed_url = wp_parse_url($url);
1725 if (!$parsed_url) return '';
1726 return $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'];
1727 }
1728
1729 /**
1730 * Check configuration for auto preloading after cache deletion
1731 *
1732 * @return bool
1733 */
1734 public function should_auto_preload_purged_contents() {
1735
1736 if (!$this->is_enabled()) return false; // as Page Cache is not enabled we should not auto preload purged content.
1737
1738 $wpo_cache = WP_Optimize()->get_page_cache();
1739 $wpo_cache_options = $wpo_cache->config->get();
1740
1741 return $wpo_cache_options['auto_preload_purged_contents'];
1742 }
1743
1744 /**
1745 * Logging of interesting messages related to Cache Purging
1746 *
1747 * @param string $message
1748 * @param int $post_id
1749 */
1750 public function file_log($message, $post_id = 0) {
1751 if (!empty($post_id)) {
1752 if (false !== strpos($message, '{{URL}}')) {
1753 $message = str_replace('{{URL}}', get_permalink($post_id), $message);
1754 }
1755 if (false !== strpos($message, '{{title}}')) {
1756 $message = str_replace('{{title}}', '"' . get_the_title($post_id) . '"', $message);
1757 }
1758 }
1759 $this->file_logger->info($message);
1760 }
1761
1762 /**
1763 * Prunes the log file
1764 */
1765 public function prune_cache_logs() {
1766 $this->file_log("Pruning the Cache log file");
1767 $this->file_logger->prune_logs();
1768 }
1769
1770 /**
1771 * Update .htaccess file to serve cached files directly.
1772 *
1773 * @param bool $enable
1774 */
1775 public function update_serve_cache_rules_htaccess_section($enable) {
1776 $htaccess = new WP_Optimize_Htaccess();
1777
1778 $section_title = 'WP-Optimize rules to serve cached files';
1779
1780 if ($enable) {
1781 if (!$htaccess->is_commented_section_exists($section_title)) {
1782 $htaccess->update_commented_section($this->prepare_apache_cache_handle_section(), $section_title, true);
1783 $htaccess->write_file();
1784 }
1785 } elseif ($htaccess->is_commented_section_exists($section_title)) {
1786 $htaccess->remove_commented_section($section_title);
1787 $htaccess->write_file();
1788 }
1789
1790 }
1791
1792 /**
1793 * Get the .htaccess section with rules to serve cached files
1794 *
1795 * @return array
1796 */
1797 private function prepare_apache_cache_handle_section() {
1798
1799 $site_root = $this->get_site_root_path();
1800
1801 return array(
1802 array(
1803 '<IfModule mod_rewrite.c>',
1804 'RewriteEngine On',
1805 'RewriteCond %{REQUEST_METHOD} GET|HEAD',
1806 'RewriteCond %{QUERY_STRING} =""',
1807 'RewriteCond %{HTTP:Cookie} =""',
1808 'RewriteCond %{REQUEST_URI} !^/(wp-(?:admin|login|register|comments-post|cron|json))/ [NC]',
1809 'RewriteCond %{HTTP_USER_AGENT} !(android|blackberry|ipad|iphone|ipod|windows.phone|iEMobile|opera.mini|opera.mobile|webos|symbian|windows.mobile|kindle|googlebot.mobile) [NC]',
1810 'RewriteCond %{DOCUMENT_ROOT}'.$site_root.'wp-content/cache/wpo-cache/%{HTTP_HOST}%{REQUEST_URI}index.html -f',
1811 'RewriteRule ^(.*)$ '.$site_root.'wp-content/cache/wpo-cache/%{HTTP_HOST}%{REQUEST_URI}index.html [L]',
1812 array(
1813 '<IfModule mod_setenvif.c>',
1814 'SetEnvIf Request_URI "/wp-content/cache/wpo-cache/" FROM_CACHE=1',
1815 '</IfModule>',
1816 ),
1817 array(
1818 '<IfModule mod_headers.c>',
1819 'Header always set X-WP-Optimize-Cache-Header "Loaded from WP-Optimize cache" env=FROM_CACHE',
1820 '</IfModule>',
1821 ),
1822 '</IfModule>',
1823 ),
1824 );
1825 }
1826
1827 /**
1828 * Get relative site root path, if site is placed in the subdirectory then it returns /subdirectory otherwise /
1829 *
1830 * @param string $default
1831 *
1832 * @return string
1833 */
1834 private function get_site_root_path($default = '/') {
1835 $site_root = wp_parse_url(site_url());
1836 if (isset($site_root['path'])) {
1837 $site_root = trailingslashit($site_root['path']);
1838 } else {
1839 $site_root = $default;
1840 }
1841
1842 return $site_root;
1843 }
1844 }
1845
1846 endif;
1847