= $length ) { return null; } $escape = self::shared_escape( $pattern[ $i ], $in_class ); if ( null === $escape ) { return null; } $output .= $escape; $quantifier = false; continue; } if ( $in_class ) { if ( ']' === $char ) { $in_class = false; } elseif ( self::is_alphanumeric( $char ) && $i + 2 < $length && '-' === $pattern[ $i + 1 ] && self::is_alphanumeric( $pattern[ $i + 2 ] ) ) { $output .= substr( $pattern, $i, 3 ); $i += 2; continue; } elseif ( ! self::is_alphanumeric( $char ) && '_' !== $char && ' ' !== $char ) { // No nested sets, set operators, or unescaped class punctuation. return null; } } elseif ( '[' === $char ) { $in_class = true; $output .= '['; if ( $i + 1 < $length && '^' === $pattern[ $i + 1 ] ) { $output .= '^'; ++$i; } $quantifier = false; continue; } elseif ( '(' === $char && $i + 1 < $length && '*' === $pattern[ $i + 1 ] ) { return null; // PCRE control verbs are not HTML-v groups. } elseif ( '(' === $char && $i + 1 < $length && '?' === $pattern[ $i + 1 ] ) { if ( $i + 2 >= $length || ':' !== $pattern[ $i + 2 ] ) { return null; } $output .= '(?:'; $i += 2; $quantifier = false; continue; } elseif ( '{' === $char ) { if ( ! preg_match( '/\A\{[0-9]+(?:,[0-9]*)?\}/', substr( $pattern, $i ), $match ) ) { return null; } $output .= $match[0]; $i += strlen( $match[0] ) - 1; $quantifier = true; continue; } elseif ( '+' === $char && $quantifier ) { return null; // PCRE possessive quantifiers are invalid in HTML-v. } elseif ( '.' === $char ) { $char = '[^\x{000A}\x{000D}\x{2028}\x{2029}]'; } elseif ( ord( $char ) < 32 || ord( $char ) > 126 || ']' === $char || '}' === $char ) { return null; } $output .= $char; $quantifier = ! $in_class && false !== strpos( '*+?', $char ); } return $in_class ? null : $output; } /** * Map only escapes with known equivalent HTML-v semantics. * * @param string $char Character following a backslash. * @param bool $in_class Whether inside a character class. * @return string|null Shared PCRE expression, or unsupported. */ private static function shared_escape( $char, $in_class ) { $classes = array( 'd' => '0-9', 'w' => 'A-Za-z0-9_', 's' => self::SPACE, ); $lower = strtolower( $char ); if ( isset( $classes[ $lower ] ) ) { if ( $char !== $lower ) { return $in_class ? null : '[^' . $classes[ $lower ] . ']'; } return $in_class ? $classes[ $char ] : '[' . $classes[ $char ] . ']'; } $controls = array( 'n' => '\x{000A}', 'r' => '\x{000D}', 't' => '\x{0009}', 'f' => '\x{000C}', 'v' => '\x{000B}', ); if ( isset( $controls[ $char ] ) ) { return $controls[ $char ]; } $punctuation = '^$\\.*+?()[]{}|/' . ( $in_class ? '-' : '' ); return false !== strpos( $punctuation, $char ) ? '\\' . $char : null; } /** * Recognize ASCII range endpoints without locale-dependent ctype rules. * * @param string $char Pattern character. * @return bool */ private static function is_alphanumeric( $char ) { return 1 === preg_match( '/\A[A-Za-z0-9]\z/', $char ); } }