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

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