65535 ) { return ''; } } return '[' . $literal . ']'; } // Bare unbracketed IPv6 literal (e.g. ::1 or 2001:db8::1). if ( filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { return '[' . $host . ']'; } $colon = strpos( $host, ':' ); if ( false !== $colon ) { $port = substr( $host, $colon + 1 ); if ( ! preg_match( '/^[0-9]{1,5}$/', $port ) || (int) $port < 1 || (int) $port > 65535 ) { return ''; } $host = substr( $host, 0, $colon ); } // Support trailing dot in FQDN (e.g. example.com.). $host = rtrim( $host, '.' ); if ( ! preg_match( '/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/', $host ) ) { return ''; } return $host; } /** * Normalizes a host for comparison (lowercase, port removed). * * Applies the same validation rules as fastlayer_normalize_request_host() so * that the two helpers always agree: a bracketed IPv6 literal must contain a * valid IPv6 address, have no trailing junk after the closing bracket, and not * be unterminated. Bare IPv6 literals are wrapped in brackets for consistency. * * @param string $host Raw host. * @return string Normalised host, or '' when the input is invalid. */ function fastlayer_normalize_host( $host ) { $host = strtolower( trim( (string) $host ) ); if ( '' === $host ) { return ''; } if ( '[' === substr( $host, 0, 1 ) ) { $end = strpos( $host, ']' ); if ( false === $end ) { // Unterminated bracket — invalid. return ''; } $literal = substr( $host, 1, $end - 1 ); if ( '' === $literal || ! filter_var( $literal, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { // Not a valid IPv6 address (e.g. [gggg] or [::1%25eth0]). return ''; } // Reject trailing characters after the closing bracket (e.g. [::1]x). // A port suffix (:port) is acceptable but must be a valid port. $remainder = substr( $host, $end + 1 ); if ( '' !== $remainder ) { if ( ':' !== substr( $remainder, 0, 1 ) ) { return ''; } $port = substr( $remainder, 1 ); if ( ! preg_match( '/^[0-9]{1,5}$/', $port ) || (int) $port < 1 || (int) $port > 65535 ) { return ''; } } return '[' . $literal . ']'; } // Bare unbracketed IPv6 literal (e.g. ::1 or 2001:db8::1). if ( filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { return '[' . $host . ']'; } $colon = strpos( $host, ':' ); if ( false !== $colon ) { $port = substr( $host, $colon + 1 ); if ( ! preg_match( '/^[0-9]{1,5}$/', $port ) || (int) $port < 1 || (int) $port > 65535 ) { return ''; } $host = substr( $host, 0, $colon ); } // Support trailing dot in FQDN (e.g. example.com.). $host = rtrim( $host, '.' ); if ( ! preg_match( '/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/', $host ) ) { return ''; } return $host; } /** * The request path, without query string or fragment. * * @return string */ function fastlayer_request_path() { $uri = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- advanced-cache.php is included before wp-includes/functions.php (wp_unslash()/sanitize_* are undefined) and before wp_magic_quotes() slashes the superglobals; the value is only used for a read-only cache decision. $question = strpos( $uri, '?' ); if ( false !== $question ) { $uri = substr( $uri, 0, $question ); } $hash = strpos( $uri, '#' ); if ( false !== $hash ) { $uri = substr( $uri, 0, $hash ); } if ( '' === $uri || '/' !== substr( $uri, 0, 1 ) ) { $uri = '/' . $uri; } return $uri; } /** * Resolves the current request to a site id using the network index. * * Mirrors the longest-prefix domain + path match WordPress performs in * ms_load_current_site_and_network(), but reads the pre-generated index instead * of the database, because the drop-in runs before WordPress is loaded. * * @param array $index Map of home URL => blog id. * @return int Blog id, or 0 when nothing matches. */ function fastlayer_resolve_blog_id( $index ) { $host = fastlayer_normalize_request_host(); if ( '' === $host ) { return 0; } $path = fastlayer_request_path(); $best_id = 0; $best_len = -1; foreach ( $index as $home_url => $blog_id ) { $blog_id = (int) $blog_id; if ( $blog_id <= 0 ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- advanced-cache.php runs before wp_parse_url() is defined. $parts = parse_url( (string) $home_url ); if ( ! is_array( $parts ) || empty( $parts['host'] ) ) { continue; } if ( fastlayer_normalize_host( $parts['host'] ) !== $host ) { continue; } $home_path = isset( $parts['path'] ) ? (string) $parts['path'] : '/'; $home_path = '/' . ltrim( $home_path, '/' ); $home_path = rtrim( $home_path, '/' ); if ( '' !== $home_path && $path !== $home_path && 0 !== strpos( $path, $home_path . '/' ) ) { continue; } if ( strlen( $home_path ) > $best_len ) { $best_len = strlen( $home_path ); $best_id = $blog_id; } } return $best_id; } /** * Loads the per-site options for the current request on multisite. * * @param string $index_file Absolute path to sites.json. * @return array|null Options array, or null when the site cannot be resolved. */ function fastlayer_load_site_options_from_index( $index_file ) { global $fastlayer_cache_dir; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- advanced-cache.php runs before the WP Filesystem API is available. $index_raw = file_get_contents( $index_file ); if ( ! is_string( $index_raw ) || '' === $index_raw ) { return null; } $index = json_decode( $index_raw, true ); if ( ! is_array( $index ) || empty( $index ) ) { return null; } $blog_id = fastlayer_resolve_blog_id( $index ); if ( $blog_id <= 0 ) { return null; } $site_file = $fastlayer_cache_dir . 'sites/' . (int) $blog_id . '.json'; if ( ! file_exists( $site_file ) ) { return null; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- advanced-cache.php runs before the WP Filesystem API is available. $site_raw = file_get_contents( $site_file ); if ( ! is_string( $site_raw ) || '' === $site_raw ) { return null; } $site_options = json_decode( $site_raw, true ); return is_array( $site_options ) ? $site_options : null; } function fastlayer_is_request_host_trusted() { $expected = fastlayer_expected_hosts(); if ( empty( $expected ) ) { return false; } $host = fastlayer_normalize_request_host(); if ( '' === $host ) { return false; } return in_array( $host, $expected, true ); } function fastlayer_strip_url_domain( $url ) { // WordPress is not loaded this early, so this mirrors the runtime // Page_Cache::strip_url_domain() using the native parse_url() instead of // wp_parse_url(). Both cache paths must agree on what an exclusion matches. // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- advanced-cache.php runs before wp_parse_url() is defined; mirrors Page_Cache::strip_url_domain() so both cache paths agree. $parsed = parse_url( $url ); if ( ! is_array( $parsed ) || empty( $parsed['path'] ) ) { return $url; } $path = $parsed['path']; if ( isset( $parsed['query'] ) ) { $path .= '?' . $parsed['query']; } return $path; } function fastlayer_matches_patterns( $value, $patterns ) { foreach ( $patterns as $pattern ) { $pattern = trim( $pattern ); if ( '' === $pattern ) { continue; } if ( false !== strpos( $pattern, '*' ) || false !== strpos( $pattern, '(.*)' ) ) { $regex = '#^' . str_replace( array( '\*', '(.*)' ), array( '.*', '.*' ), preg_quote( $pattern, '#' ) ) . '$#i'; if ( preg_match( $regex, $value ) || preg_match( $regex, fastlayer_strip_url_domain( $value ) ) ) { return true; } continue; } $value_compare = fastlayer_strip_url_domain( $value ); $pattern_compare = fastlayer_strip_url_domain( $pattern ); if ( stripos( $value_compare, $pattern_compare ) !== false || stripos( $value, $pattern ) !== false ) { return true; } } return false; } function fastlayer_has_excluded_cookie() { $patterns = fastlayer_get_option_lines( 'exclude_cookies' ); // Standard e-commerce & session cookies always excluded $ecommerce_cookies = array( 'woocommerce_items_in_cart', 'woocommerce_cart_hash', 'wp_woocommerce_session_', 'edd_items_in_cart', ); if ( ! isset( $_COOKIE ) || ! is_array( $_COOKIE ) ) { return false; } foreach ( $_COOKIE as $cookie_name => $cookie_value ) { $c_name = (string) $cookie_name; foreach ( $ecommerce_cookies as $ec_cookie ) { if ( false !== stripos( $c_name, $ec_cookie ) ) { return true; } } if ( ! empty( $patterns ) ) { foreach ( $patterns as $pattern ) { $pattern = trim( $pattern ); if ( '' !== $pattern && false !== stripos( $c_name, $pattern ) ) { return true; } } } } return false; } function fastlayer_should_skip_cache() { // WordPress is not loaded this early, so HTTP_HOST is fully client // controlled. It must be validated before it can be used in a cache key. if ( ! fastlayer_is_request_host_trusted() ) { return true; } // FastLayer's preload job reads the sitemap documents over HTTP in order to // discover which pages to warm. Those responses are XML, not HTML, so they // must never become a cache entry that a later request could be served. // Only FastLayer's own preload requests carry this header, so visitors are // unaffected. if ( ! empty( $_SERVER['HTTP_X_FASTLAYER_PRELOAD'] ) ) { return true; } // Check logged in cookie first, since is_user_logged_in() is not defined early. if ( isset( $_COOKIE ) && is_array( $_COOKIE ) ) { foreach ( $_COOKIE as $key => $value ) { if ( 0 === strpos( $key, 'wordpress_logged_in_' ) ) { return true; } } } if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) { return true; } // POST and HEAD are both excluded, matching class-page-cache.php. if ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ) { if ( in_array( strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'POST', 'HEAD' ), true ) ) { return true; } } if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return true; } if ( defined( 'DOING_CRON' ) && DOING_CRON ) { return true; } if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { return true; } if ( defined( 'WP_ADMIN' ) && WP_ADMIN ) { return true; } $request_uri = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- advanced-cache.php is included before wp-includes/functions.php (wp_unslash()/sanitize_* are undefined) and before wp_magic_quotes() slashes the superglobals; the value is only used for a read-only cache decision or a validated cache key. // REST_REQUEST is defined long after this drop-in runs, so match both the // pretty permalink path and the ?rest_route= query form as well. if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return true; } if ( false !== strpos( $request_uri, 'rest_route=' ) ) { return true; } // Preview and search, matching is_preview() and is_search() at runtime. if ( isset( $_GET['preview'] ) || isset( $_GET['preview_id'] ) || isset( $_GET['post_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only cache-decision input in the pre-WordPress drop-in; no nonce exists and no state changes. return true; } if ( isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only cache-decision input in the pre-WordPress drop-in; no nonce exists and no state changes. return true; } // A 404 cannot be detected before WordPress parses the query. It is already // covered the other way round: class-page-cache.php never writes a 404 or // search response to disk, so no such file exists for this drop-in to serve. $skip_paths = array( '/wp-admin/', '/wp-login.php', '/wp-register.php', '/wp-json/', '/cart/', '/checkout/', '/my-account/', '/order-received/', '/wc-api/', 'wc-ajax=', '/edd-checkout/', '/feed/', ); foreach ( $skip_paths as $path ) { if ( false !== strpos( $request_uri, $path ) ) { return true; } } if ( fastlayer_matches_patterns( $request_uri, fastlayer_get_option_lines( 'exclude_urls' ) ) ) { return true; } if ( fastlayer_has_excluded_cookie() ) { return true; } $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) && is_string( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- advanced-cache.php is included before wp-includes/functions.php (wp_unslash()/sanitize_* are undefined) and before wp_magic_quotes() slashes the superglobals; the value is only used for a read-only cache decision or a validated cache key. if ( fastlayer_matches_patterns( $user_agent, fastlayer_get_option_lines( 'exclude_user_agents' ) ) ) { return true; } return false; } function fastlayer_get_accept_cache_variant() { $accept = isset( $_SERVER['HTTP_ACCEPT'] ) && is_string( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( $_SERVER['HTTP_ACCEPT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- advanced-cache.php is included before wp-includes/functions.php (wp_unslash()/sanitize_* are undefined) and before wp_magic_quotes() slashes the superglobals; the value is only used for a read-only cache decision or a validated cache key. if ( false !== strpos( $accept, 'image/avif' ) ) { return 'accept-avif'; } if ( false !== strpos( $accept, 'image/webp' ) ) { return 'accept-webp'; } return 'accept-legacy'; } function fastlayer_get_cache_file() { global $fastlayer_cache_dir; $protocol = function_exists( 'is_ssl' ) && is_ssl() ? 'https' : 'http'; $host = isset( $_SERVER['HTTP_HOST'] ) && is_string( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- advanced-cache.php is included before wp-includes/functions.php (wp_unslash()/sanitize_* are undefined) and before wp_magic_quotes() slashes the superglobals; the value is only used for a read-only cache decision or a validated cache key. $request_uri = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- advanced-cache.php is included before wp-includes/functions.php (wp_unslash()/sanitize_* are undefined) and before wp_magic_quotes() slashes the superglobals; the value is only used for a read-only cache decision or a validated cache key. $url = $protocol . '://' . $host . $request_uri; $cache_key = md5( $url . '|' . fastlayer_get_accept_cache_variant() ); return $fastlayer_cache_dir . $cache_key . '.html'; } function fastlayer_is_cache_expired( $file ) { global $fastlayer_cache_expiration; return ( time() - filemtime( $file ) ) > $fastlayer_cache_expiration; } function fastlayer_is_safe_path( $path ) { global $fastlayer_cache_dir; $realpath = realpath( $path ); if ( false === $realpath ) { return false; } $cache_realpath = realpath( $fastlayer_cache_dir ); if ( false === $cache_realpath ) { return false; } return 0 === strpos( $realpath, $cache_realpath ); } if ( fastlayer_cache_enabled() && ! fastlayer_should_skip_cache() ) { $fastlayer_cache_file = fastlayer_get_cache_file(); if ( file_exists( $fastlayer_cache_file ) && fastlayer_is_safe_path( $fastlayer_cache_file ) ) { if ( fastlayer_is_cache_expired( $fastlayer_cache_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- advanced-cache.php runs before wp_delete_file() is defined (the drop-in is included before wp-includes/functions.php). @unlink( $fastlayer_cache_file ); } else { if ( ! headers_sent() ) { $fastlayer_expiration = $fastlayer_cache_expiration; header( 'Vary: Accept', false ); header( 'X-Cache-Status: HIT' ); header( 'Cache-Control: public, max-age=' . $fastlayer_expiration . ', s-maxage=' . $fastlayer_expiration ); header( 'X-Cache-Generated: yes' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- advanced-cache.php runs before WP_Filesystem exists; readfile() streams the cache file and the request ends here. readfile( $fastlayer_cache_file ); exit; } } }