$reject_uris, 'never_paths' => self::never_cache_paths(), 'reject_cookies' => $reject_cookies, 'ignore_qs' => $ignore_qs, ); } /** * Parse textarea rules into a clean limited list. * * @param string $raw Textarea value. * @return string[] */ public static function parse_lines( $raw ) { $out = array(); foreach ( explode( "\n", (string) $raw ) as $line ) { $line = trim( $line ); if ( '' === $line || 0 === strpos( $line, '#' ) ) { continue; } if ( strlen( $line ) > self::MAX_RULE_LENGTH ) { $line = substr( $line, 0, self::MAX_RULE_LENGTH ); } if ( 0 === stripos( $line, 'regex:' ) ) { $pattern = substr( $line, 6 ); if ( ! self::regex_is_safe( $pattern ) ) { continue; } $line = 'regex:' . $pattern; } $out[] = $line; if ( count( $out ) >= self::MAX_RULES ) { break; } } return $out; } /** * Reject overly long or complex regex patterns. * * @param string $pattern Pattern without delimiter. * @return bool */ public static function regex_is_safe( $pattern ) { $pattern = (string) $pattern; if ( '' === $pattern || strlen( $pattern ) > self::MAX_REGEX_LENGTH ) { return false; } // Nested quantifiers / catastrophic backtracking shapes: (a+)+, (a*)*, (a+){2,}, etc. if ( preg_match( '/\([^)]*[\*\+][^)]*\)[\*\+]/', $pattern ) ) { return false; } if ( preg_match( '/\([^)]*[\*\+][^)]*\)\{\d/', $pattern ) ) { return false; } // Adjacent stacked quantifiers on groups. if ( preg_match( '/\)\+[\*\+]/', $pattern ) || preg_match( '/\)\*[\*\+]/', $pattern ) ) { return false; } $meta = preg_match_all( '/[\*\+\{\(\|]/', $pattern ); if ( false === $meta || $meta > self::MAX_REGEX_META ) { return false; } $set = @preg_match( '#' . $pattern . '#i', '' ); return false !== $set; } /** * Path-only portion of a request URI (no query string). * * @param string $uri URI. * @return string */ public static function path_only( $uri ) { $uri = (string) $uri; $q = strpos( $uri, '?' ); if ( false !== $q ) { $uri = substr( $uri, 0, $q ); } return $uri; } /** * Whether the request path matches a reject rule (query string ignored). * * @param string $uri Request URI (path + optional query). * @param string[] $rules Rules. * @return bool */ public static function uri_is_rejected( $uri, array $rules ) { $path = self::path_only( $uri ); foreach ( $rules as $rule ) { $rule = trim( (string) $rule ); if ( '' === $rule ) { continue; } if ( 0 === stripos( $rule, 'regex:' ) ) { $pattern = substr( $rule, 6 ); if ( ! self::regex_is_safe( $pattern ) ) { continue; } $set = @preg_match( '#' . $pattern . '#i', $path ); if ( 1 === $set ) { return true; } continue; } if ( false !== stripos( $path, $rule ) ) { return true; } } return false; } /** * @param array $cookies Cookies. * @param string[] $rules Prefix rules. * @return bool */ public static function cookies_are_rejected( array $cookies, array $rules, $allow_empty_woo_cart = false ) { // When cart is empty, Woo still sets session cookie; allow page cache if no items_in_cart. if ( $allow_empty_woo_cart ) { $has_items = false; foreach ( array_keys( $cookies ) as $name ) { $n = (string) $name; if ( 0 === strpos( $n, 'woocommerce_items_in_cart' ) || 0 === strpos( $n, 'woocommerce_cart_hash' ) ) { $has_items = true; break; } } if ( ! $has_items ) { $rules = array_values( array_filter( $rules, static function ( $r ) { $r = (string) $r; return 0 !== strpos( $r, 'wp_woocommerce_session_' ) && 0 !== strpos( $r, 'woocommerce_recently_viewed' ); } ) ); } } foreach ( $rules as $rule ) { $rule = (string) $rule; if ( '' === $rule ) { continue; } foreach ( array_keys( $cookies ) as $name ) { if ( 0 === strpos( (string) $name, $rule ) ) { return true; } } } return false; } /** * @param string $ua UA. * @param string[] $rules Rules. * @return bool */ public static function ua_is_rejected( $ua, array $rules ) { $ua = (string) $ua; if ( '' === $ua || empty( $rules ) ) { return false; } foreach ( $rules as $rule ) { $rule = (string) $rule; if ( '' !== $rule && false !== stripos( $ua, $rule ) ) { return true; } } return false; } /** * @param array $query Query map. * @param string[] $reject_keys Reject keys. * @return bool */ public static function query_is_rejected( array $query, array $reject_keys ) { if ( empty( $query ) || empty( $reject_keys ) ) { return false; } $reject = array(); foreach ( $reject_keys as $k ) { $k = strtolower( trim( (string) $k ) ); if ( '' !== $k ) { $reject[ $k ] = true; } } foreach ( array_keys( self::flatten_query_keys( $query ) ) as $key ) { if ( isset( $reject[ strtolower( (string) $key ) ] ) ) { return true; } } return false; } /** * When an allow-list is configured, any non-ignored key not in the allow-list * must bypass the cache (unknown parameters must not share a cache entry). * * @param array $query Query map. * @param string[] $ignore Ignored keys. * @param string[]|null $allowed Allow-list or null/empty = unrestricted. * @return bool True if request must not be served/stored from cache. */ public static function query_has_unknown_allowed_params( array $query, array $ignore, $allowed ) { if ( ! is_array( $allowed ) || empty( $allowed ) ) { return false; } $ignore_map = array(); foreach ( $ignore as $k ) { $k = strtolower( trim( (string) $k ) ); if ( '' !== $k ) { $ignore_map[ $k ] = true; } } $allowed_map = array(); foreach ( $allowed as $k ) { $k = strtolower( trim( (string) $k ) ); if ( '' !== $k ) { $allowed_map[ $k ] = true; } } // Only top-level parameter names decide allow-list membership. // Nested keys under an allowed parent (e.g. filter[a] when "filter" is allowed) are fine. foreach ( array_keys( $query ) as $key ) { $lk = strtolower( (string) $key ); if ( isset( $ignore_map[ $lk ] ) ) { continue; } if ( ! isset( $allowed_map[ $lk ] ) ) { return true; } } return false; } /** * Flatten nested query arrays to top-level key presence map. * * @param array $query Query. * @param string $prefix Prefix. * @return array */ public static function flatten_query_keys( array $query, $prefix = '' ) { $keys = array(); foreach ( $query as $key => $value ) { $base = (string) $key; $name = '' === $prefix ? $base : $prefix . '[' . $base . ']'; // Always record this level (parent keys included for nested values). $keys[ $base ] = true; $keys[ $name ] = true; if ( is_array( $value ) ) { $keys = array_merge( $keys, self::flatten_query_keys( $value, $name ) ); } } return $keys; } /** * Recursively normalize query values for stable cache keys. * * @param mixed $value Value. * @return mixed */ public static function normalize_query_value( $value ) { if ( is_array( $value ) ) { $out = array(); foreach ( $value as $k => $v ) { $out[ (string) $k ] = self::normalize_query_value( $v ); } ksort( $out ); return $out; } if ( is_bool( $value ) ) { return $value ? '1' : '0'; } if ( is_scalar( $value ) || null === $value ) { return (string) $value; } return ''; } /** * Filter query args for cache key. * * @param array $query Query. * @param string[] $ignore Ignore. * @param string[]|null $allowed Allow. * @return array */ public static function filter_query_for_cache_key( array $query, array $ignore, $allowed = null ) { $ignore_map = array(); foreach ( $ignore as $k ) { $k = strtolower( trim( (string) $k ) ); if ( '' !== $k ) { $ignore_map[ $k ] = true; } } $allowed_map = null; if ( is_array( $allowed ) && ! empty( $allowed ) ) { $allowed_map = array(); foreach ( $allowed as $k ) { $k = strtolower( trim( (string) $k ) ); if ( '' !== $k ) { $allowed_map[ $k ] = true; } } } $out = array(); foreach ( $query as $key => $value ) { $lk = strtolower( (string) $key ); if ( isset( $ignore_map[ $lk ] ) ) { continue; } if ( null !== $allowed_map && ! isset( $allowed_map[ $lk ] ) ) { continue; } $out[ (string) $key ] = self::normalize_query_value( $value ); } ksort( $out ); return $out; } /** * @param array $filtered Filtered query. * @return string */ public static function build_query_string( array $filtered ) { if ( empty( $filtered ) ) { return ''; } return http_build_query( $filtered, '', '&', PHP_QUERY_RFC3986 ); } /** * Configured site host for cache keys (not client HTTP_HOST). * * @return string */ public static function site_host() { $host = 'localhost'; if ( function_exists( 'home_url' ) ) { $parsed = wp_parse_url( home_url(), PHP_URL_HOST ); if ( is_string( $parsed ) && '' !== $parsed ) { $host = strtolower( $parsed ); } } if ( function_exists( 'apply_filters' ) ) { $host = (string) apply_filters( 'qwsc_site_host', $host ); } return $host; } }