array(),
'size' => 0,
);
if (! is_string($url) || empty($url)) {
return $cache;
}
$url = esc_url_raw($url, array('http', 'https'));
$cache_dir = self::get_cache_dir($url);
if (! is_dir($cache_dir)) {
return $cache;
}
$switched = false;
if (is_multisite() && ! ms_is_switched()) {
$blog_domain = (string) wp_parse_url($url, PHP_URL_HOST);
$blog_path = is_subdomain_install() ? '/' : Fastless_Cache::get_blog_path_from_url($url);
$blog_id = get_blog_id_from_url($blog_domain, $blog_path);
if ($blog_id !== 0) {
$switched = Fastless_Cache::switch_to_blog($blog_id, true);
}
}
$args = self::get_cache_iterator_args($url, $args);
$recursive = ($args['subpages'] === 1 || ! empty($args['subpages']['include']) || isset($args['subpages']['exclude']));
$filter = ($recursive && $args['subpages'] !== 1) ? $args['subpages'] : null;
$cache_objects = self::get_dir_objects($cache_dir, $recursive, $filter);
$cache_keys_regex = self::get_cache_keys_regex($args['keys']);
foreach ($cache_objects as $cache_object) {
if (is_file($cache_object)) {
if ($args['root'] && strpos($cache_object, $args['root']) !== 0) {
// Skip to the next object because the file does not start with the provided root path.
continue;
}
$cache_object_name = basename($cache_object);
if ($cache_keys_regex && ! preg_match($cache_keys_regex, $cache_object_name)) {
// Skip to the next object because the file name does not match the provided cache keys.
continue;
}
if ($args['expired'] && ! self::cache_expired($cache_object)) {
// Skip to the next object because the file is not expired.
continue;
}
$cache_object_dir = dirname($cache_object);
$cache_object_size = (int) @filesize($cache_object);
if ($args['clear']) {
if (! @unlink($cache_object)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- native unlink inside purge loops; wp_delete_file() adds a filter call per file and is not loaded this early.
// Skip to the next object because the file deletion failed.
continue;
}
// The cache size is negative when cleared.
$cache_object_size = -$cache_object_size;
// Remove the containing directory if empty along with any of its empty parents.
self::rmdir($cache_object_dir, true);
}
if (strpos($cache_object_name, 'index') === false) {
// Skip to the next object because the file is not a cache version and no longer
// needs to be handled, such as a hidden file.
continue;
}
if (! isset($cache['index'][$cache_object_dir]['url'])) {
$cache['index'][$cache_object_dir]['url'] = self::get_cache_url($cache_object_dir);
// Resolving the post ID costs a database query per cached
// URL. Only the clear path (the page_cache_cleared hook
// payload) needs it; size and count runs just sum files.
$cache['index'][$cache_object_dir]['id'] = $args['clear']
? url_to_postid($cache['index'][$cache_object_dir]['url'])
: 0;
}
$cache['index'][$cache_object_dir]['versions'][$cache_object_name] = $cache_object_size;
$cache['size'] += $cache_object_size;
}
}
// Sort the cache index by forward slashes from the lowest to highest.
uksort($cache['index'], self::class . '::sort_dir_objects');
if ($args['clear']) {
self::fire_cache_cleared_hooks($cache['index'], $args['hooks']);
}
if ($switched) {
Fastless_Cache::restore_current_blog(true);
}
return $cache;
}
/**
* Get the cache size (deprecated).
*/
public static function cache_size($dir = null)
{
return self::get_cache_size($dir);
}
/**
* Clear the cache (deprecated).
*
*/
public static function clear_cache($clear_url = null, $clear_type = 'page')
{
Fastless_Cache::clear_page_cache_by_url($clear_url, $clear_type);
}
/**
* Create the advanced-cache.php drop-in file.
*
* @return string|bool Path to the created file, false on failure.
*/
public static function create_advanced_cache_file()
{
if (! wp_is_writable(WP_CONTENT_DIR)) {
return false;
}
$advanced_cache_sample_file = FASTLESS_CACHE_DIR . '/advanced-cache.php';
if (! is_readable($advanced_cache_sample_file)) {
return false;
}
$advanced_cache_file = WP_CONTENT_DIR . '/advanced-cache.php';
// If advanced-cache.php exists and does not carry our signature, do not overwrite.
if (file_exists($advanced_cache_file)) {
$existing_contents = file_get_contents($advanced_cache_file);
if (is_string($existing_contents) && strpos($existing_contents, 'FASTLESS_CACHE_DROPIN') === false) {
// Foreign drop-in detected. Do not overwrite.
// Set a flag so admin can show a notice.
update_option('fastless_cache_dropin_conflict', true);
return false;
}
}
$advanced_cache_file_contents = file_get_contents($advanced_cache_sample_file);
$search = "realpath(__DIR__) . '/constants.php'";
$replace = "'" . FASTLESS_CACHE_CONSTANTS_FILE . "'";
// Add our signature to the drop-in so we can identify it later.
$advanced_cache_file_contents = str_replace($search, $replace, $advanced_cache_file_contents);
// Insert signature comment after the opening \n\tRequire all denied\n\n" .
"\n\tOrder deny,allow\n\tDeny from all\n\n"
);
}
}
/**
* Fire the cache cleared hooks.
*
* @param array[] $cache_cleared_index Index of the cache cleared.
* @param array[] $hooks Cache cleared hooks to 'include' and/or 'exclude' from being fired.
*/
private static function fire_cache_cleared_hooks($cache_cleared_index, $hooks)
{
if (empty($cache_cleared_index) || empty($hooks)) {
return;
}
if (isset($hooks['include'])) {
$hooks_to_fire = $hooks['include'];
} else {
$hooks_to_fire = array('fastless_cache_complete_cache_cleared', 'fastless_cache_site_cache_cleared', 'fastless_cache_page_cache_cleared');
}
if (! empty($hooks['exclude'])) {
$hooks_to_fire = array_diff($hooks_to_fire, $hooks['exclude']);
}
if (empty($hooks_to_fire)) {
return;
}
if (in_array('fastless_cache_page_cache_cleared', $hooks_to_fire, true)) {
foreach ($cache_cleared_index as $cache_cleared_dir => $cache_cleared_data) {
$page_cleared_url = $cache_cleared_data['url'];
$page_cleared_id = $cache_cleared_data['id'];
/**
* Fires after the page cache has been cleared.
*
* @param string $page_cleared_url Full URL of the page cleared.
* @param int $page_cleared_id Post ID of the page cleared.
* @param array[] $cache_cleared_index Index of the cache cleared.
*/
do_action('fastless_cache_page_cache_cleared', $page_cleared_url, $page_cleared_id, $cache_cleared_index);
do_action('ce_action_cache_by_url_cleared', $page_cleared_url); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- deprecated 1.6.0 back-compat hook kept intentionally; renaming would break existing integrations.
}
}
if (in_array('fastless_cache_site_cache_cleared', $hooks_to_fire, true) && empty(Fastless_Cache::get_cache_index())) {
$site_cleared_url = user_trailingslashit(home_url());
$site_cleared_id = get_current_blog_id();
/**
* Fires after the site cache has been cleared.
*
* @param string $site_cleared_url Full URL of the site cleared.
* @param int $site_cleared_id Post ID of the site cleared.
* @param array[] $cache_cleared_index Index of the cache cleared.
*/
do_action('fastless_cache_site_cache_cleared', $site_cleared_url, $site_cleared_id, $cache_cleared_index);
}
if (in_array('fastless_cache_complete_cache_cleared', $hooks_to_fire, true) && ! is_dir(FASTLESS_CACHE_CACHE_DIR)) {
/**
* Fires after the complete cache has been cleared.
*/
do_action('fastless_cache_complete_cache_cleared');
do_action('ce_action_cache_cleared'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- deprecated 1.6.0 back-compat hook kept intentionally; renaming would break existing integrations.
}
}
/**
* Filters whether a file or directory should be included or excluded.
*
* @param string $dir_object File or directory path to filter (without trailing slash).
* @param array[] $filter File or directory path(s) to 'include' and/or 'exclude' (without trailing slash).
* @return bool True if directory object should be included, false if excluded.
*/
private static function filter_dir_object($dir_object, $filter)
{
if (isset($filter['exclude'])) {
$match = in_array($dir_object, $filter['exclude'], true);
if ($match) {
return false;
}
}
if (isset($filter['include'])) {
$match = in_array($dir_object, $filter['include'], true);
if ($match) {
return true;
}
}
if (! isset($match)) {
return true;
}
ksort($filter); // Sort the keys in alphabetical order to check for an exclusion first.
if (is_dir($dir_object)) {
$dir_object = $dir_object . '/'; // Append a trailing slash to prevent a false match.
}
foreach ($filter as $filter_type => $filter_value) {
if ($filter_type !== 'include' && $filter_type !== 'exclude') {
continue;
}
foreach ($filter_value as $filter_object) {
// If a trailing asterisk exists remove it to allow a wildcard match.
if (substr($filter_object, -1, 1) === '*') {
$filter_object = substr($filter_object, 0, -1);
// Otherwise, maybe append a trailing slash to force a strict match.
} elseif (is_dir($dir_object)) {
$filter_object = $filter_object . '/';
}
if (str_replace($filter_object, '', $dir_object) !== $dir_object) {
switch ($filter_type) {
case 'include':
return true; // Past inclusion or present wildcard inclusion.
case 'exclude':
return false; // Present wildcard exclusion.
}
}
if (strpos($filter_object, $dir_object) === 0 && $filter_type === 'include') {
return true; // Future strict or wildcard inclusion.
}
}
}
if (isset($filter['include'])) {
return false; // Match not found.
}
return true;
}
/**
* Get the cache directory path for the current URL or from a given URL.
*
* This does not check whether the returned cache directory path exists. The
* untrailingslashit() function is not being used to remove the trailing slash
* because it is not available when the cache engine is started early.
*
* @param string $url (Optional) Full URL to a cached page (with or without wildcard path). Default
* is the current URL.
* @return string Cache directory path (without trailing slash), empty string if the URL is invalid.
*/
public static function get_cache_dir($url = null)
{
if (empty($url)) {
$request_uri = isset($_SERVER['REQUEST_URI']) ? Fastless_Cache_Engine::validate_request_uri($_SERVER['REQUEST_URI']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- value passed to validate_request_uri() which unslashes and validates.
if ('' === $request_uri) {
return FASTLESS_CACHE_CACHE_DIR;
}
$url = 'http://' . Fastless_Cache_Engine::$request_headers['Host'] . $request_uri;
}
$url_parts = Fastless_Cache_Engine::wp_parse_url_safe($url);
if (! is_array($url_parts) || ! isset($url_parts['host']) || ! is_string($url_parts['host'])) {
return FASTLESS_CACHE_CACHE_DIR;
}
$url_host = $url_parts['host'];
$url_path = (isset($url_parts['path']) && is_string($url_parts['path'])) ? $url_parts['path'] : '';
if ('' !== $url_path && substr($url_path, -1, 1) === '*') {
$url_path = dirname($url_path);
}
$cache_dir = sprintf(
'%s/%s%s',
FASTLESS_CACHE_CACHE_DIR,
strtolower($url_host),
$url_path
);
$cache_dir = rtrim($cache_dir, '/\\');
return $cache_dir;
}
/**
* Get the cache iterator arguments.
*
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
*
* @param string $url (Optional) Full URL to a cached page (with or without wildcard path and query
* string). Default null.
* @param array|string $args (Optional) Cache iterator arguments or an arguments template. Default empty array.
* @return array Cache iterator arguments.
*/
private static function get_cache_iterator_args($url = null, $args = array())
{
$default_args = array(
'clear' => 0,
'expired' => 0,
'hooks' => 0,
'keys' => 0,
'root' => '',
'subpages' => 0,
);
if (! is_array($args)) {
$args_template = $args;
$args = array(
'clear' => 1,
'hooks' => array('include' => 'fastless_cache_page_cache_cleared'),
);
switch ($args_template) {
case 'pagination':
global $wp_rewrite;
$included_subpages[] = isset($wp_rewrite->pagination_base) ? $wp_rewrite->pagination_base : '';
$included_subpages[] = isset($wp_rewrite->comments_pagination_base) ? $wp_rewrite->comments_pagination_base . '-*' : '';
$args['subpages']['include'] = $included_subpages;
break;
case 'subpages':
$args['subpages'] = 1;
break;
default:
$args = array();
}
}
$url_path = (string) wp_parse_url($url, PHP_URL_PATH);
if (substr($url_path, -1, 1) === '*') {
$args['root'] = FASTLESS_CACHE_CACHE_DIR . '/' . substr((string) wp_parse_url($url, PHP_URL_HOST) . $url_path, 0, -1);
$args['subpages']['include'] = basename($url_path);
}
// Merge query string arguments into the parameter arguments and then the default arguments.
wp_parse_str((string) wp_parse_url($url, PHP_URL_QUERY), $query_string_args);
$args = wp_parse_args($query_string_args, $args);
$args = wp_parse_args($args, $default_args);
$args = self::validate_cache_iterator_args($args);
return $args;
}
/**
* Get the path to the cache file for the current request.
*
* This does not check whether the returned cache file exists. It sets the
* $cache_file property to prevent different paths being returned on the same
* request. This can occur because the $_SERVER['REQUEST_URI'] superglobal can be
* updated, like by another plugin, between trying to deliver a cached page and
* then actually creating it.
*
* @return string Path to the cache file.
*/
public static function get_cache_file()
{
if (! empty(self::$cache_file)) {
return self::$cache_file;
}
self::$cache_file = sprintf(
'%s/%s',
self::get_cache_dir(),
self::get_cache_file_name()
);
return self::$cache_file;
}
/**
* Get the name of the cache file for the current request.
*
* @return string Name of the cache file.
*/
private static function get_cache_file_name()
{
$cache_keys = self::get_cache_keys();
$cache_file_name = $cache_keys['scheme'] . 'index' . $cache_keys['query'] . $cache_keys['cookie'] . $cache_keys['device'] . '.html' . $cache_keys['compression'];
return $cache_file_name;
}
/**
* Get the cache keys from the request headers for the cache file name.
*
* This has some functionality copied from is_ssl() and wp_is_mobile().
*
* @return string[] An array of cache keys with names as the keys and keys as the values.
*/
private static function get_cache_keys()
{
$cache_keys = array(
'scheme' => 'http-',
'device' => '',
'query' => '',
'cookie' => '',
'compression' => '',
);
// Determine scheme using validated server variables.
$https = isset($_SERVER['HTTPS']) ? strtolower((string) Fastless_Cache_Engine::wp_unslash_safe($_SERVER['HTTPS'])) : ''; // 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.
$server_port = isset($_SERVER['SERVER_PORT']) ? (string) Fastless_Cache_Engine::wp_unslash_safe($_SERVER['SERVER_PORT']) : ''; // 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.
if ($https === 'on' || $https === '1' || $server_port === '443') {
$cache_keys['scheme'] = 'https-';
} elseif (
Fastless_Cache_Engine::$request_headers['X-Forwarded-Proto'] === 'https'
|| Fastless_Cache_Engine::$request_headers['X-Forwarded-Scheme'] === 'https'
) {
$cache_keys['scheme'] = 'https-';
}
if (Fastless_Cache_Engine::$settings['query_cache']) {
$query_cache_params = Fastless_Cache_Engine::$default_include_queries;
$user_query_params = ! empty(Fastless_Cache_Engine::$settings['included_query_strings_list'])
? (array) Fastless_Cache_Engine::$settings['included_query_strings_list']
: Fastless_Cache_Engine::parse_exclusion_list(Fastless_Cache_Engine::$settings['included_query_strings']);
if (!empty($user_query_params)) {
$query_cache_params = array_unique(array_merge($query_cache_params, $user_query_params));
}
if (!empty($_GET)) { // phpcs:ignore WordPress.Security.NonceVerification -- read-only inspection of request data for cache key/bypass decisions; no state change occurs here, so no nonce applies.
$query_cache_values = array();
foreach ($_GET as $key => $value) { // phpcs:ignore WordPress.Security.NonceVerification -- read-only inspection of request data for cache key/bypass decisions; no state change occurs here, so no nonce applies.
if (!in_array($key, Fastless_Cache_Engine::$default_ignore_queries) && in_array($key, $query_cache_params)) {
$query_cache_values[$key] = $value;
}
}
if (!empty($query_cache_values)) {
$cache_keys['query'] = '-' . md5(serialize($query_cache_values));
}
}
}
if (Fastless_Cache_Engine::$settings['cookie_cache']) {
$cookie_strings = ! empty(Fastless_Cache_Engine::$settings['included_cookie_strings_list'])
? (array) Fastless_Cache_Engine::$settings['included_cookie_strings_list']
: Fastless_Cache_Engine::parse_exclusion_list(Fastless_Cache_Engine::$settings['included_cookie_strings']);
if (! empty($_COOKIE)) {
$cookie_cached_values = array();
foreach ($_COOKIE as $key => $value) {
if (in_array($key, $cookie_strings)) {
$cookie_cached_values[$key] = $value;
}
}
if (!empty($cookie_cached_values)) {
$cache_keys['cookie'] = '-' . md5(serialize($cookie_cached_values));
}
}
}
if (Fastless_Cache_Engine::$settings['mobile_cache']) {
if (
strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'Mobile') !== false
|| strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'Android') !== false
|| strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'Silk/') !== false
|| strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'Kindle') !== false
|| strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'BlackBerry') !== false
|| strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'Opera Mini') !== false
|| strpos(Fastless_Cache_Engine::$request_headers['User-Agent'], 'Opera Mobi') !== false
) {
$cache_keys['device'] = '-mobile';
}
}
if (Fastless_Cache_Engine::$settings['compress_cache']) {
$cache_keys['compression'] = self::preferred_compression(
Fastless_Cache_Engine::$request_headers['Accept-Encoding'],
function_exists('brotli_compress'),
self::supports_gzip()
);
}
return $cache_keys;
}
/**
* Get the cache keys regex for the cache iterator.
*
* This uses positive and negative lookaheads to create a regex that will be used
* to check the name of the cache file in the cache iterator, for example:
* * #^(?=.*https)(?=.*mobile).+$#
* * #^(?=.*https)(?!.*mobile).+$#
* * #^.+$#
*
* @param array[] $cache_keys Cache keys to 'include' and/or 'exclude'.
* @return string Cache keys regex, false on failure.
*/
private static function get_cache_keys_regex($cache_keys)
{
if (! is_array($cache_keys)) {
return false;
}
$cache_keys_regex = '#^';
foreach ($cache_keys as $filter_type => $filter_value) {
switch ($filter_type) {
case 'include':
$lookahead = '?=';
break;
case 'exclude':
$lookahead = '?!';
break;
default:
continue 2; // Skip to the next filter value.
}
foreach ($filter_value as $cache_key) {
$cache_keys_regex .= '(' . $lookahead . '.*' . preg_quote($cache_key) . ')';
}
}
$cache_keys_regex .= '.+$#';
return $cache_keys_regex;
}
/**
* Get the cache signature.
*
* This gets the HTML comment that is inserted at the bottom of a new cache file.
*
* @param string $cache_file_name Name of the new cache file.
* @return string HTML comment with the current time in HTTP-date format and the new cache file name.
*/
private static function get_cache_signature($cache_file_name)
{
$cache_signature = sprintf(
'',
'Fastless Cache by AppsCreo',
self::get_current_time(),
'https://wordpress.org/plugins/fastless-cache/'
);
if (! empty(Fastless_Cache_Engine::$settings['disable_footprint'])) {
$cache_signature = '';
}
return $cache_signature;
}
/**
* Get the cache size from the disk (deprecated).
*
*/
public static function get_cache_size($dir = null)
{
if (empty($dir)) {
$cache_size = Fastless_Cache::get_cache_size();
} else {
$url = self::get_cache_url($dir);
$cache = self::cache_iterator($url, array('subpages' => 1));
$cache_size = $cache['size'];
}
return $cache_size;
}
/**
* Get the cache URL for a given directory path.
*
* This only checks if the given directory path is in the plugin cache directory. It
* does not check whether the URL returned is from a cache directory that exists.
*
* @param string $dir Directory path to a cached page.
* @return string Full cache URL (with trailing slash if set), empty string if the directory path
* is invalid.
*/
private static function get_cache_url($dir)
{
if (strpos($dir, FASTLESS_CACHE_CACHE_DIR) !== 0) {
return '';
}
$cache_url = wp_parse_url(home_url(), PHP_URL_SCHEME) . '://' . str_replace(FASTLESS_CACHE_CACHE_DIR . '/', '', $dir);
$cache_url = user_trailingslashit($cache_url);
return $cache_url;
}
/**
* Get the path to the settings file for the current site.
*
* @param bool $fallback (Optional) Whether the fallback settings file should be returned. Default false.
* @return string Path to the settings file.
*/
private static function get_settings_file($fallback = false)
{
$settings_file = sprintf(
'%s/%s',
FASTLESS_CACHE_SETTINGS_DIR,
self::get_settings_file_name($fallback)
);
return $settings_file;
}
/**
* Get the name of the settings file for the current site.
*
* This uses home_url() in the late cache engine start to get the settings file
* name when creating and deleting the settings file or when getting the plugin
* settings from the settings file. Otherwise, it finds the name of the settings
* file in the settings directory when the cache engine is started early.
*
* @param bool $fallback (Optional) Whether the fallback settings file name should be returned. Default false.
* @param bool $skip_blog_path (Optional) Whether the blog path should be included in the settings file name.
* Default false.
* @return string Name of the settings file.
*/
private static function get_settings_file_name($fallback = false, $skip_blog_path = false)
{
$settings_file_name = '';
if (function_exists('home_url')) {
$settings_file_name = wp_parse_url(home_url(), PHP_URL_HOST);
if (is_multisite() && defined('SUBDOMAIN_INSTALL') && ! SUBDOMAIN_INSTALL) {
$blog_path = Fastless_Cache::get_blog_path();
$settings_file_name .= (! empty($blog_path)) ? '.' . trim($blog_path, '/') : '';
}
$settings_file_name .= '.json';
} elseif (is_dir(FASTLESS_CACHE_SETTINGS_DIR)) {
if ($fallback) {
$settings_files = array_map('basename', self::get_dir_objects(FASTLESS_CACHE_SETTINGS_DIR));
$settings_file_regex = '/\.json$/';
if (is_multisite()) {
$settings_file_regex = '/^' . strtolower(Fastless_Cache_Engine::$request_headers['Host']);
$settings_file_regex = str_replace('.', '\.', $settings_file_regex);
if (defined('SUBDOMAIN_INSTALL') && ! SUBDOMAIN_INSTALL && ! $skip_blog_path) {
$request_uri = isset($_SERVER['REQUEST_URI']) ? Fastless_Cache_Engine::validate_request_uri($_SERVER['REQUEST_URI']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- value passed to validate_request_uri() which unslashes and validates.
$url_path = trim(Fastless_Cache_Engine::wp_parse_url_safe($request_uri, PHP_URL_PATH), '/');
if (! empty($url_path)) {
$url_path_regex = str_replace('/', '|', $url_path);
$url_path_regex = '\.(' . $url_path_regex . ')';
$settings_file_regex .= $url_path_regex;
}
}
$settings_file_regex .= '\.json$/';
}
$filtered_settings_files = preg_grep($settings_file_regex, $settings_files);
if (! empty($filtered_settings_files)) {
$settings_file_name = current($filtered_settings_files);
} elseif (is_multisite() && defined('SUBDOMAIN_INSTALL') && ! SUBDOMAIN_INSTALL && ! $skip_blog_path) {
$fallback = true;
$skip_blog_path = true;
$settings_file_name = self::get_settings_file_name($fallback, $skip_blog_path);
}
} else {
$settings_file_name = strtolower(Fastless_Cache_Engine::$request_headers['Host']);
if (is_multisite() && defined('SUBDOMAIN_INSTALL') && ! SUBDOMAIN_INSTALL && ! $skip_blog_path) {
$request_uri = isset($_SERVER['REQUEST_URI']) ? Fastless_Cache_Engine::validate_request_uri($_SERVER['REQUEST_URI']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- value passed to validate_request_uri() which unslashes and validates.
$url_path_pieces = explode('/', $request_uri, 3);
$blog_path = $url_path_pieces[1] ?? '';
if (! empty($blog_path)) {
$settings_file_name .= '.' . $blog_path;
}
$settings_file_name .= '.json';
// Check if the main site in a subdirectory network.
if (! is_file(FASTLESS_CACHE_SETTINGS_DIR . '/' . $settings_file_name)) {
$fallback = false;
$skip_blog_path = true;
$settings_file_name = self::get_settings_file_name($fallback, $skip_blog_path);
}
}
$settings_file_name .= (strpos($settings_file_name, '.json') === false) ? '.json' : '';
}
}
return $settings_file_name;
}
/**
* Get the plugin settings from the settings file for the current site.
*
* Settings are stored as a JSON file rather than an executable PHP file, so
* they are read back with json_decode() instead of being included.
*
* This will create the settings file if it does not exist and the cache engine
* was started late. If that occurs, the settings from the new settings file will
* be returned. Before it is created, checking if the settings file exists after
* retrieving the database settings is done in case an update occurred, which
* would have resulted in a new settings file being created.
*
* This can update the disk and backend requirements and then clear the site cache
* if the settings are outdated. If that occurs, a new settings file will be
* created and an empty array returned.
*
* @param bool $update Whether to update the disk and backend requirements if the settings are
* outdated. Default true.
* @return array Plugin settings from the settings file, empty array when outdated or on failure.
*/
public static function get_settings($update = true)
{
$settings = array();
$settings_file = self::get_settings_file();
if (is_file($settings_file)) {
$settings = self::read_settings_file($settings_file);
} else {
$fallback = true;
$settings_file = self::get_settings_file($fallback);
if (is_file($settings_file)) {
$settings = self::read_settings_file($settings_file);
}
}
$outdated_settings = (! empty($settings) && (! defined('FASTLESS_CACHE_VERSION') || ! isset($settings['version']) || $settings['version'] !== FASTLESS_CACHE_VERSION));
if ($outdated_settings) {
$settings = array();
}
if (empty($settings) && class_exists('Fastless_Cache')) {
if ($outdated_settings) {
if ($update) {
Fastless_Cache::update();
}
} else {
$_settings = Fastless_Cache::get_settings();
$settings_file = self::get_settings_file();
if (is_file($settings_file)) {
$settings = self::read_settings_file($settings_file);
} else {
$settings_file = self::create_settings_file($_settings);
if ($settings_file !== false) {
$settings = self::read_settings_file($settings_file);
}
}
}
}
return $settings;
}
/**
* Read plugin settings from a JSON settings file.
*
* @param string $settings_file Path to the JSON settings file.
* @return array Plugin settings from the file, empty array on failure.
*/
private static function read_settings_file($settings_file)
{
$contents = (string) file_get_contents($settings_file);
$settings = json_decode($contents, true);
return (is_array($settings)) ? $settings : array();
}
/**
* Get the files and directories inside of a given directory.
*
* @param string $dir Directory path to scan (without trailing slash).
* @param bool $recursive (Optional) Whether to recursively include directory objects in nested
* directories. Default false.
* @param array[] $filter (Optional) Directory paths relative to $dir (without leading and/or trailing
* slashes) to 'include' and/or 'exclude'. Default null.
* @return string[] File and directory paths to objects found, empty array if the directory path is invalid.
*/
private static function get_dir_objects($dir, $recursive = false, $filter = null)
{
$dir_objects = array();
self::collect_dir_objects($dir, $recursive, $filter, $dir_objects);
return $dir_objects;
}
/**
* Collect the files and directories inside a directory into an accumulator.
*
* Appends into $dir_objects by reference so a deep or wide tree does not
* copy the whole list at every level; the previous recursive array_merge
* was quadratic in the number of objects. Order is unchanged: a directory's
* children are appended before the directory itself.
*
* @param string $dir Directory path to scan (without trailing slash).
* @param bool $recursive Whether to recurse into nested directories.
* @param array[] $filter Directory filter, or null.
* @param string[] $dir_objects Accumulator, appended to by reference.
*/
private static function collect_dir_objects($dir, $recursive, $filter, &$dir_objects)
{
if (! is_dir($dir)) {
return;
}
$dir_object_names = scandir($dir); // The sorted order is alphabetical in ascending order.
if (is_array($filter) && empty($filter['full_path'])) {
$filter['full_path'] = 1;
foreach ($filter as $filter_type => &$filter_value) {
if ($filter_type === 'include' || $filter_type === 'exclude') {
foreach ($filter_value as &$filter_object) {
$filter_object = $dir . '/' . $filter_object;
}
}
}
}
foreach ($dir_object_names as $dir_object_name) {
if ($dir_object_name === '.' || $dir_object_name === '..') {
continue; // Skip object because it is the current or parent folder link.
}
$dir_object = $dir . '/' . $dir_object_name;
if (is_dir($dir_object)) {
if (! empty($filter['full_path']) && ! self::filter_dir_object($dir_object, $filter)) {
continue; // Skip object because it is excluded.
}
if ($recursive) {
self::collect_dir_objects($dir_object, $recursive, $filter, $dir_objects);
}
}
$dir_objects[] = $dir_object;
}
}
/**
* Get the site objects (deprecated).
*
*/
public static function get_site_objects($site_url)
{
$site_objects = array();
$dir = self::get_cache_dir($site_url);
if (! is_dir($dir)) {
return $site_objects;
}
$site_objects = array_map('basename', self::get_dir_objects($dir));
// Maybe filter the site objects.
if (is_multisite() && ! is_subdomain_install()) {
$blog_path = Fastless_Cache::get_blog_path();
$blog_paths = Fastless_Cache::get_blog_paths();
// Check if the main site in a subdirectory network.
if (! in_array($blog_path, $blog_paths, true)) {
foreach ($site_objects as $key => $site_object) {
// Delete the site object if it does not belong to the main site.
if (in_array('/' . $site_object . '/', $blog_paths, true)) {
unset($site_objects[$key]);
}
}
}
}
return $site_objects;
}
/**
* Get the current time.
*
* @return string Current time in HTTP-date format.
*/
private static function get_current_time()
{
$current_time = current_time('D, d M Y H:i:s', true) . ' GMT';
return $current_time;
}
/**
* Get the current WordPress filesystem instance.
*
* This will initialize the WordPress filesystem if it has not yet been and will
* cache the result afterward.
*
* @throws \RuntimeException If the WordPress filesystem could not be initialized.
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*
* @return WP_Filesystem_Base WordPress filesystem.
*/
public static function get_filesystem()
{
global $wp_filesystem;
if ($wp_filesystem instanceof WP_Filesystem_Base) {
return $wp_filesystem;
}
try {
require_once ABSPATH . 'wp-admin/includes/file.php';
$filesystem = WP_Filesystem();
if ($filesystem === null) {
throw new \RuntimeException('The provided filesystem method is unavailable.');
}
if ($filesystem === false) {
if (is_wp_error($wp_filesystem->errors) && $wp_filesystem->errors->has_errors()) {
throw new \RuntimeException(
$wp_filesystem->errors->get_error_message(),
is_numeric($wp_filesystem->errors->get_error_code()) ? (int) $wp_filesystem->errors->get_error_code() : 0
);
}
throw new \RuntimeException('Unspecified failure.');
}
if (! is_object($wp_filesystem) || ! $wp_filesystem instanceof WP_Filesystem_Base) {
throw new \RuntimeException('$wp_filesystem is not an instance of WP_Filesystem_Base.');
}
} catch (\Exception $e) {
throw new \RuntimeException(
sprintf('There was an error initializing the WP_Filesystem class: %1$s', $e->getMessage()), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message is re-thrown for programmatic handling, not printed as page output.
$e->getCode(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message is re-thrown for programmatic handling, not printed as page output.
$e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message is re-thrown for programmatic handling, not printed as page output.
);
}
return $wp_filesystem;
}
/**
* Make a directory recursively based on the directory path.
*
* This assumes that the directory (and its parent) should have 755 permissions,
* and will attempt to update any existing directories accordingly.
*
* @param string $dir Directory path to create.
* @return bool True if the directory either already exists or was created *and* has the
* correct permissions, false otherwise.
*/
private static function mkdir_p($dir)
{
/**
* Filters the mode assigned to directories on creation.
*
* @param int $mode Mode that defines the access permissions for the created directory. The mode
* must be an octal number, which means it should have a leading zero. Default is 0755.
*/
$mode_octal = (int) apply_filters('fastless_cache_mkdir_mode', 0755);
$parent_dir = dirname($dir);
// Native stat instead of WP_Filesystem: this runs on the request
// shutdown path, and booting the filesystem API would pull
// wp-admin/includes/file.php into a frontend request for a chmod check.
$has_expected_mode = function_exists('fileperms')
&& ((int) @fileperms($dir) & 0777) === $mode_octal
&& ((int) @fileperms($parent_dir) & 0777) === $mode_octal;
if (is_dir($dir) && $has_expected_mode) {
return true;
}
// Directory validation
$valid = false;
if (! empty(FASTLESS_CACHE_CACHE_DIR) && strpos($dir, FASTLESS_CACHE_CACHE_DIR) === 0) {
$valid = true;
}
if (! empty(FASTLESS_CACHE_SETTINGS_DIR) && strpos($dir, FASTLESS_CACHE_SETTINGS_DIR) === 0) {
$valid = true;
}
if (! $valid || strpos($dir, '../') !== false) {
return false;
}
if (! wp_mkdir_p($dir)) {
return false;
}
return true;
}
/**
* Remove an empty directory based on the directory path.
*
* This is a wrapper for rmdir() that can delete empty parent directories and will
* call clearstatcache() when necessary. It suppresses errors on failure.
*
* @param string $dir Directory path to remove.
* @param bool $parents (Optional) Whether empty parent directories should also be removed. Default false.
* @return array[]|bool An array of removed directories with paths as the keys and objects as the
* values. There are no directory objects because a directory has to be empty to
* be removed, which is why it will always be an empty array. False if no
* directories were removed.
*/
private static function rmdir($dir, $parents = false)
{
$removed_dir = @rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- native FS required: purge iterates thousands of entries and WP_Filesystem is not loaded in every context where this runs.
clearstatcache();
if ($removed_dir) {
$removed_dir = array($dir => array());
if ($parents) {
$parent_dir = dirname($dir);
while (@rmdir($parent_dir)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- native FS required: purge iterates thousands of entries and WP_Filesystem is not loaded in every context where this runs.
clearstatcache();
$removed_dir[$parent_dir] = array();
$parent_dir = dirname($parent_dir);
}
}
}
return $removed_dir;
}
private static function set_htaccess_rules()
{
$htaccess_file = ABSPATH . '.htaccess';
if (!file_exists($htaccess_file) || !is_writeable($htaccess_file)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writeable -- writability probe only; WP_Filesystem is not loaded at this point.
return;
}
// Get the contents of the .htaccess file
$htaccess_contents = file_get_contents($htaccess_file);
// Get the rules we want to add
$htaccess_rules = file_get_contents(FASTLESS_CACHE_DIR . '/htaccess.txt');
// Get the site's hostname
$hostname = wp_parse_url(site_url(), PHP_URL_HOST);
// Replace HOSTNAME with the current site's hostname
$htaccess_rules = str_replace('HOSTNAME', $hostname, $htaccess_rules);
// Allow others to modify the rules
$htaccess_rules = apply_filters('fastless_cache_htaccess_rules', $htaccess_rules);
$marker_regex = '/# BEGIN FastlessCache.*?# END FastlessCache/s';
// If the rules is already in the file, replace it
if (preg_match($marker_regex, $htaccess_contents)) {
$htaccess_contents = preg_replace($marker_regex, $htaccess_rules, $htaccess_contents);
}
// If WordPress rules are present, add it before that
elseif (strpos($htaccess_contents, '# BEGIN WordPress') !== false) {
$htaccess_contents = str_replace(
'# BEGIN WordPress',
"$htaccess_rules\n\n# BEGIN WordPress",
$htaccess_contents
);
}
// Otherwise, add it to the top of the file
else {
$htaccess_contents = "$htaccess_rules\n$htaccess_contents";
}
file_put_contents($htaccess_file, $htaccess_contents); // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- managed # BEGIN/END FastlessCache block in .htaccess protects the cache directory; standard cache plugin behavior.
}
private static function remove_htaccess_rules()
{
$htaccess_file = ABSPATH . '.htaccess';
if (!file_exists($htaccess_file) || !is_writeable($htaccess_file)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writeable -- writability probe only; WP_Filesystem is not loaded at this point.
return;
}
$htaccess = file_get_contents($htaccess_file);
if (! is_string($htaccess)) {
return;
}
// Remove our rules. The pattern is line-ending agnostic so LF and
// CRLF files are both handled without leaving stray \r characters.
// Non-greedy so duplicate blocks never swallow user lines between them.
$new_htaccess = preg_replace('/# BEGIN FastlessCache.*?# END FastlessCache(?:\r?\n)*/s', '', $htaccess);
if (! is_string($new_htaccess) || $new_htaccess === $htaccess) {
return; // Nothing owned found: leave the user's file untouched.
}
// Write back to htaccess
file_put_contents($htaccess_file, $new_htaccess); // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- managed # BEGIN/END FastlessCache block in .htaccess protects the cache directory; standard cache plugin behavior.
}
/**
* Remove the global files exactly once per request.
*
* The drop-in, the wp-config.php constant and the .htaccess block are
* shared by every site, so on multisite the per-blog deactivation loop
* must not process them more than once.
*/
public static function clean_global_files()
{
static $done = false;
if ($done) {
return;
}
$done = true;
// Only delete our own drop-in.
self::delete_advanced_cache_file();
// Ownership is detected by marker inside the callee.
self::set_wp_cache_constant(false);
self::remove_htaccess_rules();
}
/**
* Set or unset the WP_CACHE constant in the wp-config.php file.
*
* This has some functionality copied from wp-load.php when trying to find the
* wp-config.php file. It will only set the WP_CACHE constant if the wp-config.php
* file is considered to be default and it is not already set. It will only unset
* the WP_CACHE constant if previously set by the plugin.
*
* @param bool $set (Optional) True to set the WP_CACHE constant, false to unset. Default true.
* @return string|bool Path to the updated wp-config.php file, false otherwise.
*/
private static function set_wp_cache_constant($set = true)
{
if (file_exists(ABSPATH . 'wp-config.php')) {
// The config file resides in ABSPATH.
$wp_config_file = ABSPATH . 'wp-config.php';
} elseif (@file_exists(dirname(ABSPATH) . '/wp-config.php') && ! @file_exists(dirname(ABSPATH) . '/wp-settings.php')) {
// The config file resides one level above ABSPATH but is not part of another installation.
$wp_config_file = dirname(ABSPATH) . '/wp-config.php';
} else {
// The config file could not be found.
return false;
}
if (! wp_is_writable($wp_config_file)) {
return false;
}
$wp_config_file_contents = file_get_contents($wp_config_file);
if (! is_string($wp_config_file_contents)) {
return false;
}
if ($set) {
$default_wp_config_file = (strpos($wp_config_file_contents, '/** Sets up WordPress vars and included files. */') !== false);
if (! $default_wp_config_file) {
return false;
}
$found_wp_cache_constant = preg_match('#define\s*\(\s*[\'\"]WP_CACHE[\'\"]\s*,.+\);#', $wp_config_file_contents);
if ($found_wp_cache_constant) {
return false;
}
// Match the file's own line-ending style so the inserted block can
// always be found again by the marker-based removal below.
$eol = (strpos($wp_config_file_contents, "\r\n") !== false) ? "\r\n" : "\n";
$new_wp_config_lines = '/** Enables page caching for Fastless Cache. */' . $eol;
$new_wp_config_lines .= "if ( ! defined( 'WP_CACHE' ) ) {" . $eol;
$new_wp_config_lines .= "\tdefine( 'WP_CACHE', true );" . $eol;
$new_wp_config_lines .= '}' . $eol;
$new_wp_config_lines .= $eol;
$new_wp_config_file_contents = preg_replace('#(/\*\* Sets up WordPress vars and included files\. \*/)#', $new_wp_config_lines . '$1', $wp_config_file_contents);
// Track that we set it.
update_option('fastless_cache_wp_cache_set_by_us', true);
} else { // Unset.
// Ownership is detected by marker, never by the flag option: the
// flag may be missing while the block is present. Patterns are
// line-ending agnostic so LF and CRLF files are both handled.
$eol = '\r?\n';
if (strpos($wp_config_file_contents, '/** Enables page caching for Fastless Cache. */') !== false) {
$new_wp_config_file_contents = preg_replace('#/\*\* Enables page caching for Fastless Cache\. \*/' . $eol . '.+' . $eol . '.+' . $eol . '\}' . $eol . '(' . $eol . ')?#', '', $wp_config_file_contents);
} elseif (strpos($wp_config_file_contents, '// Added by Fastless Cache') !== false) { // < 1.5.0
$new_wp_config_file_contents = preg_replace('#.+Added by Fastless Cache\r?\n#', '', $wp_config_file_contents);
} else {
return false; // Not previously set by the plugin.
}
}
if (! is_string($new_wp_config_file_contents) || empty($new_wp_config_file_contents)) {
return false;
}
$wp_config_file_updated = file_put_contents($wp_config_file, $new_wp_config_file_contents, LOCK_EX); // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- inserting the WP_CACHE define into wp-config.php is required to enable the advanced-cache.php drop-in; standard cache plugin behavior.
return ($wp_config_file_updated === false) ? false : $wp_config_file;
}
/**
* Sort file and directory paths by the number of forward slashes.
*
* This sorts paths by the lowest amount of forward slashes to the highest.
*
* @param string $a File or directory path to compare in sort.
* @param string $b File or directory path to compare in sort.
* @return int 1 if $a has more slashes than $b, 0 if equal, and -1 if less.
*/
private static function sort_dir_objects($a, $b)
{
$a = substr_count($a, '/');
$b = substr_count($b, '/');
if ($a === $b) {
return 0;
}
return ($a > $b) ? 1 : -1;
}
/**
* Minify HTML.
*
* This removes HTML comments and collapses whitespace. Inline