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

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