`), so tightening them would serve shared cached * pages to commenters and logged-in-adjacent visitors. * * @package XSpeed */ declare(strict_types=1); namespace XSpeed; defined( 'ABSPATH' ) || exit; final class Glob_Matcher { /** * @var array pattern => compiled regex */ private static $compiled = array(); /** * Does any of `$patterns` match `$subject`? * * @param string[] $patterns */ public static function any_match( array $patterns, string $subject ): bool { foreach ( $patterns as $p ) { $p = (string) $p; if ( '' === $p ) { continue; } if ( self::matches( $p, $subject ) ) { return true; } } return false; } public static function matches( string $pattern, string $subject ): bool { $regex = self::compile( $pattern ); // Anchored glob (regex returned starts with '#^') vs substring // (regex returned starts with '#'). Both use preg_match the // same way; the anchoring is baked into the pattern. return 1 === preg_match( $regex, $subject ); } /** * Does any of `$patterns` match the identifier `$name`? * * Name mode: whole-string matching for every pattern form. See the class * docblock for why an identifier must not use contains semantics. * * @param string[] $patterns */ public static function any_match_name( array $patterns, string $name ): bool { foreach ( $patterns as $p ) { $p = (string) $p; if ( '' === $p ) { continue; } if ( self::matches_name( $p, $name ) ) { return true; } } return false; } /** * Match one pattern against an identifier, whole-string and * case-insensitively. */ public static function matches_name( string $pattern, string $name ): bool { return 1 === preg_match( self::compile_name( $pattern ), $name ); } /** * Compile a pattern for name mode. Cached separately from URL mode — * the same pattern compiles to a different regex in each — under a key * no user pattern can produce, since a NUL byte cannot survive the * settings sanitizer. */ public static function compile_name( string $pattern ): string { $cache_key = "\0name:" . $pattern; if ( isset( self::$compiled[ $cache_key ] ) ) { return self::$compiled[ $cache_key ]; } if ( '' !== $pattern && '~' === $pattern[0] ) { // Anchored, unlike URL mode: an unanchored `~utm_[a-z0-9_-]+` // still contains-matches `my_utm_source`, which is the very // over-match name mode exists to stop. $regex = self::compile_regex( substr( $pattern, 1 ), true ); } elseif ( preg_match( '/(? 200 ) { return self::NEVER; } $escaped = str_replace( '#', '\\#', $body ); // Group before anchoring so a top-level alternation (`a|b`) anchors // as a whole rather than binding `^` to the first branch only. $regex = $anchored ? '#^(?:' . $escaped . ')$#i' : '#' . $escaped . '#'; // Validate by compiling against an empty subject. preg_match returns // false on a malformed pattern; suppress the warning it emits. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- intentional: invalid user regex must degrade to never-match, not warn. if ( false === @preg_match( $regex, '' ) ) { return self::NEVER; } return $regex; } /** * Translate glob syntax → regex body (no delimiters, no anchors). * Mirrors fnmatch's FNM_PATHNAME-disabled semantics: `*` matches * across `/` so `/cart/*` correctly catches `/cart/items/sub`. */ private static function glob_to_regex( string $glob ): string { $out = ''; $in_class = false; $len = strlen( $glob ); $escape = false; for ( $i = 0; $i < $len; $i++ ) { $ch = $glob[ $i ]; if ( $escape ) { $out .= preg_quote( $ch, '#' ); $escape = false; continue; } if ( '\\' === $ch ) { $escape = true; continue; } if ( $in_class ) { if ( ']' === $ch ) { $out .= ']'; $in_class = false; } else { // Inside a character class, dash + letters are passed // through; we still preg_quote dangerous chars. $out .= preg_quote( $ch, '#' ); } continue; } switch ( $ch ) { case '*': $out .= '.*'; break; case '?': $out .= '.'; break; case '[': $out .= '['; $in_class = true; break; default: $out .= preg_quote( $ch, '#' ); } } return $out; } /** * Test-only: clear the compile cache. Production code never needs * this (PHP request lifetime handles it). */ public static function reset_cache(): void { self::$compiled = array(); } }