Source: utils.php

<?php
/**
 * Utils
 *
 * @package PoweredCache
 */

namespace PoweredCache\Utils;

use PoweredCache\Encryption;
use const PoweredCache\Constants\SETTING_OPTION;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

/**
 * Is plugin activated network wide?
 *
 * @param string $plugin_file file path
 *
 * @return bool
 * @since 2.0
 */
function is_network_wide( $plugin_file ) {
	if ( ! is_multisite() ) {
		return false;
	}

	if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
		require_once ABSPATH . '/wp-admin/includes/plugin.php';
	}

	return is_plugin_active_for_network( plugin_basename( $plugin_file ) );
}

/**
 * Get settings with defaults
 *
 * @param bool $force_network_wide Whether getting settings for network or not.
 *                                 The function respects `POWERED_CACHE_IS_NETWORK` by default.
 *                                 However, `POWERED_CACHE_IS_NETWORK` is not functional on
 *                                 (de)activation hooks.
 *
 * @return array
 * @since  2.0
 */
function get_settings( $force_network_wide = false ) {
	global $is_apache;

	$settings = [
		// basic options
		'enable_page_cache'                => true,
		'object_cache'                     => 'off',
		'cache_mobile'                     => true,
		'cache_mobile_separate_file'       => false,
		'loggedin_user_cache'              => false,
		'ssl_cache'                        => true, // deprecated
		'gzip_compression'                 => false,
		'cache_timeout'                    => 1440,
		// advanced options
		'auto_configure_htaccess'          => $is_apache,
		'rejected_user_agents'             => '',
		'rejected_cookies'                 => '',
		'vary_cookies'                     => '',
		'rejected_uri'                     => '',
		'ignored_query_strings'            => '',
		'cache_query_strings'              => '',
		'purge_additional_pages'           => '',
		// file optimization
		'minify_html'                      => false,
		'minify_html_dom_optimization'     => false,
		'combine_google_fonts'             => false,
		'swap_google_fonts_display'        => true,
		'use_bunny_fonts'                  => false,
		'minify_css'                       => false,
		'combine_css'                      => false,
		'critical_css'                     => false,
		'critical_css_additional_files'    => '',
		'critical_css_excluded_files'      => '',
		'critical_css_appended_content'    => '',
		'critical_css_fallback'            => '',
		'excluded_css_files'               => '',
		'remove_unused_css'                => false,
		'ucss_safelist'                    => '',
		'ucss_excluded_files'              => '',
		'minify_js'                        => false,
		'combine_js'                       => false,
		'excluded_js_files'                => '',
		'js_execution_method'              => 'blocking', // deprecated @since 3.2
		'js_defer'                         => false,
		'js_defer_exclusions'              => '',
		'js_delay'                         => false,
		'js_delay_exclusions'              => '',
		'js_delay_timeout'                 => 0,
		'js_execution_optimized_only'      => true,   // deprecated @since 3.2
		'rewrite_file_optimizer'           => $is_apache,
		// media optimization
		'enable_image_optimization'        => false,
		'image_optimizer_preferred_format' => '',
		'add_missing_image_dimensions'     => false,
		// lazyload
		'enable_lazy_load'                 => false,
		'lazy_load_post_content'           => true,
		'lazy_load_images'                 => true,
		'lazy_load_iframes'                => true,
		'lazy_load_widgets'                => true,
		'lazy_load_post_thumbnail'         => true,
		'lazy_load_avatars'                => true,
		'lazy_load_youtube'                => false,
		'lazy_load_skip_first_nth_img'     => 3,
		'lazy_load_exclusions'             => '',
		'disable_wp_lazy_load'             => false,
		'disable_wp_embeds'                => false,
		'disable_emoji_scripts'            => false,
		// cdn
		'enable_cdn'                       => false,
		'cdn_hostname'                     => array( '' ),
		'cdn_zone'                         => array( '' ),
		'cdn_rejected_files'               => '',
		// preload
		'enable_cache_preload'             => false,
		'preload_homepage'                 => true,
		'preload_public_posts'             => true,
		'preload_public_tax'               => true,
		'enable_sitemap_preload'           => false,
		'preload_request_interval'         => 2, // in seconds
		'preload_sitemap'                  => '',
		'prefetch_dns'                     => '',
		'preconnect_resource'              => '',
		'prefetch_links'                   => true,
		// db options
		'db_cleanup_post_revisions'        => false,
		'db_cleanup_auto_drafts'           => false,
		'db_cleanup_trashed_posts'         => false,
		'db_cleanup_spam_comments'         => false,
		'db_cleanup_trashed_comments'      => false,
		'db_cleanup_expired_transients'    => false,
		'db_cleanup_all_transients'        => false,
		'db_cleanup_optimize_tables'       => false,
		'enable_scheduled_db_cleanup'      => false,
		'scheduled_db_cleanup_frequency'   => 'daily',
		// add-ons
		'enable_cloudflare'                => false,
		'cloudflare_api_token'             => '',
		'cloudflare_email'                 => '',
		'cloudflare_api_key'               => '',
		'cloudflare_zone'                  => '',
		'enable_heartbeat'                 => false, // extension status
		'heartbeat_dashboard_status'       => 'enable', // enable,disable,modify
		'heartbeat_dashboard_interval'     => 60, // default interval in seconds
		'heartbeat_editor_status'          => 'enable', // enable,disable,modify
		'heartbeat_editor_interval'        => 15, // default interval in seconds
		'heartbeat_frontend_status'        => 'enable', // enable,disable,modify
		'heartbeat_frontend_interval'      => 60, // default interval in seconds
		'enable_varnish'                   => false,
		'varnish_ip'                       => '',
		// misc
		'cache_footprint'                  => true,
		'async_cache_cleaning'             => false,
		// new options needs to migrate from extensions
		'enable_google_tracking'           => false,
		'enable_fb_tracking'               => false,
	];

	/**
	 * Filter default settings.
	 *
	 * @hook   powered_cache_default_settings
	 *
	 * @param  {array} $settings Default settings.
	 *
	 * @return {array} New value
	 * @since  2.0
	 */
	$default_settings = apply_filters( 'powered_cache_default_settings', $settings );

	if ( POWERED_CACHE_IS_NETWORK || $force_network_wide ) {
		$settings = get_site_option( SETTING_OPTION, [] );
	} else {
		$settings = get_option( SETTING_OPTION, [] );
	}

	$settings = wp_parse_args( $settings, $default_settings );

	return $settings;
}


/**
 * return base caching dir
 * use this function to get base caching directory instead of directly calling constant
 *
 * @return string path
 * @since 1.0
 */
function get_cache_dir() {
	if ( defined( 'POWERED_CACHE_CACHE_DIR' ) ) {
		return POWERED_CACHE_CACHE_DIR; // don't change unless have a particular reason
	}

	return WP_CONTENT_DIR . '/cache/';
}


/**
 * Object cache methods keys will use as option
 *
 * @return array $object_caches
 * @since 1.2 apcu added
 * @since 1.0
 */
function get_object_cache_dropins() {

	$object_caches = array(
		'memcache'  => POWERED_CACHE_DROPIN_DIR . 'memcache-object-cache.php',
		'memcached' => POWERED_CACHE_DROPIN_DIR . 'memcached-object-cache.php',
		'redis'     => POWERED_CACHE_DROPIN_DIR . 'redis-object-cache.php',
		'apcu'      => POWERED_CACHE_DROPIN_DIR . 'apcu-object-cache.php',
	);

	/**
	 * Filter object cache dropins.
	 *
	 * @hook   powered_cache_object_cache_dropins
	 *
	 * @param  {array} $object_caches The list of supported object-cache dropins.
	 *
	 * @return {array} New value
	 * @since  1.0
	 */
	return apply_filters( 'powered_cache_object_cache_dropins', $object_caches );
}


/**
 * Get available object cache backends
 *
 * @return array
 * @since 1.2 unset apcu
 * @since 1.0
 */
function get_available_object_caches() {
	$object_cache_methods = get_object_cache_dropins();

	if ( ! class_exists( '\Memcache' ) || version_compare( PHP_VERSION, '5.6.20', '<' ) ) {
		unset( $object_cache_methods['memcache'] );
	}

	if ( ! class_exists( '\Memcached' ) ) {
		unset( $object_cache_methods['memcached'] );
	}

	if ( ! class_exists( '\Redis' ) ) {
		unset( $object_cache_methods['redis'] );
	}

	if ( ! function_exists( '\apcu_add' ) ) {
		unset( $object_cache_methods['apcu'] );
	}

	return array_keys( $object_cache_methods );
}


/**
 * convert minutes to possible time format
 *
 * @param int $timeout_in_minutes TTL in minutes
 *
 * @return array
 * @since 1.1
 */
function get_timeout_with_interval( $timeout_in_minutes ) {
	$cache_timeout     = $timeout_in_minutes;
	$selected_interval = 'MINUTE';

	if ( $cache_timeout > 0 ) {
		if ( 0 === (int) ( $cache_timeout % 1440 ) ) {
			$cache_timeout     = $cache_timeout / 1440;
			$selected_interval = 'DAY';
		} elseif ( 0 === (int) ( $cache_timeout % 60 ) ) {
			$cache_timeout     = $cache_timeout / 60;
			$selected_interval = 'HOUR';
		}
	}

	return array(
		$cache_timeout,
		$selected_interval,
	);
}

/**
 * Determine whether display or not display htaccess configuration
 * .htaccess can affect the way of serving cached files.
 * Therefore it's only available for network admin on multisite
 *
 * @return bool
 */
function can_configure_htaccess() {
	global $is_apache;

	if ( ! $is_apache ) {
		return false;
	}

	if ( POWERED_CACHE_IS_NETWORK && current_user_can( 'manage_network' ) ) {
		return true;
	}

	if ( is_multisite() && ! POWERED_CACHE_IS_NETWORK ) {
		return false;
	}

	if ( current_user_can( 'manage_options' ) ) {
		return true;
	}

	return false;
}

/**
 * Whether current user capable to do any configuration changes
 *
 * @return bool
 */
function can_control_all_settings() {
	if ( is_multisite() ) {
		if ( current_user_can( 'manage_network' ) ) {
			return true;
		}

		return false;
	}

	if ( current_user_can( 'manage_options' ) ) {
		return true;
	}

	return false;
}


/**
 * Object cache has an effect on all WP
 * So, it should be available for the network admin on multisite
 * regardless of network-wide or individual activated
 *
 * @return bool
 */
function can_configure_object_cache() {
	if ( is_multisite() ) {
		// only allow on network-wide activation
		if ( POWERED_CACHE_IS_NETWORK && current_user_can( 'manage_network' ) ) {
			return true;
		}

		return false;
	}

	if ( current_user_can( 'manage_options' ) ) {
		return true;
	}

	return false;
}

/**
 * Supported js execution methods
 *
 * @depreacated since 3.2
 *
 * @return mixed|void
 */
function js_execution_methods() {
	$methods = [
		'blocking' => esc_html__( 'Blocking – (default)', 'powered-cache' ),
		'async'    => esc_html__( 'Non-blocking using async', 'powered-cache' ),
		'defer'    => esc_html__( 'Non-blocking using defer', 'powered-cache' ),
		'delayed'  => esc_html__( 'Delayed for user interaction', 'powered-cache' ),
	];

	/**
	 * Filter supported JS execution methods.
	 *
	 * @hook   powered_cache_js_execution_methods
	 *
	 * @param  {array} $powered_cache_js_execution_methods JS execution methods.
	 *
	 * @return {array} New value
	 * @since  2.0
	 */
	return apply_filters( 'powered_cache_js_execution_methods', $methods );
}


/**
 * Get available zones
 *
 * @return mixed|void
 * @since 1.0
 */
function cdn_zones() {
	$zones = [
		'all'   => esc_html__( 'All files', 'powered-cache' ),
		'image' => esc_html__( 'Images', 'powered-cache' ),
		'js'    => esc_html__( 'JavaScript', 'powered-cache' ),
		'css'   => esc_html__( 'CSS', 'powered-cache' ),
	];

	/**
	 * Filter CDN zone options.
	 *
	 * @hook   powered_cache_cdn_zones
	 *
	 * @param  {array} $zones CDN Zones (all,image,js,css)
	 *
	 * @return {array} New value
	 * @since  1.0
	 */
	return apply_filters( 'powered_cache_cdn_zones', $zones );
}

/**
 * Which version of plugin running
 *
 * @return bool
 */
function is_premium() {
	if ( defined( 'POWERED_CACHE_PREMIUM_PLUGIN_FILE' ) && POWERED_CACHE_PREMIUM_PLUGIN_FILE ) {
		return true;
	}

	return false;
}

/**
 * Scheduled cleanup options
 *
 * @return array
 */
function scheduled_cleanup_frequency_options() {
	$options = [
		'daily'   => esc_html__( 'Daily', 'powered-cache' ),
		'weekly'  => esc_html__( 'Weekly', 'powered-cache' ),
		'monthly' => esc_html__( 'Monthly', 'powered-cache' ),
	];

	/**
	 * Filter scheduled cleanup options.
	 *
	 * @hook   powered_cache_scheduled_cleanup_frequency_options
	 *
	 * @param  {array} $options The list of supported schedules.
	 *
	 * @return {array} New value
	 * @since  2.0
	 */
	return apply_filters( 'powered_cache_scheduled_cleanup_frequency_options', $options );
}

/**
 * ports \settings_errors for SUI
 *
 * @param string $setting        Slug title of a specific setting
 * @param bool   $sanitize       Whether to re-sanitize the setting value before returning errors
 * @param bool   $hide_on_update Whether hide or not hide on update
 *
 * @see settings_errors
 */
function settings_errors( $setting = '', $sanitize = false, $hide_on_update = false ) {

	if ( $hide_on_update && ! empty( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return;
	}

	$settings_errors = get_settings_errors( $setting, $sanitize );

	if ( empty( $settings_errors ) ) {
		return;
	}

	$output = '';

	foreach ( $settings_errors as $key => $details ) {
		if ( 'updated' === $details['type'] ) {
			$details['type'] = 'sui-notice-success';
		}

		if ( in_array( $details['type'], array( 'error', 'success', 'warning', 'info' ), true ) ) {
			$details['type'] = 'sui-notice-' . $details['type'];
		}

		$css_id = sprintf(
			'setting-error-%s',
			esc_attr( $details['code'] )
		);

		$css_class = sprintf(
			'sui-notice %s settings-error is-dismissible',
			esc_attr( $details['type'] )
		);

		$output .= "<div id='$css_id' class='$css_class'> \n";
		$output .= "<div class='sui-notice-content'><div class='sui-notice-message'>";
		$output .= "<span class='sui-notice-icon sui-icon-info sui-md' aria-hidden='true'></span>";
		$output .= "<p>{$details['message']}</p></div></div>";
		$output .= "</div> \n";
	}

	echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * remove directories recursively
 * Adopted from W3TC Utility
 *
 * @param string $path    The target path
 * @param array  $exclude list of the files that will excluded
 *
 * @return void
 * @since 1.2.5
 */
function remove_dir( $path, $exclude = array() ) {
	// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
	$dir = @opendir( $path );

	if ( $dir ) {
		while ( ( $entry = @readdir( $dir ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
			if ( '.' === $entry || '..' === $entry ) {
				continue;
			}

			foreach ( $exclude as $mask ) {
				if ( fnmatch( $mask, basename( $entry ) ) ) {
					continue 2;
				}
			}

			$full_path = $path . DIRECTORY_SEPARATOR . $entry;

			if ( @is_dir( $full_path ) ) {
				remove_dir( $full_path, $exclude );
			} else {
				@unlink( $full_path );
			}
		}

		@closedir( $dir );
		@rmdir( $path );
	}
	// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
}

/**
 * Get base caching directory of the site.
 *
 * @return mixed|void
 * @since 1.1
 */
function site_cache_dir() {
	$base_dir = get_page_cache_dir();

	// compatible with multisite
	$site_url = get_site_url();

	$site_url_parsed = wp_parse_url( $site_url );

	$site_path = $site_url_parsed['host'];

	if ( ! empty( $site_url_parsed['path'] ) ) {
		$site_path .= $site_url_parsed['path'];
	}

	$site_cache_dir = trailingslashit( $base_dir . $site_path );

	/**
	 * Filter get base caching directory of site
	 *
	 * @hook   powered_cache_site_cache_dir
	 *
	 * @param  {string} $site_cache_dir Site cache dir.
	 *
	 * @return {string} New value
	 * @since  1.1
	 */
	return apply_filters( 'powered_cache_site_cache_dir', $site_cache_dir );
}

/**
 * Page cache base directory.
 *
 * @return string
 * @since 1.1 $url parameter removed
 * @since 1.0
 */
function get_page_cache_dir() {
	$path = get_cache_dir() . 'powered-cache/';

	/**
	 * Filter page cache base directory.
	 *
	 * @hook   powered_cache_get_page_cache_dir
	 *
	 * @param  {string} $path Page cache dir
	 *
	 * @return {string} New value
	 * @since  1.0
	 */
	return apply_filters( 'powered_cache_get_page_cache_dir', $path );
}

/**
 * Clean up cache directory
 *
 * @return mixed
 * @since 1.0
 */
function clean_page_cache_dir() {
	remove_dir( get_page_cache_dir() );
}

/**
 * Clean cache base for the current site
 *
 * @return mixed
 * @since 1.1
 */
function clean_site_cache_dir() {
	$site_cache_dir = site_cache_dir();

	/**
	 * When deleting cache for the main site on multisite subdirectory setup
	 * Don't delete other site's cache
	 */
	if ( is_multisite() && ! is_subdomain_install() && is_main_site() ) {
		$base_dir    = get_page_cache_dir();
		$directories = glob( $site_cache_dir . '*', GLOB_ONLYDIR );
		$site_url    = get_site_url();

		$site_domain = wp_parse_url( $site_url, PHP_URL_HOST );
		foreach ( $directories as $directory ) {
			$dir_name  = str_replace( $base_dir . $site_domain, '', $directory );
			$site_info = get_site_by_path( $site_domain, $dir_name );
			if ( ! $site_info || is_main_site( $site_info->blog_id ) ) {
				remove_dir( $directory );
			}
		}
	} else {
		remove_dir( $site_cache_dir );
	}

	/**
	 * Fires after deleting site cache dir
	 *
	 * @hook  powered_cache_clean_site_cache_dir
	 *
	 * @param {string} $site_cache_dir The caching directory of the current site.
	 *
	 * @since 2.0
	 */
	do_action( 'powered_cache_clean_site_cache_dir', $site_cache_dir );
}


/**
 * Supported mobile browsers
 *
 * @return mixed|void
 * @since 1.0
 */
function mobile_browsers() {
	$mobile_browsers
		= '2.0 MMP, 240x320, 400X240, AvantGo, BlackBerry, Blazer, Cellphone, Danger, DoCoMo, Elaine/3.0, EudoraWeb, Googlebot-Mobile, hiptop, IEMobile, KYOCERA/WX310K, LG/U990, MIDP-2., MMEF20, MOT-V, NetFront, Newt, Nintendo Wii, Nitro, Nokia, Opera Mini, Palm, PlayStation Portable, portalmmm, Proxinet, ProxiNet, SHARP-TQ-GX10, SHG-i900, Small, SonyEricsson, Symbian OS, SymbianOS, TS21i-10, UP.Browser, UP.Link, webOS, Windows CE, WinWAP, YahooSeeker/M1A1-R2D2, iPhone, iPod, Android, BlackBerry9530, LG-TU915 Obigo, LGE VX, webOS, Nokia5800';

	/**
	 * Filters supported mobile browsers.
	 *
	 * @hook  powered_cache_mobile_browsers
	 *
	 * @param {string} $mobile_browsers Comma separated list of the defined mobile browsers.
	 *
	 * @since 1.0
	 */
	return apply_filters( 'powered_cache_mobile_browsers', $mobile_browsers );
}

/**
 * Supported mobile prefixes
 *
 * @return mixed|void
 * @since 1.0
 */
function mobile_prefixes() {
	$mobile_prefixes
		= 'w3c , w3c-, acs-, alav, alca, amoi, audi, avan, benq, bird, blac, blaz, brew, cell, cldc, cmd-, dang, doco, eric, hipt, htc_, inno, ipaq, ipod, jigs, kddi, keji, leno, lg-c, lg-d, lg-g, lge-, lg/u, maui, maxo, midp, mits, mmef, mobi, mot-, moto, mwbp, nec-, newt, noki, palm, pana, pant, phil, play, port, prox, qwap, sage, sams, sany, sch-, sec-, send, seri, sgh-, shar, sie-, siem, smal, smar, sony, sph-, symb, t-mo, teli, tim-, tosh, tsm-, upg1, upsi, vk-v, voda, wap-, wapa, wapi, wapp, wapr, webc, winw, winw, xda , xda-';

	/**
	 * Filters supported mobile prefixes.
	 *
	 * @hook  powered_cache_mobile_prefixes
	 *
	 * @param {string} $mobile_prefixes Comma separated list of the defined mobile prefixes.
	 *
	 * @since 1.0
	 */
	return apply_filters( 'powered_cache_mobile_prefixes', $mobile_prefixes );
}


/**
 * Collect post related urls
 *
 * @param int $post_id Post ID
 *
 * @return array
 * @since 1.0
 * @since 1.1 powered_cache_post_related_urls filter added
 */
function get_post_related_urls( $post_id ) {
	// Valid post statuses that require cache purging.
	$valid_post_statuses = [ 'publish', 'private', 'trash', 'pending', 'draft' ];
	$post_status         = get_post_status( $post_id );
	$post                = get_post( $post_id );

	// Post types that should not have their cache purged.
	$excluded_post_types = [ 'nav_menu_item', 'revision' ];
	$post_type           = get_post_type( $post_id );
	$rest_api_route      = 'wp/v2';

	$related_urls = [];

	if ( false !== get_permalink( $post_id ) && in_array( $post_status, $valid_post_statuses, true ) && ! in_array( $post_type, $excluded_post_types, true ) ) {
		// Add the post URL.
		$related_urls[] = get_permalink( $post_id );

		// Add REST API URL if applicable.
		if ( $rest_api_route ) {
			$post_type_object = get_post_type_object( $post_type );
			if ( ! empty( $post_type_object->show_in_rest ) ) {
				$post_type_base = $post_type_object->rest_base ? $post_type_object->rest_base : $post_type_object->name;
				$related_urls[] = get_rest_url() . $rest_api_route . '/' . $post_type_base . '/' . $post_id . '/';
			} elseif ( in_array( $post_type, [ 'post', 'page' ], true ) ) {
				$related_urls[] = get_rest_url() . $rest_api_route . '/' . $post_type . 's/' . $post_id . '/';
			}
		}

		// Add AMP URL if AMP plugin is active.
		if ( function_exists( 'amp_get_permalink' ) ) {
			$related_urls[] = amp_get_permalink( $post_id );
		}

		// Regular AMP url for posts if ant of the following are active:
		// https://wordpress.org/plugins/accelerated-mobile-pages/
		if ( defined( 'AMPFORWP_AMP_QUERY_VAR' ) ) {
			$related_urls[] = get_permalink( $post_id ) . 'amp/';
		}

		// Handle trashed post URLs.
		if ( 'trash' === $post_status ) {
			$trash_permalink = str_replace( '__trashed', '', get_permalink( $post_id ) );
			$related_urls[]  = $trash_permalink;
			$related_urls[]  = $trash_permalink . 'feed/';
		}

		$taxonomies = get_object_taxonomies( get_post_type( $post_id ), 'objects' );

		// Purge terms associated with the post.
		foreach ( $taxonomies as $taxonomy ) {
			// Skip non-public taxonomies.
			if ( ! $taxonomy->public ) {
				continue;
			}

			$terms = get_the_terms( $post_id, $taxonomy->name );

			if ( empty( $terms ) || is_wp_error( $terms ) ) {
				continue;
			}

			foreach ( $terms as $term ) {
				$term_url = get_term_link( $term->slug, $taxonomy->name );
				if ( ! is_wp_error( $term_url ) ) {
					$related_urls[] = $term_url;
					if ( $taxonomy->show_in_rest ) {
						$taxonomy_base  = $taxonomy->rest_base ? $taxonomy->rest_base : $taxonomy->name;
						$related_urls[] = rest_url( "{$taxonomy->rest_namespace}/{$taxonomy_base}/{$term->term_id}/" ); // REST API URL for the term
					}
				}

				if ( ! is_taxonomy_hierarchical( $taxonomy->name ) ) {
					continue;
				}

				$ancestors = (array) get_ancestors( $term->term_id, $taxonomy->name );
				foreach ( $ancestors as $ancestor ) {
					$ancestor_object = get_term( $ancestor, $taxonomy->name );
					if ( ! is_a( $ancestor, '\WP_Term' ) ) {
						continue;
					}

					$ancestor_term_url = get_term_link( $ancestor_object->slug, $taxonomy->name );
					if ( ! is_wp_error( $ancestor_term_url ) ) {
						$related_urls[] = $ancestor_term_url;
						if ( $taxonomy->show_in_rest ) {
							$taxonomy_base  = $taxonomy->rest_base ? $taxonomy->rest_base : $taxonomy->name;
							$related_urls[] = rest_url( "{$taxonomy->rest_namespace}/{$taxonomy_base}/{$ancestor_object->term_id}/" ); // REST API URL for the ancestor term
						}
					}
				}
			}
		}

		// Purge author and feed URLs for posts.
		if ( 'post' === $post_type ) {
			$author_id      = get_post_field( 'post_author', $post_id );
			$related_urls[] = get_author_posts_url( $author_id );
			$related_urls[] = get_author_feed_link( $author_id );
			if ( $rest_api_route ) {
				$related_urls[] = get_rest_url() . $rest_api_route . '/users/' . $author_id . '/';
			}

			// Include various feed URLs.
			$feed_urls    = [
				get_bloginfo_rss( 'rdf_url' ),
				get_bloginfo_rss( 'rss_url' ),
				get_bloginfo_rss( 'rss2_url' ),
				get_bloginfo_rss( 'atom_url' ),
				get_bloginfo_rss( 'comments_rss2_url' ),
				get_post_comments_feed_link( $post_id ),
			];
			$related_urls = array_merge( $related_urls, $feed_urls );
		}

		// Purge archive pages if not excluded.
		if ( ! in_array( $post_type, [ 'post', 'page' ], true ) ) {
			$related_urls[] = get_post_type_archive_link( $post_type );
			$related_urls[] = get_post_type_archive_feed_link( $post_type );
		}

		$post_date = strtotime( $post->post_date );

		if ( $post_date ) {
			// Generate the date archive URLs
			$year  = gmdate( 'Y', $post_date );
			$month = gmdate( 'm', $post_date );
			$day   = gmdate( 'd', $post_date );

			$related_urls[] = get_year_link( $year );
			$related_urls[] = get_month_link( $year, $month );
			$related_urls[] = get_day_link( $year, $month, $day );
		}

		// Always purge the home page.
		$related_urls[] = home_url( '/' );

		// Purge the posts page if it's set to a static page.
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$posts_page_id = get_option( 'page_for_posts' );
			if ( $posts_page_id ) {
				$related_urls[] = get_permalink( $posts_page_id );
			}
		}
	}

	// Remove query strings and ensure unique URLs before purging.
	$related_urls = array_unique(
		array_map(
			function ( $url ) {
				return strtok( $url, '?' );
			},
			$related_urls
		)
	);

	/**
	 * Filters post related urls.
	 *
	 * @hook  powered_cache_post_related_urls
	 *
	 * @param {array} $related_urls The list of the URLs that related with the post.
	 *
	 * @since 1.0
	 */
	$related_urls = apply_filters( 'powered_cache_post_related_urls', $related_urls );

	return $related_urls;
}


/**
 * Delete cache file
 *
 * @param string $url Target URL
 *
 * @return bool  true when found cache dir, otherwise false
 * @since 1.0
 */
function delete_page_cache( $url ) {

	$dir = get_url_dir( trim( $url ) );

	if ( is_dir( $dir ) ) {
		$files = scandir( $dir );
		foreach ( $files as $file ) {
			/**
			 * Don't need to lookup for index-https, index-https-mobile etc..
			 * Just clean that directory's files only.
			 */
			if ( ! is_dir( $dir . $file ) && file_exists( $dir . $file ) && ! in_array( $file, array( '.', '..' ), true ) ) {
				unlink( $dir . $file );
			}
		}

		if ( file_exists( $dir ) && is_dir_empty( $dir ) ) {
			remove_dir( $dir );
		}

		return true;
	}

	return false;
}


/**
 * Get cache location of given url
 *
 * @param string $url The url to retrieve path
 *
 * @return mixed|void
 * @since 1.1
 */
function get_url_dir( $url ) {
	$url_info = wp_parse_url( $url );
	$sub_dir  = $url_info['host'];

	if ( ! empty( $url_info['path'] ) ) {
		$sub_dir .= $url_info['path'];
	}

	$path = trailingslashit( get_page_cache_dir() ) . ltrim( $sub_dir, '/' );
	$path = trailingslashit( $path );

	/**
	 * Filters the path of the given url in the cache directory.
	 *
	 * @hook  powered_cache_get_url_dir
	 *
	 * @param {string} $path The cache directory of the given URL.
	 *
	 * @since 1.1
	 */
	return apply_filters( 'powered_cache_get_url_dir', $path );
}

/**
 * Prepare cdn addresses with hostname + zone
 *
 * @return mixed|void
 * @since 1.0
 */
function cdn_addresses() {
	$settings = get_settings(); // phpcs:ignore WordPress.WP.DeprecatedFunctions.get_settingsFound

	$hostnames = $settings['cdn_hostname'];
	$zones     = $settings['cdn_zone'];

	$cdn_addresses = array();
	foreach ( $hostnames as $host_key => $host ) {
		if ( filter_var( $host, FILTER_VALIDATE_URL ) ) {
			$host = wp_parse_url( $host, PHP_URL_HOST );
		}

		if ( ! empty( $host ) ) {
			$cdn_addresses[ $zones[ $host_key ] ][] = $host;
		}
	}

	/**
	 * Filters CDN Addresses.
	 *
	 * @hook   powered_cache_cdn_addresses
	 *
	 * @param  {array} $cdn_addresses CDN Addresses.
	 *
	 * @return {array} New value.
	 * @since  1.0
	 */
	return apply_filters( 'powered_cache_cdn_addresses', $cdn_addresses );
}


/**
 * Get list of expired files for given directory
 *
 * @param string $path     directory location
 * @param int    $lifespan lifespan in seconds
 *
 * @return array expired file list
 * @since 1.1
 */
function get_expired_files( $path, $lifespan = 0 ) {

	$current_time = time();

	$expired_files = array();

	// return immediately if the path is not exist!
	if ( ! file_exists( $path ) ) {
		return $expired_files;
	}

	$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path ) );

	foreach ( $files as $file ) {

		if ( $file->isDir() ) {
			continue;
		}

		$path = $file->getPathname();

		if ( @filemtime( $path ) + $lifespan <= $current_time ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			$expired_files[] = $path;
		}
	}

	return $expired_files;
}


/**
 * Flush object cache and clean cache directory
 *
 * @since 1.0
 */
function powered_cache_flush() {
	if ( function_exists( 'wp_cache_flush' ) ) {
		wp_cache_flush();
	}

	remove_dir( get_cache_dir() );

	/**
	 * Fires after cache flush.
	 *
	 * @hook   powered_cache_flushed
	 * @since  1.0
	 */
	do_action( 'powered_cache_flushed' );
}

/**
 * Log to stdout or a file
 *
 * @param string $message Log message
 *
 * @return bool
 */
function log( $message ) {
	if ( ! defined( 'POWERED_CACHE_ENABLE_LOG' ) ) {
		return false;
	}

	if ( ! POWERED_CACHE_ENABLE_LOG ) {
		return false;
	}

	$log_message = gmdate( 'H:i:s' ) . ' ' . getmypid() . ' ' . get_client_ip() . " {$message}" . PHP_EOL;

	/**
	 * Filters log message.
	 *
	 * @hook   powered_cache_log_message
	 *
	 * @param  {string} $log_message The log message.
	 *
	 * @return {string} New value.
	 * @since  2.0
	 */
	$log_message = apply_filters( 'powered_cache_log_message', $log_message );

	/**
	 * Filters log message type.
	 *
	 * @hook   powered_cache_log_message_type
	 *
	 * @param  {null|int} null default message type
	 *
	 * @return {null|int} New value.
	 * @since  2.0
	 */
	$message_type = apply_filters( 'powered_cache_log_message_type', null );
	$destination  = null;

	if ( defined( 'POWERED_CACHE_LOG_FILE' ) ) {
		$destination  = POWERED_CACHE_LOG_FILE;
		$message_type = 3;
	}

	/**
	 * Filters destination of the log.
	 *
	 * @hook   powered_cache_log_destination
	 *
	 * @param  {null|string} $destination The destination of the log.
	 *
	 * @return {null|string} New value.
	 * @since  2.0
	 */
	$log_destination = apply_filters( 'powered_cache_log_destination', $destination );

	// don't log anything when it used for particular IP address
	if ( defined( 'POWERED_CACHE_LOG_IP' ) && POWERED_CACHE_LOG_IP !== get_client_ip() ) {
		return false;
	}

	return error_log( $log_message, $message_type, $log_destination ); // phpcs:ignore
}

/**
 * Get client raw ip
 *
 * @return mixed
 */
function get_client_ip() {
	if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
		return wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	}

	if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
		return wp_unslash( $_SERVER['REMOTE_ADDR'] );  // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	}
}

/**
 * Fragment caching
 *
 * @link  https://gist.github.com/markjaquith/2653957
 * @see   https://gist.github.com/westonruter/5475349
 *
 * @param string   $key      Fragment key
 * @param int      $ttl      Cache TTL
 * @param callable $function callback
 *
 * @throws \Exception Exception
 * @since 1.2
 * @since 2.0 Renamed powered_cache_fragment -> \PoweredCache\Utils\cache_fragment
 */
function cache_fragment( $key, $ttl, $function ) {
	$group  = 'powered-fragments';
	$output = wp_cache_get( $key, $group );
	if ( empty( $output ) ) {
		ob_start();
		call_user_func( $function );
		$output = ob_get_clean();
		wp_cache_add( $key, $output, $group, $ttl );
	}
	echo $output; // phpcs:ignore
}

/**
 * Fetches known headers, ported from WP Super Cache but not using apache_response_headers
 *
 * @return array|false
 * @since 1.2
 */
function get_response_headers() {
	static $known_headers = array(
		'Access-Control-Allow-Origin',
		'Accept-Ranges',
		'Age',
		'Allow',
		'Cache-Control',
		'Connection',
		'Content-Encoding',
		'Content-Language',
		'Content-Length',
		'Content-Location',
		'Content-MD5',
		'Content-Disposition',
		'Content-Range',
		'Content-Type',
		'Date',
		'ETag',
		'Expires',
		'Last-Modified',
		'Link',
		'Location',
		'P3P',
		'Pragma',
		'Proxy-Authenticate',
		'Referrer-Policy',
		'Refresh',
		'Retry-After',
		'Server',
		'Status',
		'Strict-Transport-Security',
		'Trailer',
		'Transfer-Encoding',
		'Upgrade',
		'Vary',
		'Via',
		'Warning',
		'WWW-Authenticate',
		'X-Frame-Options',
		'Public-Key-Pins',
		'X-XSS-Protection',
		'Content-Security-Policy',
		'X-Pingback',
		'X-Content-Security-Policy',
		'X-WebKit-CSP',
		'X-Content-Type-Options',
		'X-Powered-By',
		'X-UA-Compatible',
		'X-Robots-Tag',
	);

	/**
	 * Filters known headers.
	 *
	 * @hook   powered_cache_known_headers
	 *
	 * @param  {array} $known_headers The list of known HTTP headers.
	 *
	 * @return {array} New value.
	 * @since  1.2
	 */
	$known_headers = apply_filters( 'powered_cache_known_headers', $known_headers );

	if ( ! isset( $known_headers['age'] ) ) {
		$known_headers = array_map( 'strtolower', $known_headers );
	}

	$headers = array();

	if ( function_exists( 'headers_list' ) ) {
		$headers = array();
		foreach ( headers_list() as $hdr ) {
			$header_parts = explode( ':', $hdr, 2 );
			$header_name  = isset( $header_parts[0] ) ? trim( $header_parts[0] ) : '';
			$header_value = isset( $header_parts[1] ) ? trim( $header_parts[1] ) : '';

			$headers[ $header_name ] = $header_value;
		}
	}

	foreach ( $headers as $key => $value ) {
		if ( ! in_array( strtolower( $key ), $known_headers, true ) ) {
			unset( $headers[ $key ] );
		}
	}

	return $headers;
}


/**
 * Check if the given url exists in the cache
 *
 * @param string $url       URL
 * @param bool   $is_mobile Check mobile cache
 * @param bool   $is_gzip   check gzipped cache
 *
 * @return bool
 */
function is_url_cached( $url, $is_mobile = false, $is_gzip = false ) {
	$file_name = 'index';
	$url_parts = wp_parse_url( $url );

	if ( 'https' === $url_parts['scheme'] ) {
		$file_name .= '-https';
	}

	if ( $is_mobile ) {
		$file_name .= '-mobile';
	}

	$file_name .= '.html';

	if ( $is_gzip ) {
		$file_name .= '.gz';
	}

	$rel_path = $url_parts['host'];
	if ( ! empty( $url_parts['path'] ) ) {
		$rel_path .= $url_parts['path'];
	}

	$path = trailingslashit( get_page_cache_dir() . $rel_path );

	$cache_file = $path . $file_name;

	return file_exists( $cache_file );
}

/**
 * Check if the permalink structure of the site end with trailingslash
 *
 * @return bool
 * @since 2.0
 */
function permalink_structure_has_trailingslash() {
	if ( '/' === substr( get_option( 'permalink_structure' ), - 1 ) ) {
		return true;
	}

	return false;
}

/**
 * Check if the given directory empty
 *
 * @param string $dir Path
 *
 * @return bool
 */
function is_dir_empty( $dir ) {
	foreach ( new \DirectoryIterator( $dir ) as $file_info ) {
		if ( $file_info->isDot() ) {
			continue;
		}

		return false;
	}

	return true;
}

/**
 * Get the documentation url
 *
 * @param string $path     The path of documentation
 * @param string $fragment URL Fragment
 *
 * @return string final URL
 */
function get_doc_url( $path = null, $fragment = '' ) {
	$doc_site       = 'https://docs.poweredcache.com/';
	$utm_parameters = '?utm_source=wp_admin&utm_medium=plugin&utm_campaign=settings_page';

	if ( ! empty( $path ) ) {
		$doc_site .= ltrim( $path, '/' );
	}

	$doc_url = trailingslashit( $doc_site ) . $utm_parameters;

	if ( ! empty( $fragment ) ) {
		$doc_url .= '#' . $fragment;
	}

	return $doc_url;
}

/**
 * Sanitize CSS
 *
 * @param string $css Input
 *
 * @return string|string[] $css
 * @since 2.1
 */
function sanitize_css( $css ) {
	$css = wp_strip_all_tags( $css );

	if ( false !== strpos( $css, '<' ) ) {
		$css = preg_replace( '#<(\/?\w+)#', '\00003C$1', $css );
	}

	return $css;
}


/**
 *  Test if the current browser runs on a mobile device (smart phone, tablet, etc.)
 *  Sort of custom version of wp_is_mobile
 */
function powered_cache_is_mobile() {

	global $powered_cache_mobile_browsers, $powered_cache_mobile_prefixes;

	$mobile_browsers = addcslashes( implode( '|', preg_split( '/[\s*,\s*]*,+[\s*,\s*]*/', (string) $powered_cache_mobile_browsers ) ), ' ' );
	$mobile_prefixes = addcslashes( implode( '|', preg_split( '/[\s*,\s*]*,+[\s*,\s*]*/', (string) $powered_cache_mobile_prefixes ) ), ' ' );

	if ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
		return false;
	}

	$user_agent = wp_unslash( $_SERVER['HTTP_USER_AGENT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

	if ( ( preg_match( '#^.*(' . $mobile_browsers . ').*#i', $user_agent ) || preg_match( '#^(' . $mobile_prefixes . ').*#i', substr( $user_agent, 0, 4 ) ) ) ) {
		return true;
	}

	return false;
}

/**
 * If the site is a local site.
 *
 * @return bool
 * @since 2.2
 */
function is_local_site() {
	$site_url = site_url();

	// Check for localhost and sites using an IP only first.
	$is_local = $site_url && false === strpos( $site_url, '.' );

	// Use Core's environment check, if available. Added in 5.5.0 / 5.5.1 (for `local` return value).
	if ( function_exists( 'wp_get_environment_type' ) && 'local' === wp_get_environment_type() ) {
		$is_local = true;
	}

	// Then check for usual usual domains used by local dev tools.
	$known_local = array(
		'#\.local$#i',
		'#\.localhost$#i',
		'#\.test$#i',
		'#\.docksal$#i',      // Docksal.
		'#\.docksal\.site$#i', // Docksal.
		'#\.dev\.cc$#i',       // ServerPress.
		'#\.lndo\.site$#i',    // Lando.
	);

	if ( ! $is_local ) {
		foreach ( $known_local as $url ) {
			if ( preg_match( $url, $site_url ) ) {
				$is_local = true;
				break;
			}
		}
	}

	/**
	 * Filters is_local_site check.
	 *
	 * @param bool $is_local If the current site is a local site.
	 *
	 * @since 2.2
	 */
	$is_local = apply_filters( 'powered_cache_is_local_site', $is_local );

	return $is_local;
}

/**
 * Check whether request for bypass or process normally
 *
 * @return bool
 * @since 3.0
 */
function bypass_request() {
	if ( isset( $_GET['nopoweredcache'] ) && $_GET['nopoweredcache'] ) { // phpcs:ignore
		return true;
	}

	return false;
}


/**
 * Calculate total amount of autoloaded data.
 *
 * @return int autoloaded data in bytes.
 * @global wpdb $wpdb WordPress database abstraction object.
 * @since 3.4
 */
function autoloaded_options_size() {
	global $wpdb;

	return (int) $wpdb->get_var( 'SELECT SUM(LENGTH(option_value)) FROM ' . $wpdb->prefix . 'options WHERE autoload = \'yes\'' ); // phpcs:ignore
}

/**
 * Mask the string with asterisk
 *
 * @param string $input_string  String
 * @param int    $unmask_length The length of the string that will not be masked
 *
 * @return string
 * @since 3.4
 */
function mask_string( $input_string, $unmask_length ) {
	$output_string = substr( $input_string, 0, $unmask_length );

	if ( strlen( $input_string ) > $unmask_length ) {
		$output_string .= str_repeat( '*', strlen( $input_string ) - $unmask_length );
	}

	return $output_string;
}

/**
 * Get sensitive data in decrypted form
 *
 * @param string $field field name
 *
 * @return bool|mixed|string
 */
function get_decrypted_setting( $field ) {
	$settings = \PoweredCache\Utils\get_settings();
	$value    = isset( $settings[ $field ] ) ? $settings[ $field ] : '';

	// decrypt the value
	$encryption      = new Encryption();
	$decrypted_value = $encryption->decrypt( $value );
	if ( false !== $decrypted_value ) {
		return $decrypted_value;
	}

	return $value;
}


/**
 * Check if a given IP is within a specific range.
 * Supports both IPv4 and IPv6 addresses.
 *
 * @param string $ip    The IP address to check.
 * @param string $range The IP range in CIDR notation.
 *
 * @return bool True if the IP is in the range, false otherwise.
 */
function is_ip_in_range( $ip, $range ) {
	if ( false !== strpos( $range, '/' ) ) {
		list( $subnet, $bits ) = explode( '/', $range, 2 );
	} else {
		$subnet = $range;
		$bits   = ( false === filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) ? 32 : 128;
	}

	$bits = intval( $bits );

	if ( false !== filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
		$subnet_bin = inet_pton( $subnet );
		$ip_bin     = inet_pton( $ip );

		if ( false === $subnet_bin || false === $ip_bin ) {
			return false;
		}

		$subnet_bin = str_pad( $subnet_bin, 16, "\0" );
		$ip_bin     = str_pad( $ip_bin, 16, "\0" );

		for ( $i = 0; $i * 8 < $bits; $i ++ ) {
			if ( $bits >= ( $i + 1 ) * 8 && $subnet_bin[ $i ] !== $ip_bin[ $i ] ) {
				return false;
			} elseif ( $bits > $i * 8 ) {
				$bitmask = 0xff00 >> ( $bits % 8 );
				if ( ( ord( $subnet_bin[ $i ] ) & $bitmask ) !== ( ord( $ip_bin[ $i ] ) & $bitmask ) ) {
					return false;
				}
			}
		}

		return true;
	}

	if ( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
		return false;
	}

	$subnet_decimal = ip2long( $subnet );
	$ip_decimal     = ip2long( $ip );
	$mask_decimal   = - 1 << ( 32 - $bits );

	return ( $subnet_decimal & $mask_decimal ) === ( $ip_decimal & $mask_decimal );
}