init(); } /** * When the plugin is activated. * * This runs on the 'activate_fastless-cache/fastless-cache.php' action. It adds or * updates the 'fastless_cache' option in the database for each site Fastless Cache * is activated on, creates the advanced-cache.php file in the wp-content * directory, and then maybe sets the WP_CACHE constant in the wp-config.php file. * * @param bool $network_wide True if the plugin was network activated, false otherwise. */ public static function on_activation($network_wide) { self::each_site($network_wide, self::class . '::update_backend'); Fastless_Cache_Disk::setup(); } /** * When the upgrader process is complete. * * This runs on the 'upgrader_process_complete' action. It clears the cache when * the core, themes, or plugins are updated. * * @param WP_Upgrader $upgrader Upgrader instance. * @param array $data Array of bulk item update data. */ public static function on_upgrade($upgrader, $data) { if ($data['action'] !== 'update') { return; } if ($data['type'] === 'core') { self::clear_complete_cache(); } if ($data['type'] === 'theme' && isset($data['themes'])) { $updated_themes = (array) $data['themes']; $sites_themes = self::each_site(is_multisite(), 'wp_get_theme'); foreach ($sites_themes as $blog_id => $site_theme) { // Clear the site cache if the active or parent theme has been updated. if (in_array($site_theme->stylesheet, $updated_themes, true) || in_array($site_theme->template, $updated_themes, true)) { self::clear_page_cache_by_site($blog_id); } } } if ($data['type'] === 'plugin' && isset($data['plugins'])) { $updated_plugins = (array) $data['plugins']; $network_plugins = is_multisite() ? array_flip((array) get_site_option('active_sitewide_plugins', array())) : array(); // Clear the complete cache if a network activated plugin has been updated. if (! empty(array_intersect($updated_plugins, $network_plugins))) { self::clear_complete_cache(); } else { $sites_plugins = self::each_site(is_multisite(), 'get_option', array('active_plugins', array())); foreach ($sites_plugins as $blog_id => $site_plugins) { // Clear the site cache if an activated plugin has been updated. if (! empty(array_intersect($updated_plugins, (array) $site_plugins))) { self::clear_page_cache_by_site($blog_id); } } } } } /** * When Fastless Cache is updated (deprecated). */ public static function on_fastless_cache_update() { self::each_site(is_multisite(), 'Fastless_Cache_Disk::clean'); Fastless_Cache_Disk::setup(); self::clear_complete_cache(); } /** * When the plugin is deactivated. * * This runs on the 'deactivate_fastless-cache/fastless-cache.php' action. It * deletes the settings and advanced-cache.php files and then maybe unsets the * WP_CACHE constant in the wp-config.php file. The site cache is then cleared and * the WP-Cron events unscheduled. * * @param bool $network_wide True if the plugin was network deactivated, false otherwise. */ public static function on_deactivation($network_wide) { self::each_site($network_wide, 'Fastless_Cache_Disk::clean'); self::each_site($network_wide, self::class . '::clear_site_cache', array(), true); self::each_site($network_wide, self::class . '::unschedule_events'); } /** * When the plugin is uninstalled. * * This runs on the 'uninstall_fastless-cache/fastless-cache.php' action. It deletes * the 'fastless_cache' option and plugin transients from the database for each * site in the installation and then removes everything the plugin created on * disk: the cache and settings directories, the advanced-cache.php drop-in, the * WP_CACHE constant and the plugin's .htaccess rules. */ public static function on_uninstall() { self::each_site(is_multisite(), self::class . '::uninstall_backend'); Fastless_Cache_Disk::uninstall(); } /** * When the cache is created or cleared. * * This runs on the 'fastless_cache_page_cache_created', * 'fastless_cache_site_cache_cleared', and 'fastless_cache_page_cache_cleared' * actions. It keeps the 'fastless_cache_cache_size' transient up to date. The * cache index count can only be greater than 1 when the cache has been cleared, * which can be the case for both cache cleared hooks. * * @param string $url Site or post URL. * @param int $id Blog or post ID * @param array $index Index of the cache created or cleared. */ public static function on_cache_created_cleared($url, $id, $index) { if (is_multisite() && ! wp_is_site_initialized(get_current_blog_id())) { return; } $current_cache_size = get_transient('fastless_cache_cache_size'); $current_cache_count = get_transient('fastless_cache_cache_count'); if (count($index) > 1) { if ($current_cache_size !== false) { // Prevent an incorrect cache size being built when the cache cleared index is not the entire site. delete_transient('fastless_cache_cache_size'); delete_transient('fastless_cache_cache_count'); } return; } // The changed cache size is negative when the cache is cleared. $changed_cache_size = array_sum(current($index)['versions']); $changed_cache_count = ($changed_cache_size > 0) ? 1 : -1; // Only keep stats that are already being tracked up to date. Recomputing // them here would walk the whole cache directory during a visitor // request; the dashboard recomputes them on demand when the transients // are missing (see self::get_cache_size() and self::get_cache_count()). if ($current_cache_size !== false) { $new_cache_size = $current_cache_size + $changed_cache_size; $new_cache_size = ($new_cache_size >= 0) ? $new_cache_size : 0; set_transient('fastless_cache_cache_size', $new_cache_size, DAY_IN_SECONDS); } if ($current_cache_count !== false) { $new_cache_count = (int) $current_cache_count + $changed_cache_count; $new_cache_count = ($new_cache_count >= 0) ? $new_cache_count : 0; set_transient('fastless_cache_cache_count', $new_cache_count, DAY_IN_SECONDS); } } /** * When a site's initialization routine should be executed. * * This runs on the 'wp_initialize_site' action. If the plugin is network * activated the 'fastless_cache' option will be added to the new site's database, * triggering the new site's settings file to be created. * * @param WP_Site $new_site New site instance. */ public static function install_later($new_site) { if (! is_plugin_active_for_network(FASTLESS_CACHE_BASE)) { return; } self::switch_to_blog((int) $new_site->blog_id); self::update_backend(); self::restore_current_blog(); } /** * Update the disk and backend requirements for the current site. * * This update process begins by first deleting the settings and * advanced-cache.php files and then maybe unsets the WP_CACHE constant in the * wp-config.php file. A new advanced-cache.php file is then created and the * WP_CACHE constant is maybe set. If a multisite network, the preceding actions * will only be done when the first site in the network is updated. Next, the * 'fastless_cache' option is updated in the database for the current site, which * triggers a new settings file to be created. Lastly, the site cache is cleared. */ public static function update() { self::update_disk(); self::update_backend(); self::clear_site_cache(); } /** * Add or update the backend requirements for the current site. * * This adds or updates the 'fastless_cache' option in the database, which triggers * the creation of the settings file. It will call self::on_update_backend() when * the plugin actions have not been registered as hooks yet, like when the plugin * is activated, but in this case even if the backend was not truly updated. * * @return array The new or current option value. */ public static function update_backend() { delete_metadata('user', 0, '_clear_post_cache_on_update', '', true); // < 1.5.0 $old_value = get_option('fastless-cache'); // < 1.5.0 if ($old_value !== false) { delete_option('fastless-cache'); add_option('fastless_cache', $old_value); } $old_value = get_option('fastless_cache', array()); $value = self::normalise_settings($old_value); // On a fresh install the `fastless_cache` option does not exist yet, so // the WP_CACHE consent check in Fastless_Cache_Disk::setup() would read // nothing and never enable the cache. Seed the option with sectioned // defaults (the shape Fastless_Cache_Options and the disk class read) // so the plugin works straight after activation. The flat engine shape // in $value is still used for the settings file. if (get_option('fastless_cache') === false && class_exists('Fastless_Cache_Settings_Schema')) { $sectioned_defaults = array(); foreach (Fastless_Cache_Settings_Schema::all() as $section => $fields) { $sectioned_defaults[$section] = Fastless_Cache_Settings_Schema::defaults($section); } add_option('fastless_cache', $sectioned_defaults); } // The settings version lives in its own option, so the sectioned // `fastless_cache` option is never rewritten in the flat engine shape. update_option('fastless_cache_settings_version', FASTLESS_CACHE_VERSION); if (has_action('update_option', array(__CLASS__, 'on_update_option')) === false) { self::on_update_backend('fastless_cache', $value); } return $value; } /** * Update the disk requirements for the current site. * * This deletes the settings and advanced-cache.php files and then maybe unsets * the WP_CACHE constant in the wp-config.php file. A new advanced-cache.php file * is then created and the WP_CACHE constant is maybe set. If a multisite network, * the 'fastless_cache_disk_updated' site transient is set afterward to only allow * this to be ran once. */ public static function update_disk() { if (is_multisite()) { if (get_site_transient('fastless_cache_disk_updated') !== FASTLESS_CACHE_VERSION) { self::each_site(true, 'Fastless_Cache_Disk::clean'); Fastless_Cache_Disk::setup(); set_site_transient('fastless_cache_disk_updated', FASTLESS_CACHE_VERSION, HOUR_IN_SECONDS); } } else { Fastless_Cache_Disk::clean(); Fastless_Cache_Disk::setup(); } } /** * When the backend is about to be updated. * * This runs when the 'fastless_cache' option is about to be added or updated in * the database. * * @param string $option Name of the option (for legacy reasons). * @param array $value The new option value. */ public static function on_update_backend($option, $value) { $settings = self::normalise_settings($value); // Recorded for the advanced-cache.php drop-in, which runs before // WooCommerce is loaded and so cannot call is_cart() and friends. $settings['uncacheable_paths'] = self::get_uncacheable_paths(); // Precompile the exclusion lists into ready-to-use forms so the cache // engine does not re-parse and re-quote them on every request. These are // derived values regenerated whenever the settings file is written; the // engine falls back to runtime compilation when they are absent. $page_paths = trim( (string) $settings['excluded_page_paths'] . "\n" . (string) $settings['uncacheable_paths'], "\n" ); $settings['excluded_page_paths_regex'] = Fastless_Cache_Engine::build_exclusion_regex( Fastless_Cache_Engine::parse_exclusion_list($page_paths) ); $settings['excluded_cookies_regex'] = Fastless_Cache_Engine::build_exclusion_regex(array_merge( array('wp-postpass*', 'wordpress_logged_in*', 'comment_author*'), Fastless_Cache_Engine::parse_exclusion_list((string) $settings['excluded_cookies']) )); $settings['excluded_query_strings_list'] = Fastless_Cache_Engine::parse_exclusion_list((string) $settings['excluded_query_strings']); $settings['included_query_strings_list'] = Fastless_Cache_Engine::parse_exclusion_list((string) $settings['included_query_strings']); $settings['included_cookie_strings_list'] = Fastless_Cache_Engine::parse_exclusion_list((string) $settings['included_cookie_strings']); // The delivered page's Content-Type charset, captured here because the // drop-in cannot call get_option() when it serves a cached page. $settings['blog_charset'] = function_exists('get_option') ? (string) get_option('blog_charset', 'UTF-8') : 'UTF-8'; // Elementor is not loaded when the drop-in serves a cached page, so the // decision to bypass preview requests is resolved here and stored. $settings['elementor_preview_bypass'] = ( class_exists('Fastless_Cache_Elementor') && Fastless_Cache_Elementor::is_active() && ! empty($settings['elementor_cache']) ); Fastless_Cache_Disk::create_settings_file($settings); self::unschedule_events(); } /** * Before an option is added. * * This runs on the 'add_option' action. * * @param string $option Name of the option to add. * @param mixed $value Value of the option. */ public static function on_add_option($option, $value) { if ($option === 'fastless_cache') { self::on_update_backend($option, $value); } } /** * Before an option value is updated. * * This runs on the 'update_option' action. * * @param string $option Name of the option to update. * @param mixed $old_value The old option value. * @param mixed $value The new option value. */ public static function on_update_option($option, $old_value, $value) { $options = array( // wp-admin/admin.php?page=fastless-cache 'fastless_cache', // wp-admin/options-general.php 'home', // wp-admin/options-reading.php 'page_on_front', 'page_for_posts', ); if (in_array($option, $options, true)) { if ($option === 'fastless_cache') { self::on_update_backend($option, $value); } else { self::clear_cache_on_option_save($option, $old_value, $value); } if ($option === 'home') { Fastless_Cache_Disk::delete_settings_file(); } } } /** * After the value of an option has been successfully updated. * * This runs on the 'updated_option' action. * * @param string $option Name of the updated option. * @param mixed $old_value The old option value. * @param mixed $value The new option value. */ public static function on_updated_option($option, $old_value, $value) { $options = array( // wp-admin/options-general.php 'blogname', 'blogdescription', 'WPLANG', 'timezone_string', 'gmt_offset', 'date_format', 'time_format', 'start_of_week', // wp-admin/options-reading.php 'page_on_front', 'page_for_posts', 'posts_per_page', 'blog_public', // wp-admin/options-discussion.php 'require_name_email', 'comment_registration', 'close_comments_for_old_posts', 'show_comments_cookies_opt_in', 'thread_comments', 'thread_comments_depth', 'page_comments', 'comments_per_page', 'default_comments_page', 'comment_order', 'show_avatars', 'avatar_rating', 'avatar_default', // wp-admin/options-permalink.php 'permalink_structure', 'category_base', 'tag_base', // wp-admin/themes.php 'template', 'stylesheet', // wp-admin/widgets.php 'sidebars_widgets', 'widget_*', // wp-admin/customize.php 'site_icon', // WooCommerce → Settings → Advanced (page setup) 'woocommerce_cart_page_id', 'woocommerce_checkout_page_id', 'woocommerce_myaccount_page_id', ); if (strpos($option, 'widget_') === 0) { $option = 'widget_*'; } if (in_array($option, $options, true)) { self::clear_cache_on_option_save($option, $old_value, $value); if ($option === 'permalink_structure') { self::update_backend(); } // Moving the cart, checkout or my-account page changes the paths the // pre-WordPress drop-in bypasses, so the settings file is refreshed. if (in_array($option, self::get_woocommerce_page_options(), true)) { self::on_update_backend('fastless_cache', self::get_settings(false)); } } } /** * The WooCommerce options that hold the transactional page IDs. * * @return string[] Option names. */ private static function get_woocommerce_page_options() { return array( 'woocommerce_cart_page_id', 'woocommerce_checkout_page_id', 'woocommerce_myaccount_page_id', ); } /** * The URL paths of the WooCommerce cart, checkout and my-account pages. * * These are written into the settings file so the advanced-cache.php drop-in * can bypass them before WordPress — and therefore WooCommerce — is loaded. * * @return string One path per line, empty when WooCommerce is not active. */ private static function get_uncacheable_paths() { if (! function_exists('wc_get_page_id') || ! function_exists('get_permalink')) { return ''; } $paths = array(); foreach (array('cart', 'checkout', 'myaccount') as $page) { $page_id = (int) wc_get_page_id($page); if ($page_id <= 0) { continue; } $permalink = get_permalink($page_id); if (! is_string($permalink) || '' === $permalink) { continue; } $path = wp_parse_url($permalink, PHP_URL_PATH); if (is_string($path) && '' !== $path) { $paths[] = $path; } } return implode("\n", array_unique($paths)); } /** * When a site's uninitialization routine should be executed. * * This runs on the 'wp_uninitialize_site' action. This deletes the settings file * and then clears the site cache for the deleted site. The advanced-cache.php * file will also be deleted and the WP_CACHE constant maybe unset if it was the * only site that had Fastless Cache activated. * * @param WP_Site $old_site Deleted site instance. */ public static function uninstall_later($old_site) { Fastless_Cache_Disk::clean(); self::clear_page_cache_by_site((int) $old_site->blog_id); } /** * Uninstall backend requirements. * * This deletes the 'fastless_cache' option, the settings version stamp, * every 'fastless_cache_*' transient (the per-user cache cleared notices * never expire, so they cannot be left behind) and the * 'fastless_cache_disk_updated' site transient. */ public static function uninstall_backend() { global $wpdb; delete_option('fastless_cache'); delete_option('fastless_cache_settings_version'); delete_transient('fastless_cache_cache_size'); delete_transient('fastless_cache_cache_count'); delete_transient(self::get_cache_cleared_transient_name()); delete_site_transient('fastless_cache_disk_updated'); if (! isset($wpdb) || ! is_object($wpdb)) { return; } // Sweep up any remaining 'fastless_cache_*' transients, such as the // cache cleared notices of users other than the one uninstalling. $transients = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- deliberate direct query: cache maintenance must read live data; a persistent cache layer here would serve stale results. $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", '_transient_fastless_cache_%', '_transient_timeout_fastless_cache_%' ) ); if (! is_array($transients)) { return; } foreach ($transients as $transient) { delete_transient(preg_replace('/^_transient_(timeout_)?/', '', $transient)); } } /** * Enter each site and call a callback with an array of parameters. * * This assumes that the callback function exists on the site being entered. It * will not perform the callback or restart the cache engine on sites that do not * have Fastless Cache active, unless it is a must-use plugin or it is being * activated or uninstalled. * * @param bool $sites Whether to enter all sites or the current site. * @param string $callback Callback function. * @param array $callback_params (Optional) Callback function parameters. Default empty array. * @param bool $restart_engine (Optional) Whether to restart the cache engine. Default false. * @return array An array of callback returns with blog IDs as the keys. */ private static function each_site($sites, $callback, $callback_params = array(), $restart_engine = false) { $blog_ids = $sites ? self::get_blog_ids() : array(get_current_blog_id()); $last_blog_id = end($blog_ids); $skip_active_check = ! self::is_fastless_cache_active(); $callback_return = array(); foreach ($blog_ids as $blog_id) { self::switch_to_blog($blog_id, $restart_engine, $skip_active_check); if ($skip_active_check || self::is_fastless_cache_active()) { $callback_return[$blog_id] = call_user_func_array($callback, $callback_params); } $_restart_engine = ($restart_engine && $blog_id === $last_blog_id) ? true : false; self::restore_current_blog($_restart_engine, $skip_active_check); } return $callback_return; } /** * Switch the current blog. * * This is a wrapper for switch_to_blog() that can restart the cache engine, * allowing the correct site data to be picked up after the switch. * * @param int $blog_id The ID of the blog to switch to. * @param bool $restart_engine (Optional) Whether to restart the cache engine after the switch. Default false. * @param bool $force_restart (Optional) Whether to force restart the cache engine. Default false. * @return bool True if the current blog was switched, false otherwise. */ public static function switch_to_blog($blog_id, $restart_engine = false, $force_restart = false) { if (! is_multisite() || $blog_id === get_current_blog_id()) { return false; } switch_to_blog($blog_id); if (($force_restart || self::is_fastless_cache_active()) && $restart_engine) { Fastless_Cache_Engine::start(); } return true; } /** * Restore the current blog after switching. * * This is a wrapper for restore_current_blog() that can restart the cache engine, * allowing the correct site data to be picked up after the switch. * * @param bool $restart_engine (Optional) Whether to restart the cache engine after the switch. Default false. * @param bool $force_restart (Optional) Whether to force restart the cache engine. Default false. * @return bool True if the current blog was restored, false otherwise. */ public static function restore_current_blog($restart_engine = false, $force_restart = false) { if (! is_multisite() || ! ms_is_switched()) { return false; } restore_current_blog(); if (($force_restart || self::is_fastless_cache_active()) && $restart_engine) { Fastless_Cache_Engine::start(true); } return true; } /** * Whether Fastless Cache is active. * * This checks if Fastless Cache is in the active plugins list. It will not be in * that list when installed as a must-use plugin. This copies is_plugin_active(). * That function is not being used directly because of its availability. * * @return bool True if Fastless Cache is in the active plugins list, false if not. */ private static function is_fastless_cache_active() { if (in_array(FASTLESS_CACHE_BASE, (array) get_option('active_plugins', array()), true)) { return true; } if (! is_multisite()) { return false; } $plugins = get_site_option('active_sitewide_plugins'); if (isset($plugins[FASTLESS_CACHE_BASE])) { return true; } return false; } /** * Whether the page cache is actually running. * * True only when WordPress is set to load the cache (WP_CACHE) and the * advanced-cache.php drop-in that serves cached pages is present. This is * the signal behind the dashboard's "cache is running" indicator. * * @return bool True when the page cache is armed, false otherwise. */ public static function is_cache_active() { if (! defined('WP_CACHE') || ! WP_CACHE) { return false; } if (! defined('WP_CONTENT_DIR')) { return false; } return file_exists(WP_CONTENT_DIR . '/advanced-cache.php'); } /** * After a plugin has been activated or deactivated. * * This runs on the 'activated_plugin' and 'deactivated_plugin' actions. * */ public static function on_plugin_activation_deactivation() { // A plugin (de)activation can change what the pre-WordPress drop-in must // bypass and whether Elementor is present, so refresh the derived values // in the settings file (uncacheable_paths, elementor_preview_bypass). self::update_backend(); if (Fastless_Cache_Engine::$settings['clear_site_cache_on_changed_plugin']) { self::clear_site_cache(); } } /** * Get the plugin settings from the database for the current site. * * This can update the disk and backend requirements and then clear the site * cache if the settings do not exist or are outdated. If that occurs, the * settings after the update will be returned. * * @param bool $update Whether to update the disk and backend requirements if the settings are * outdated. Default true. * @return array|bool Plugin settings from the database, false if settings do not exist and update * was skipped or failed. */ public static function get_settings($update = true) { if ($update && get_option('fastless_cache_settings_version') !== FASTLESS_CACHE_VERSION) { self::update(); } return self::normalise_settings(get_option('fastless_cache', array())); } /** * Flatten sectioned settings into the flat shape the cache engine uses. * * The `fastless_cache` option is stored as an array keyed by section (see * Fastless_Cache_Options), while the engine and the settings file use flat * top-level keys. Anything already flat passes through; when both shapes * are present the sectioned values win, so a value written by the admin * panel supersedes a leftover flat key from an earlier build. * * @param mixed $settings Settings, sectioned or flat. * @return array Flat settings. */ private static function flatten_settings($settings) { if (! is_array($settings)) { return array(); } // Derived from the schema so a section added to (or split out of) the // admin panel can never be silently dropped here again. The static // fallback covers the case where the admin subsystem is unavailable. $sections = class_exists('Fastless_Cache_Settings_Schema') ? array_keys(Fastless_Cache_Settings_Schema::all()) : array('general', 'clearing', 'exclusions', 'query_strings', 'cookies', 'tools'); $flat = array(); foreach ($settings as $key => $value) { if (! in_array($key, $sections, true)) { $flat[$key] = $value; } } foreach ($sections as $section) { if (isset($settings[$section]) && is_array($settings[$section])) { $flat = array_merge($flat, $settings[$section]); } } return $flat; } /** * Normalise settings into the flat shape stored in the settings file and * read by the cache engine, filling in every default. * * @param mixed $settings Settings, sectioned or flat. * @return array Flat settings merged over the defaults. */ private static function normalise_settings($settings) { $normalised = wp_parse_args(self::flatten_settings($settings), self::get_default_settings()); // System settings are computed at runtime and must never be taken from a // stale stored value: a leftover flat 'version' key (from an older build) // would otherwise override the plugin version and mark the settings file // as permanently outdated, so the cache engine would never start. return wp_parse_args(self::get_default_system_settings(), $normalised); } /** * Get the blog ID for the current site or of a given site. * * @param WP_Site|int|string $site (Optional) Site instance or site blog ID. Default is the current site. * @return int The blog ID or 0 if not found. */ private static function get_blog_id($site = null) { if (empty($site)) { return get_current_blog_id(); } if ($site instanceof WP_Site) { return (int) $site->blog_id; } if (is_numeric($site)) { $blog_id = (int) $site; if (in_array($blog_id, self::get_blog_ids(), true)) { return $blog_id; } } return 0; } /** * Get the blog IDs. * * @global wpdb $wpdb WordPress database abstraction object. * * @return int[] Blog IDs. */ private static function get_blog_ids() { if (is_multisite()) { global $wpdb; $blog_ids = array_map('absint', $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs")); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- deliberate direct query: cache maintenance must read live data; a persistent cache layer here would serve stale results. } else { $blog_ids = array(1); } return $blog_ids; } /** * Get the blog path for the current site. * * This gets the end part of the URL in case the installation is in a nested * subdirectory. An empty string is being returned instead of '/' as WordPress * does because it simplifies checking the blog path in * self::get_root_blog_exclusions(). * * @return string Blog path from site address URL (with leading and trailing slashes), empty * string if not found. */ public static function get_blog_path() { $site_url_path = (string) wp_parse_url(home_url(), PHP_URL_PATH); $site_url_path_pieces = explode('/', trim($site_url_path, '/')); $blog_path = end($site_url_path_pieces); $blog_path = (! empty($blog_path)) ? '/' . $blog_path . '/' : ''; return $blog_path; } /** * Get the blog path from a given URL. * * @return string Blog path from URL (with leading and trailing slashes), '/' if not found. */ public static function get_blog_path_from_url($url) { $url_path = (string) wp_parse_url($url, PHP_URL_PATH); $url_path_pieces = explode('/', trim($url_path, '/')); $blog_path = '/'; $blog_paths = self::get_blog_paths(); foreach ($url_path_pieces as $url_path_piece) { $url_path_piece = '/' . $url_path_piece . '/'; if (in_array($url_path_piece, $blog_paths, true)) { $blog_path = $url_path_piece; break; } } return $blog_path; } /** * Get the blog paths. * * @global wpdb $wpdb WordPress database abstraction object. * * @return string[] Blog paths. */ public static function get_blog_paths() { if (is_multisite()) { global $wpdb; $blog_paths = $wpdb->get_col("SELECT path FROM $wpdb->blogs"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- deliberate direct query: cache maintenance must read live data; a persistent cache layer here would serve stale results. } else { $blog_paths = array('/'); } return $blog_paths; } /** * Get the WP-Cron events. * * @return string[] An array of events with action hooks as the keys and recurrences as the values. */ private static function get_events() { $events = array('fastless_cache_clear_expired_cache' => 'hourly'); return $events; } /** * Get the permalink structure (deprecated). * */ private static function get_permalink_structure() { $permalink_structure = get_option('permalink_structure'); if ($permalink_structure && preg_match('/\/$/', $permalink_structure)) { return 'has_trailing_slash'; } if ($permalink_structure && ! preg_match('/\/$/', $permalink_structure)) { return 'no_trailing_slash'; } if (empty($permalink_structure)) { return 'plain'; } } /** * Get the cache index for the current site. * * @return array[] Cache index from the disk. */ public static function get_cache_index() { $args['subpages']['exclude'] = self::get_root_blog_exclusions(); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams -- filesystem cache-index exclusion array, not a WP_Query argument. $cache = Fastless_Cache_Disk::cache_iterator(home_url(), $args); $cache_index = $cache['index']; return $cache_index; } public static function count_cache_pages($path) { $count = 0; $iterator = new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS); foreach ($iterator as $fileInfo) { // If it's a directory, recurse into it if ($fileInfo->isDir()) { $count += self::count_cache_pages($fileInfo->getPathname()); } // If it's a file, increment the count elseif ($fileInfo->isFile()) { $count++; } } return $count; } public static function get_cache_count() { $cache_size = get_transient('fastless_cache_cache_count'); if ($cache_size === false) { $url = home_url(); $url = esc_url_raw($url, array('http', 'https')); $cache_dir = Fastless_Cache_Disk::get_cache_dir($url) . '/'; if (! is_dir($cache_dir)) { return 0; } $cache_size = self::count_cache_pages($cache_dir); set_transient('fastless_cache_cache_count', $cache_size, DAY_IN_SECONDS); } return $cache_size; } /** * Get the cache size for the current site. * * This sets the 'fastless_cache_cache_size' transient in the database when the * cache size is retrieved from the disk. * * @return int Cache size in bytes, either from the database or disk. */ public static function get_cache_size() { $cache_size = get_transient('fastless_cache_cache_size'); if ($cache_size === false) { $args['subpages']['exclude'] = self::get_root_blog_exclusions(); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams -- filesystem cache-index exclusion array, not a WP_Query argument. $cache = Fastless_Cache_Disk::cache_iterator(home_url(), $args); $cache_size = $cache['size']; set_transient('fastless_cache_cache_size', $cache_size, DAY_IN_SECONDS); } return $cache_size; } /** * Get the name of the transient that is used in the cache clear notice. * * @return string Name of the transient. */ private static function get_cache_cleared_transient_name() { $transient_name = 'fastless_cache_cache_cleared_' . get_current_user_id(); return $transient_name; } /** * Get the default plugin settings. * * @param string $settings_type (Optional) The default plugin 'system' or 'user' settings, all default plugin * settings otherwise. * @return array Default plugin settings. */ private static function get_default_settings($settings_type = '') { switch ($settings_type) { case 'system': return self::get_default_system_settings(); case 'user': return self::get_default_user_settings(); default: return wp_parse_args(self::get_default_user_settings(), self::get_default_system_settings()); } } /** * Get the default plugin system settings. * * @return array Default plugin system settings. */ private static function get_default_system_settings() { $default_system_settings = array( 'version' => (string) FASTLESS_CACHE_VERSION, 'use_trailing_slashes' => (int) (substr(get_option('permalink_structure'), -1, 1) === '/'), 'permalink_structure' => (string) self::get_permalink_structure(), ); return $default_system_settings; } /** * Get the default plugin user settings. * * @return array Default plugin user settings. */ private static function get_default_user_settings() { $default_user_settings = array( 'cache_expires' => 0, 'cache_expiry_time' => 0, 'clear_site_cache_on_saved_post' => 0, 'clear_site_cache_on_saved_comment' => 0, 'clear_site_cache_on_saved_term' => 0, 'clear_site_cache_on_saved_user' => 0, 'clear_site_cache_on_changed_plugin' => 0, 'mobile_cache' => 0, 'compress_cache' => (int) Fastless_Cache_Disk::supports_gzip(), 'minify_html' => 0, 'excluded_post_ids' => '', 'excluded_page_paths' => '', 'excluded_query_strings' => '', 'query_cache' => 0, 'cookie_cache' => 0, 'excluded_cookies' => '', 'included_query_strings' => '', 'included_cookie_strings' => '', 'woocommerce_cart_cache' => 0, 'browser_cache_max_age' => 0, 'elementor_cache' => 1, 'excluded_page_paths_detailed' => '', 'clear_site_cache_on_saved_settings' => 0, 'disable_footprint' => 0 ); return $default_user_settings; } /** * Get the subpages that do not belong to the root blog in a subdirectory network. * * @return string[] Blog paths to the other sites in a network if the current site is the root blog * in a subdirectory network, empty otherwise. */ private static function get_root_blog_exclusions() { if (! is_multisite() || is_subdomain_install()) { return array(); } $current_blog_path = self::get_blog_path(); $network_blog_paths = self::get_blog_paths(); if (! in_array($current_blog_path, $network_blog_paths, true)) { return $network_blog_paths; } return array(); } /** * Add the plugin action links in the plugins list table. * * This runs on the 'plugin_action_links_fastless-cache/fastless-cache.php' action. * * @param string[] $action_links Action links. * @return string[] The action links after maybe being updated. */ public static function add_plugin_action_links($action_links) { if (! current_user_can('manage_options')) { return $action_links; } array_unshift($action_links, sprintf( '%s', admin_url('admin.php?page=' . Fastless_Cache_Admin::SLUG), esc_html__('Settings', 'fastless-cache') )); return $action_links; } /** * Add the plugin metadata in the plugins list table. * * This runs on the 'plugin_row_meta' action. * * @param string[] $plugin_meta An array of the plugin's metadata, including the version, author, author URI, * and plugin URI. * @param string $plugin_file Path to the plugin file relative to the plugins directory. * @return string[] An array of the plugin's metadata after maybe being updated. */ public static function add_plugin_row_meta($plugin_meta, $plugin_file) { if ($plugin_file !== FASTLESS_CACHE_BASE) { return $plugin_meta; } $plugin_meta = wp_parse_args( array( '' . esc_html__('Documentation', 'fastless-cache') . '', ), $plugin_meta ); $plugin_meta = wp_parse_args( array( 'Sponsor', ), $plugin_meta ); return $plugin_meta; } /** * Add the admin bar items. * * This runs on the 'admin_bar_menu' action. It adds the clear cache buttons to * the admin bar. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance, passed by reference. */ public static function add_admin_bar_items($wp_admin_bar) { if (! self::user_can_clear_cache()) { return; } // Add top-level Fastless Cache menu $wp_admin_bar->add_menu( array( 'id' => 'fastless_cache', 'href' => admin_url('admin.php?page=' . Fastless_Cache_Admin::SLUG), 'title' => '' . esc_html__('Fastless Cache', 'fastless-cache') . '', 'meta' => array('title' => esc_html__('Fastless Cache Settings', 'fastless-cache')), ) ); // Add Clear Site/Network Cache as first sub-item $title = (is_multisite() && is_network_admin()) ? esc_html__('Clear Network Cache', 'fastless-cache') : esc_html__('Clear Site Cache', 'fastless-cache'); $wp_admin_bar->add_menu( array( 'id' => 'fastless_cache_clear_cache', 'href' => wp_nonce_url(add_query_arg(array( '_cache' => 'fastless-cache', '_action' => 'clear', )), 'fastless_cache_clear_cache_nonce'), 'parent' => 'fastless_cache', 'title' => '' . $title . '', 'meta' => array('title' => $title), ) ); // Add Clear Page Cache as second sub-item (if not in admin) if (! is_admin()) { $wp_admin_bar->add_menu( array( 'id' => 'fastless_cache_clear_page_cache', 'href' => wp_nonce_url(add_query_arg(array( '_cache' => 'fastless-cache', '_action' => 'clearurl', )), 'fastless_cache_clear_cache_nonce'), 'parent' => 'fastless_cache', 'title' => '' . esc_html__('Clear Page Cache', 'fastless-cache') . '', 'meta' => array('title' => esc_html__('Clear Page Cache', 'fastless-cache')), ) ); } } /** * Whether the current user can clear the cache. * * @return bool True if the current user can clear the cache, false otherwise. */ private static function user_can_clear_cache() { /** * Filters whether the current user can clear the cache. * * @since 1.6.0 * * @param bool $can_clear_cache Whether the current user can clear the cache. Default is whether the current * user has the 'manage_options' capability. */ $can_clear_cache = apply_filters('fastless_cache_user_can_clear_cache', current_user_can('manage_options')); $can_clear_cache = apply_filters_deprecated('user_can_clear_cache', array($can_clear_cache), '1.6.0', 'fastless_cache_user_can_clear_cache'); return $can_clear_cache; } public static function process_clear_cache_asset_manager() { self::each_site((is_multisite() && is_network_admin()), self::class . '::clear_site_cache', array(), true); if (is_admin()) { set_transient(self::get_cache_cleared_transient_name(), 1); } } /** * Process a clear cache request. * * This runs on the 'init' action. It clears the cache when a clear cache button * is clicked in the admin bar. * */ public static function process_clear_cache_request() { if (empty($_GET['_cache']) || empty($_GET['_action']) || $_GET['_cache'] !== 'fastless-cache' || ($_GET['_action'] !== 'clear' && $_GET['_action'] !== 'clearurl')) { return; } if (empty($_GET['_wpnonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'fastless_cache_clear_cache_nonce')) { return; } if (! self::user_can_clear_cache()) { return; } if ($_GET['_action'] === 'clearurl') { $request_uri = isset($_SERVER['REQUEST_URI']) ? Fastless_Cache_Engine::wp_unslash_safe($_SERVER['REQUEST_URI']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- uses Fastless_Cache_Engine::wp_unslash_safe() which calls wp_unslash() when available, stripslashes() in early bootstrap. $request_uri = Fastless_Cache_Engine::validate_request_uri($request_uri); if ('' !== $request_uri) { self::clear_page_cache_by_url(Fastless_Cache_Engine::$request_headers['Host'] . $request_uri); } } elseif ($_GET['_action'] === 'clear') { self::each_site((is_multisite() && is_network_admin()), self::class . '::clear_site_cache', array(), true); } // Redirect to the same page. wp_safe_redirect(remove_query_arg(array('_cache', '_action', '_wpnonce'))); if (is_admin()) { set_transient(self::get_cache_cleared_transient_name(), 1); } exit; } /** * Display an admin notice after the cache has been cleared. * * This runs on the 'admin_notices' action. Only shown on our own settings screen * to comply with Guideline 11. * */ public static function cache_cleared_notice() { if (! self::user_can_clear_cache()) { return; } // Only show on our own settings screen. $screen = get_current_screen(); if (! $screen || strpos($screen->id, 'fastless-cache') === false) { return; } if (get_transient(self::get_cache_cleared_transient_name())) { printf( '

%s

', (is_multisite() && is_network_admin()) ? esc_html__('Network cache cleared.', 'fastless-cache') : esc_html__('Site cache cleared.', 'fastless-cache') ); delete_transient(self::get_cache_cleared_transient_name()); } } /** * When a post has been saved or before it is sent to the trash. * * This runs on the 'save_post' and 'wp_trash_post' actions. It will clear the cache * when any published post type has been created, updated, or about to be trashed. * * @param int $post_id Post ID. */ public static function on_save_trash_post($post_id) { $post_status = get_post_status($post_id); if ($post_status === 'publish') { self::clear_cache_on_post_save($post_id); } } /** * Before an existing post is updated in the database. * * This runs on the 'pre_post_update' action. It will clear the cache when any * published post type is about to be updated but not trashed. * * @param int $post_id Post ID. * @param array $post_data Array of unslashed post data. */ public static function on_pre_post_update($post_id, $post_data) { $old_post_status = get_post_status($post_id); $new_post_status = $post_data['post_status']; if ($old_post_status === 'publish' && $new_post_status !== 'trash') { self::clear_cache_on_post_save($post_id); } } /** * After a comment is inserted into the database. * * This runs on the 'comment_post' action. It will clear the cache when a new * approved comment is posted. * * @param int $comment_id Comment ID. * @param int|string $comment_approved 1 if the comment is approved, 0 if not, 'spam' if spam. */ public static function on_comment_post($comment_id, $comment_approved) { if ($comment_approved === 1) { self::clear_cache_on_comment_save($comment_id); } } /** * After a comment is updated in the database. * * This runs on the 'edit_comment' action. It will clear the cache when an * approved comment is edited. * * @param int $comment_id Comment ID. * @param array $comment_data Comment data. */ public static function on_edit_comment($comment_id, $comment_data) { $comment_approved = (int) $comment_data['comment_approved']; if ($comment_approved === 1) { self::clear_cache_on_comment_save($comment_id); } } /** * When the comment status is in transition. * * This runs on the 'transition_comment_status' action. It will clear the cache * when a comment's status has changed from or to 'approved'. * * @param int|string $new_status The new comment status. * @param int|string $old_status The old comment status. * @param WP_Comment $comment Comment instance. */ public static function on_transition_comment_status($new_status, $old_status, $comment) { if ($old_status === 'approved' || $new_status === 'approved') { self::clear_cache_on_comment_save($comment); } } /** * Before the given terms are edited. * * This runs on the 'edit_terms' action. It will clear the cache before a term is * updated in the database and its taxonomy is viewable. * * @param int $term_id Term ID * @param string $taxonomy Taxonomy name that `$term_id` is part of. */ public static function on_edit_terms($term_id, $taxonomy) { if (is_taxonomy_viewable($taxonomy)) { self::clear_cache_on_term_save($term_id, $taxonomy); } } /** * After a term has been saved or deleted and the term cache has been cleaned. * * This runs on the 'saved_term' and 'delete_term' actions. It will clear the * cache after a term has been updated or deleted from the database and its * taxonomy is viewable. * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy name that `$term_id` is part of. */ public static function on_saved_delete_term($term_id, $tt_id, $taxonomy) { if (is_taxonomy_viewable($taxonomy)) { self::clear_cache_on_term_save($term_id, $taxonomy); } } /** * After a user is registered or updated and before a user is deleted. * * This runs on the 'user_register', 'profile_update', and 'delete_user' actions. * It will clear the cache after a new user is registered or an existing user is * updated, and before a user is deleted from the database. * * @param int $user_id ID of the newly registered, updated, or about to be deleted user. */ public static function on_register_update_delete_user($user_id) { self::clear_cache_on_user_save($user_id); } /** * After a user is deleted from the database. * * This runs on the 'deleted_user' action. It will clear the cache after a user is * deleted from the database and the old posts of that user were reassigned. * * @param int $user_id ID of the deleted user. * @param int|null $reassign ID of the user reassigned to the old posts of `$user_id`. */ public static function on_deleted_user($user_id, $reassign) { if ($reassign) { self::clear_cache_on_user_save($reassign); } } /** * When the WooCommerce stock is updated. * * This runs on the 'woocommerce_product_set_stock', * 'woocommerce_variation_set_stock', 'woocommerce_product_set_stock_status', and * 'woocommerce_variation_set_stock_status' actions. It will clear the cache after * a product's stock is updated. * * @param WC_Product|int $product Product instance or product ID. */ public static function on_woocommerce_stock_update($product) { if (is_int($product)) { $product_id = $product; } else { $product_id = $product->get_id(); } self::clear_cache_on_post_save($product_id); } /** * Clear the site cache of a single site or all sites in a multisite network. */ public static function clear_complete_cache() { self::each_site(is_multisite(), self::class . '::clear_site_cache', array(), true); } /** * Clear the complete cache (deprecated). * */ public static function clear_total_cache() { self::clear_complete_cache(); } /** * Clear the site cache for the current site or of a given site. * * * @param WP_Site|int|string $site (Optional) Site instance or site blog ID. Default is the current site. */ public static function clear_site_cache($site = null) { self::clear_page_cache_by_site($site); // The preload queue, status and log files live beside the cache, not // inside it, so the site clear must drop them explicitly: the panel // must not keep showing the last run's numbers over an empty cache. Fastless_Cache_Preload::clear_state(); } /** * Clear the expired cache for the current site or of a given site. * * @param WP_Site|int|string $site (Optional) Site instance or site blog ID. Default is the current site. */ public static function clear_expired_cache($site = null) { $args['expired'] = 1; $args['hooks']['include'] = 'fastless_cache_page_cache_cleared'; self::clear_page_cache_by_site($site, $args); } /** * Clear the post cache for the current post or of a given post. * * @param WP_Post|int|string $post (Optional) Post instance or post ID. Default is the current post if set. */ public static function clear_post_cache($post = null) { $post = get_post($post); if ($post instanceof WP_Post) { self::clear_page_cache_by_post($post, 'pagination'); self::clear_post_type_archive_cache($post); self::clear_post_terms_archives_cache($post); if ($post->post_type === 'post') { self::clear_post_author_archive_cache($post); self::clear_post_date_archives_cache($post); } } } /** * Clear the comment cache for the current comment or of a given comment. * * @param WP_Comment|int|string $comment (Optional) Comment instance or comment ID. Default is the current comment if set. */ public static function clear_comment_cache($comment = null) { $comment = get_comment($comment); if ($comment instanceof WP_Comment) { self::clear_page_cache_by_comment($comment, 'pagination'); } } /** * Clear the term cache of a given term. * * @param WP_Term|int $term Term instance or term ID. * @param string $taxonomy (Optional) Taxonomy name that `$term` is part of. Default empty string. */ public static function clear_term_cache($term, $taxonomy = '') { $term = get_term($term, $taxonomy); if ($term instanceof WP_Term) { self::clear_page_cache_by_term($term, '', 'pagination'); self::clear_term_archive_cache($term); if (is_taxonomy_hierarchical($term->taxonomy)) { self::clear_term_children_archives_cache($term); self::clear_term_parents_archives_cache($term); } } } /** * Clear the user cache for the current user or of a given user. * * @param WP_User|int|string $user (Optional) User instance or user ID. Default is the current user if logged in. */ public static function clear_user_cache($user = null) { if (empty($user)) { $user = wp_get_current_user(); } elseif (is_numeric($user)) { $user = get_userdata($user); } if ($user instanceof WP_User) { self::clear_page_cache_by_user($user, 'pagination'); self::clear_author_archive_cache($user); } } /** * Clear the cache for pages associated with a new or updated post (deprecated). */ public static function clear_associated_cache($post) { self::clear_post_type_archive_cache($post); self::clear_post_terms_archives_cache($post); if ($post->post_type === 'post') { self::clear_post_author_archive_cache($post); self::clear_post_date_archives_cache($post); } } /** * Clear the post type archives page cache (deprecated). * */ public static function clear_post_type_archives_cache($post_type) { $post_type_archives_url = get_post_type_archive_link($post_type); if (! empty($post_type_archives_url)) { self::clear_page_cache_by_url($post_type_archives_url, 'pagination'); } } /** * Clear the post type archive cache for the current post or of a given post. * * @param WP_Post|int|string $post (Optional) Post instance or post ID. Default is the current post if set. */ public static function clear_post_type_archive_cache($post = null) { $post = get_post($post); if ($post instanceof WP_Post) { $post_type_archive_url = get_post_type_archive_link($post->post_type); if ($post_type_archive_url !== false && strpos($post_type_archive_url, '?') === false) { self::clear_page_cache_by_url($post_type_archive_url, 'pagination'); } } } /** * Clear the post terms archives cache for the current post or of a given post. * * @param WP_Post|int|string $post (Optional) Post instance or post ID. Default is the current post if set. */ public static function clear_post_terms_archives_cache($post = null) { $post = get_post($post); if ($post instanceof WP_Post) { $terms = wp_get_post_terms($post->ID, get_taxonomies()); if (is_array($terms)) { foreach ($terms as $term) { self::clear_term_archive_cache($term); if (is_taxonomy_hierarchical($term->taxonomy)) { self::clear_term_parents_archives_cache($term); // Post can be in the term's parents' archives. } } } } } /** * Clear the post author archive cache for the current post or of a given post. * * @param WP_Post|int|string $post (Optional) Post instance or post ID. Default is the current post if set. */ public static function clear_post_author_archive_cache($post = null) { $post = get_post($post); if ($post instanceof WP_Post) { self::clear_author_archive_cache((int) $post->post_author); } } /** * Clear the post date archives cache for the current post or of a given post. * * @param WP_Post|int|string $post (Optional) Post instance or post ID. Default is the current post if set. */ public static function clear_post_date_archives_cache($post = null) { $post = get_post($post); if ($post instanceof WP_Post) { $date_archive_day = get_the_date('d', $post); $date_archive_month = get_the_date('m', $post); $date_archive_year = get_the_date('Y', $post); $date_archive_urls[] = get_day_link($date_archive_year, $date_archive_month, $date_archive_day); $date_archive_urls[] = get_month_link($date_archive_year, $date_archive_month); $date_archive_urls[] = get_year_link($date_archive_year); foreach ($date_archive_urls as $date_archive_url) { if (strpos($date_archive_url, '?') === false) { self::clear_page_cache_by_url($date_archive_url, 'pagination'); } } } } /** * Clear the taxonomies archives cache by post ID (deprecated). */ public static function clear_taxonomies_archives_cache_by_post_id($post_id) { self::clear_post_terms_archives_cache($post_id); } /** * Clear the author archives page cache by user ID (deprecated). */ public static function clear_author_archives_cache_by_user_id($user_id) { self::clear_author_archive_cache($user_id); } /** * Clear the date archives cache by post ID (deprecated). */ public static function clear_date_archives_cache_by_post_id($post_id) { self::clear_post_date_archives_cache($post_id); } /** * Clear the term archive cache of a given term. * * @param WP_Term|int $term Term instance or term ID. * @param string $taxonomy (Optional) Taxonomy name that `$term` is part of. Default empty string. */ public static function clear_term_archive_cache($term, $taxonomy = '') { $term = get_term($term, $taxonomy); if ($term instanceof WP_Term) { if (! is_taxonomy_viewable($term->taxonomy)) { return; // Term archive cache does not exist. } $term_archive_url = get_term_link($term); if (! is_wp_error($term_archive_url) && strpos($term_archive_url, '?') === false) { self::clear_page_cache_by_url($term_archive_url, 'pagination'); } } } /** * Clear the term children archives cache of a given term. * * @param WP_Term|int $term Term instance or term ID. * @param string $taxonomy (Optional) Taxonomy name that `$term` is part of. Default empty string. */ public static function clear_term_children_archives_cache($term, $taxonomy = '') { $term = get_term($term, $taxonomy); if ($term instanceof WP_Term) { $child_ids = get_term_children($term->term_id, $term->taxonomy); if (is_array($child_ids)) { foreach ($child_ids as $child_id) { self::clear_term_archive_cache($child_id, $term->taxonomy); } } } } /** * Clear the term parents archives cache of a given term. * * @param WP_Term|int $term Term instance or term ID. * @param string $taxonomy (Optional) Taxonomy name that `$term` is part of. Default empty string. */ public static function clear_term_parents_archives_cache($term, $taxonomy = '') { $term = get_term($term, $taxonomy); if ($term instanceof WP_Term) { $parent_ids = get_ancestors($term->term_id, $term->taxonomy, 'taxonomy'); foreach ($parent_ids as $parent_id) { self::clear_term_archive_cache($parent_id, $term->taxonomy); } } } /** * Clear the author archive cache for the current user or of a given user. * * @param WP_User|int|string $author (Optional) User instance or user ID of the author. Default is the current user * if logged in. */ public static function clear_author_archive_cache($author = null) { if (empty($author)) { $author = wp_get_current_user(); } elseif (is_numeric($author)) { $author = get_userdata($author); } if ($author instanceof WP_User) { if (empty($author->user_nicename)) { return; // Author archive cache does not exist. } $author_archive_url = get_author_posts_url($author->ID, $author->user_nicename); if (strpos($author_archive_url, '?') === false) { self::clear_page_cache_by_url($author_archive_url, 'pagination'); } } } /** * Clear the page cache associated with a given site. * * @param WP_Site|int|string $site Site instance or site blog ID. * @param array|string $args (Optional) See Fastless_Cache_Disk::cache_iterator() for the available * arguments. Default empty array. */ public static function clear_page_cache_by_site($site, $args = array()) { $blog_id = self::get_blog_id($site); if ($blog_id === 0) { return; // Page cache does not exist. } if (is_array($args)) { $args['subpages']['exclude'] = self::get_root_blog_exclusions(); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams -- filesystem cache-index exclusion array, not a WP_Query argument. if (! isset($args['hooks']['include'])) { $args['hooks']['include'] = 'fastless_cache_complete_cache_cleared,fastless_cache_site_cache_cleared'; } } self::clear_page_cache_by_url(get_home_url($blog_id), $args); } /** * Clear the page cache by post ID (deprecated). */ public static function clear_page_cache_by_post_id($post_id, $args = array()) { self::clear_page_cache_by_post($post_id, $args); } /** * Clear the page cache of a given post. * * @param WP_Post|int|string $post Post instance or post ID. * @param array|string $args (Optional) See Fastless_Cache_Disk::cache_iterator() for the available * arguments. Default empty array. */ public static function clear_page_cache_by_post($post, $args = array()) { $post = get_post($post); if ($post instanceof WP_Post) { if ($post->post_status !== 'publish') { return; // Page cache does not exist. } $post_url = get_permalink($post); if ($post_url !== false && strpos($post_url, '?') === false) { self::clear_page_cache_by_url($post_url, $args); } } } /** * Clear the page cache of the post associated with a given comment. * * @param WP_Comment|int|string $comment Comment instance or comment ID. * @param array|string $args (Optional) See Fastless_Cache_Disk::cache_iterator() for the available * arguments. Default empty array. */ public static function clear_page_cache_by_comment($comment, $args = array()) { $comment = get_comment($comment); if ($comment instanceof WP_Comment) { if ($comment->comment_approved !== '1') { return; // Page cache does not exist. } self::clear_page_cache_by_post((int) $comment->comment_post_ID, $args); } } /** * Clear the page cache of the posts associated with a given term. * * This clears the page cache of the posts that have the term set. * * @param WP_Term|int $term Term instance or term ID. * @param string $taxonomy (Optional) Taxonomy name that `$term` is part of. Default empty string. * @param array|string $args (Optional) See Fastless_Cache_Disk::cache_iterator() for the available * arguments. Default empty array. */ public static function clear_page_cache_by_term($term, $taxonomy = '', $args = array()) { $term = get_term($term, $taxonomy); if (! $term instanceof WP_Term) { return; } $post_query_args = array( 'post_type' => 'any', 'post_status' => 'publish', 'numberposts' => -1, 'order' => 'none', 'cache_results' => false, 'no_found_rows' => true, 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery -- required clause: fetches the term's posts for cache invalidation. array( 'taxonomy' => $term->taxonomy, 'terms' => $term->term_id, ), ), ); $posts = get_posts($post_query_args); foreach ($posts as $post) { self::clear_page_cache_by_post($post, $args); } } /** * Clear the page cache of the posts associated with a given user. * * This clears the page cache of the posts that the user is the author of or has * commented on. * * @param WP_User|int|string $user User instance or user ID. * @param array|string $args (Optional) See Fastless_Cache_Disk::cache_iterator() for the available * arguments. Default empty array. */ public static function clear_page_cache_by_user($user, $args = array()) { if (is_numeric($user)) { $user = get_userdata($user); } if (! $user instanceof WP_User) { return; } $post_query_args = array( 'author' => $user->ID, 'post_type' => 'any', 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids', 'order' => 'none', 'cache_results' => false, 'no_found_rows' => true, ); $post_ids = get_posts($post_query_args); $comment_query_args = array( 'status' => 'approve', 'user_id' => $user->ID, ); $comments = get_comments($comment_query_args); foreach ($comments as $comment) { $comment_post_id = (int) $comment->comment_post_ID; if (! in_array($comment_post_id, $post_ids, true)) { $post_ids[] = $comment_post_id; } } foreach ($post_ids as $post_id) { self::clear_page_cache_by_post($post_id, $args); } } /** * Clear the page cache of a given URL. * * @param string $url URL to a cached page (with or without scheme, wildcard path, and query string). * @param array|string $args (Optional) See Fastless_Cache_Disk::cache_iterator() for the available * arguments. Default empty array. */ public static function clear_page_cache_by_url($url, $args = array()) { if (is_array($args)) { $args['clear'] = 1; if (! isset($args['hooks']['include'])) { $args['hooks']['include'] = 'fastless_cache_page_cache_cleared'; } } Fastless_Cache_Disk::cache_iterator($url, $args); } /** * Clear the site cache by blog ID (deprecated). */ public static function clear_site_cache_by_blog_id($blog_id, $deprecated = null) { self::clear_page_cache_by_site($blog_id); } /** * Clear the cache when any post type has been published, updated, or trashed. * * @param WP_Post|int|string $post Post instance or post ID. */ public static function clear_cache_on_post_save($post) { if (Fastless_Cache_Engine::$settings['clear_site_cache_on_saved_post']) { self::clear_site_cache(); } else { self::clear_post_cache($post); } } /** * Clear the cache when a comment been posted, updated, spammed, or trashed. * * @param WP_Comment|int|string $comment Comment instance or comment ID. */ public static function clear_cache_on_comment_save($comment) { if (Fastless_Cache_Engine::$settings['clear_site_cache_on_saved_comment']) { self::clear_site_cache(); } else { self::clear_comment_cache($comment); } } /** * Clear the cache when any term has been added, updated, or deleted. * * @param WP_Term|int $term Term instance or term ID. * @param string $taxonomy (Optional) Taxonomy name that `$term` is part of. Default empty string. */ public static function clear_cache_on_term_save($term, $taxonomy = '') { if (Fastless_Cache_Engine::$settings['clear_site_cache_on_saved_term']) { self::clear_site_cache(); } else { self::clear_term_cache($term, $taxonomy); } } /** * Clear the cache when any user has been added, updated, or deleted. * * @param WP_User|int|string $user User instance or user ID. */ public static function clear_cache_on_user_save($user) { if (Fastless_Cache_Engine::$settings['clear_site_cache_on_saved_user']) { self::clear_site_cache(); } else { self::clear_user_cache($user); } } /** * Clear the cache when an option is about to be updated or already has been. * * @param string $option Name of the option. * @param mixed $old_value The old option value. * @param mixed $value The new option value. */ public static function clear_cache_on_option_save($option, $old_value, $value) { switch ($option) { case 'page_for_posts': case 'page_on_front': array_map(self::class . '::clear_page_cache_by_post', array($old_value, $value)); break; default: self::clear_site_cache(); } } /** * Check plugin's requirements. * * @global string $wp_version WordPress version. */ public static function requirements_check() { if (! current_user_can('manage_options')) { return; } $screen = get_current_screen(); $on_own_screen = $screen && strpos($screen->id, 'fastless-cache') !== false; // Critical errors: show everywhere. // Check the PHP version. if (version_compare(PHP_VERSION, FASTLESS_CACHE_MIN_PHP, '<')) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. PHP version (e.g. 5.6) esc_html__('%1$s requires PHP %2$s or higher to function properly. Please update PHP or disable the plugin.', 'fastless-cache'), 'Fastless Cache', esc_html(FASTLESS_CACHE_MIN_PHP) ) ); } // Check the WordPress version. if (version_compare($GLOBALS['wp_version'], FASTLESS_CACHE_MIN_WP . 'alpha', '<')) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. WordPress version (e.g. 5.1) esc_html__('%1$s requires WordPress %2$s or higher to function properly. Please update WordPress or disable the plugin.', 'fastless-cache'), 'Fastless Cache', esc_html(FASTLESS_CACHE_MIN_WP) ) ); } // Check for conflicting cache plugins. if (get_option('fastless_cache_conflicting_plugin', false)) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache esc_html__('%1$s has detected another active caching plugin or an existing object-cache.php drop-in. Only one page cache plugin can be active at a time. Please deactivate the other plugin before using Fastless Cache.', 'fastless-cache'), 'Fastless Cache' ) ); } // Check for foreign advanced-cache.php drop-in. if (get_option('fastless_cache_dropin_conflict', false)) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. advanced-cache.php esc_html__('%1$s found an existing %2$s drop-in file that belongs to another plugin. Fastless Cache will not overwrite it. Please remove or rename the existing drop-in manually, then reload this page.', 'fastless-cache'), 'Fastless Cache', 'advanced-cache.php' ) ); } // Warnings: only show on our own settings screen. if (! $on_own_screen) { return; } // Check the advanced-cache.php drop-in file (only if no conflict). if (! file_exists(WP_CONTENT_DIR . '/advanced-cache.php') && ! get_option('fastless_cache_conflicting_plugin', false) && ! get_option('fastless_cache_dropin_conflict', false)) { $dropin_created = Fastless_Cache_Disk::create_advanced_cache_file(); if ($dropin_created === false) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. advanced-cache.php 3. /path/to/wp-content/plugins/fastless-cache 4. /path/to/wp-content esc_html__('%1$s was unable to create the required %2$s drop-in file. You can manually create it by locating the sample file in the %3$s directory, editing it as needed, and then saving it in the %4$s directory.', 'fastless-cache'), 'Fastless Cache', 'advanced-cache.php', '' . esc_html(FASTLESS_CACHE_DIR) . '', '' . esc_html(WP_CONTENT_DIR) . '' ) ); } } // Check the WordPress installation directory index file. if (! file_exists(FASTLESS_CACHE_INDEX_FILE)) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. /path/to/index.php 3. FASTLESS_CACHE_INDEX_FILE 4. wp-config.php esc_html__('%1$s was unable to find the WordPress installation directory index file at %2$s. Please define the %3$s constant in your %4$s file as the full path to the location of this file.', 'fastless-cache'), 'Fastless Cache', '' . esc_html(FASTLESS_CACHE_INDEX_FILE) . '', 'FASTLESS_CACHE_INDEX_FILE', 'wp-config.php' ) ); } // Check the permalink structure. if (empty(get_option('permalink_structure'))) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. Permalink Settings esc_html__('%1$s requires a custom permalink structure. Please enable a custom structure in the %2$s.', 'fastless-cache'), 'Fastless Cache', sprintf( '%s', esc_url(admin_url('options-permalink.php')), esc_html__('Permalink Settings', 'fastless-cache') ) ) ); } // Check file and directory permissions. The cache directory // is created on-demand, so we can't simply warn if it doesn't // exist. Instead we warn if (a) it exists but isn't writable, // or (b) it doesn't exist and it doesn't look like we'll be // able to create it. $dirs = array(FASTLESS_CACHE_CACHE_DIR, FASTLESS_CACHE_SETTINGS_DIR); foreach ($dirs as $dir) { $parent_dir = dirname($dir); if ( (file_exists($parent_dir) && ! wp_is_writable($parent_dir)) || (! file_exists($parent_dir) && ! wp_is_writable(dirname($parent_dir))) ) { printf( '

%s

', sprintf( // translators: 1. Fastless Cache 2. /path/to/wp-content/cache 3. 755 4. file permissions esc_html__('%1$s requires the directory %2$s to exist and be writable (mode %3$s, for example). Please create it and/or change its %4$s.', 'fastless-cache'), 'Fastless Cache', '' . esc_html($parent_dir) . '', '755', sprintf( '%s', 'https://wordpress.org/support/article/changing-file-permissions/', esc_html__('file permissions', 'fastless-cache') ) ) ); } } // Check the Autoptimize HTML optimization. if (defined('AUTOPTIMIZE_PLUGIN_DIR') && Fastless_Cache_Engine::$settings['minify_html'] && get_option('autoptimize_html', '') !== '') { printf( '

%s

', sprintf( // translators: 1. Autoptimize 2. Fastless Cache Settings esc_html__('%1$s HTML optimization is enabled. Please disable HTML minification in the %2$s.', 'fastless-cache'), 'Autoptimize', sprintf( '%s', esc_url(admin_url('admin.php?page=' . Fastless_Cache_Admin::SLUG)), esc_html__('Fastless Cache Settings', 'fastless-cache') ) ) ); } } /** * Schedule WP-Cron events. */ public static function schedule_events() { if (! Fastless_Cache_Engine::$started) { return; } $events = self::get_events(); foreach ($events as $hook => $recurrence) { if ($hook === 'fastless_cache_clear_expired_cache') { if (! Fastless_Cache_Engine::$settings['cache_expires'] || Fastless_Cache_Engine::$settings['cache_expiry_time'] === 0) { continue; } } if (! wp_next_scheduled($hook)) { wp_schedule_event(time(), $recurrence, $hook); } } } /** * Unschedule WP-Cron events. */ public static function unschedule_events() { $events = self::get_events(); foreach ($events as $hook => $recurrence) { wp_unschedule_event(wp_next_scheduled($hook), $hook); } } }