diff --git a/src/wp-includes/class-wp-token-map.php b/src/wp-includes/class-wp-token-map.php
index fc223b187f8c5..fefdeb2854645 100644
--- a/src/wp-includes/class-wp-token-map.php
+++ b/src/wp-includes/class-wp-token-map.php
@@ -452,7 +452,9 @@ public function contains( string $word, string $case_sensitivity = 'case-sensiti
}
$term = str_pad( $word, $this->key_length + 1, "\x00", STR_PAD_RIGHT );
- $word_at = $ignore_case ? stripos( $this->small_words, $term ) : strpos( $this->small_words, $term );
+ $word_at = $ignore_case
+ ? strpos( self::ascii_uppercase( $this->small_words ), self::ascii_uppercase( $term ) )
+ : strpos( $this->small_words, $term );
if ( false === $word_at ) {
return false;
}
@@ -461,7 +463,9 @@ public function contains( string $word, string $case_sensitivity = 'case-sensiti
}
$group_key = substr( $word, 0, $this->key_length );
- $group_at = $ignore_case ? stripos( $this->groups, $group_key ) : strpos( $this->groups, $group_key );
+ $group_at = $ignore_case
+ ? strpos( self::ascii_uppercase( $this->groups ), self::ascii_uppercase( $group_key ) )
+ : strpos( $this->groups, $group_key );
if ( false === $group_at ) {
return false;
}
@@ -478,7 +482,12 @@ public function contains( string $word, string $case_sensitivity = 'case-sensiti
$mapping_length = unpack( 'C', $group[ $at++ ] )[1];
$mapping_at = $at;
- if ( $token_length === $length && 0 === substr_compare( $group, $slug, $token_at, $token_length, $ignore_case ) ) {
+ if (
+ $token_length === $length &&
+ ( $ignore_case
+ ? self::matches_ascii_case_insensitively( $group, $slug, $token_at )
+ : 0 === substr_compare( $group, $slug, $token_at, $token_length ) )
+ ) {
return true;
}
@@ -547,7 +556,9 @@ public function read_token( string $text, int $offset = 0, &$matched_token_byte_
}
$group_key = substr( $text, $offset, $this->key_length );
- $group_at = $ignore_case ? stripos( $this->groups, $group_key ) : strpos( $this->groups, $group_key );
+ $group_at = $ignore_case
+ ? strpos( self::ascii_uppercase( $this->groups ), self::ascii_uppercase( $group_key ) )
+ : strpos( $this->groups, $group_key );
if ( false === $group_at ) {
// Perhaps a short word then.
return strlen( $this->small_words ) > 0
@@ -565,7 +576,11 @@ public function read_token( string $text, int $offset = 0, &$matched_token_byte_
$mapping_length = unpack( 'C', $group[ $at++ ] )[1];
$mapping_at = $at;
- if ( 0 === substr_compare( $text, $token, $offset + $this->key_length, $token_length, $ignore_case ) ) {
+ $token_matches = $ignore_case
+ ? self::matches_ascii_case_insensitively( $text, $token, $offset + $this->key_length )
+ : 0 === substr_compare( $text, $token, $offset + $this->key_length, $token_length );
+
+ if ( $token_matches ) {
$matched_token_byte_length = $this->key_length + $token_length;
return substr( $group, $mapping_at, $mapping_length );
}
@@ -597,7 +612,7 @@ private function read_small_token( string $text, int $offset = 0, &$matched_toke
$small_length = strlen( $this->small_words );
$search_text = substr( $text, $offset, $this->key_length );
if ( $ignore_case ) {
- $search_text = strtoupper( $search_text );
+ $search_text = self::ascii_uppercase( $search_text );
}
$starting_char = $search_text[0];
@@ -605,7 +620,7 @@ private function read_small_token( string $text, int $offset = 0, &$matched_toke
while ( $at < $small_length ) {
if (
$starting_char !== $this->small_words[ $at ] &&
- ( ! $ignore_case || strtoupper( $this->small_words[ $at ] ) !== $starting_char )
+ ( ! $ignore_case || self::ascii_uppercase( $this->small_words[ $at ] ) !== $starting_char )
) {
$at += $this->key_length + 1;
continue;
@@ -619,7 +634,7 @@ private function read_small_token( string $text, int $offset = 0, &$matched_toke
if (
$search_text[ $adjust ] !== $this->small_words[ $at + $adjust ] &&
- ( ! $ignore_case || strtoupper( $this->small_words[ $at + $adjust ] !== $search_text[ $adjust ] ) )
+ ( ! $ignore_case || self::ascii_uppercase( $this->small_words[ $at + $adjust ] ) !== $search_text[ $adjust ] )
) {
$at += $this->key_length + 1;
continue 2;
@@ -633,6 +648,76 @@ private function read_small_token( string $text, int $offset = 0, &$matched_toke
return null;
}
+ /**
+ * Returns the ASCII uppercase version of a given string.
+ *
+ * Only the ASCII lowercase letters a-z are folded onto their uppercase
+ * counterparts; all other bytes are left unchanged. This differs from
+ * `strtoupper()`, which before PHP 8.2 folds bytes according to the
+ * current process locale. Lookup must be locale-independent.
+ *
+ * @since 7.1.0
+ *
+ * @param string $text Text to fold.
+ * @return string ASCII-uppercase version of the given text.
+ */
+ private static function ascii_uppercase( string $text ): string {
+ return strtr( $text, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' );
+ }
+
+ /**
+ * Indicates if a span of text matches a given string, ignoring ASCII case.
+ *
+ * Matching is performed byte-by-byte and folds only the ASCII letters A-Z
+ * and a-z onto each other, regardless of the process locale. This differs
+ * from `substr_compare()` with its case-insensitivity flag, which folds
+ * bytes according to the current process locale and may treat non-ASCII
+ * bytes as case variants of each other.
+ *
+ * The span begins at the given byte offset into the text and runs for the
+ * byte length of the search string. A span extending past the end of the
+ * text never matches.
+ *
+ * @since 7.1.0
+ *
+ * @param string $text Text possibly containing the search string.
+ * @param string $search Search string; its byte length determines the span length.
+ * @param int $offset Byte offset into the text where the span begins.
+ * @return bool Whether the span is an ASCII case-insensitive match for the search string.
+ */
+ private static function matches_ascii_case_insensitively( string $text, string $search, int $offset ): bool {
+ $length = strlen( $search );
+ if ( $offset < 0 || $offset + $length > strlen( $text ) ) {
+ return false;
+ }
+
+ for ( $at = 0; $at < $length; $at++ ) {
+ $text_byte = $text[ $offset + $at ];
+ $search_byte = $search[ $at ];
+
+ if ( $text_byte === $search_byte ) {
+ continue;
+ }
+
+ $text_ord = ord( $text_byte );
+ $search_ord = ord( $search_byte );
+
+ // Fold uppercase ASCII letters onto their lowercase counterparts; no other bytes fold.
+ if ( $text_ord >= 0x41 && $text_ord <= 0x5A ) {
+ $text_ord += 0x20;
+ }
+ if ( $search_ord >= 0x41 && $search_ord <= 0x5A ) {
+ $search_ord += 0x20;
+ }
+
+ if ( $text_ord !== $search_ord ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* Exports the token map into an associate array of key/value pairs.
*
diff --git a/src/wp-includes/html-api/class-wp-html-decoder.php b/src/wp-includes/html-api/class-wp-html-decoder.php
index 6c375b8512946..f3b8b0a38354a 100644
--- a/src/wp-includes/html-api/class-wp-html-decoder.php
+++ b/src/wp-includes/html-api/class-wp-html-decoder.php
@@ -10,6 +10,116 @@
* @since 6.6.0
*/
class WP_HTML_Decoder {
+ /**
+ * Indicates if a span of text matches a given string, ignoring ASCII case.
+ *
+ * Matching is performed byte-by-byte and folds only the ASCII uppercase letters
+ * A-Z onto their lowercase counterparts, regardless of the process locale.
+ *
+ * This exists because PHP's built-in case-insensitive comparisons are locale
+ * sensitive: `substr_compare()` with its case-insensitivity flag and (before
+ * PHP 8.2) `strcasecmp()`, `strtolower()`, and friends fold bytes according
+ * to the current process locale, which may treat non-ASCII bytes as case
+ * variants of each other or of ASCII letters. HTML requires ASCII
+ * case insensitivity regardless of locale.
+ *
+ * The span begins at the given byte offset into the text and runs for the
+ * byte length of the search string. A span extending past the end of the
+ * text never matches, even if the available bytes match the search string.
+ *
+ * Example:
+ *
+ * true === WP_HTML_Decoder::matches_ascii_case_insensitively( 'DIV', 'div' );
+ * true === WP_HTML_Decoder::matches_ascii_case_insensitively( '', 'HTML', 10 );
+ * false === WP_HTML_Decoder::matches_ascii_case_insensitively( "\xCC", "\xEC" );
+ * false === WP_HTML_Decoder::matches_ascii_case_insensitively( 'DIVERT', 'div', 3 );
+ *
+ * @since 7.1.0
+ *
+ * @access private
+ *
+ * @see https://infra.spec.whatwg.org/#ascii-case-insensitive
+ *
+ * @param string $text Text possibly containing the search string.
+ * @param string $search Search string; its byte length determines the span length.
+ * @param int $offset Optional. Byte offset into the text where the span begins. Default 0.
+ * @return bool Whether the span is an ASCII case-insensitive match for the search string.
+ */
+ public static function matches_ascii_case_insensitively( string $text, string $search, int $offset = 0 ): bool {
+ $length = strlen( $search );
+ if ( $offset < 0 || $offset + $length > strlen( $text ) ) {
+ return false;
+ }
+
+ for ( $at = 0; $at < $length; $at++ ) {
+ $text_byte = $text[ $offset + $at ];
+ $search_byte = $search[ $at ];
+
+ if ( $text_byte === $search_byte ) {
+ continue;
+ }
+
+ $text_ord = ord( $text_byte );
+ $search_ord = ord( $search_byte );
+
+ // Fold uppercase ASCII letters onto their lowercase counterparts; no other bytes fold.
+ if ( $text_ord >= 0x41 && $text_ord <= 0x5A ) {
+ $text_ord += 0x20;
+ }
+ if ( $search_ord >= 0x41 && $search_ord <= 0x5A ) {
+ $search_ord += 0x20;
+ }
+
+ if ( $text_ord !== $search_ord ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the ASCII lowercase version of a given string.
+ *
+ * Only the ASCII uppercase letters A-Z are folded onto their lowercase
+ * counterparts; all other bytes are left unchanged. This differs from
+ * `strtolower()`, which before PHP 8.2 folds bytes according to the
+ * current process locale.
+ *
+ * @since 7.1.0
+ *
+ * @access private
+ *
+ * @see https://infra.spec.whatwg.org/#ascii-lowercase
+ *
+ * @param string $text Text to fold.
+ * @return string ASCII-lowercase version of the given text.
+ */
+ public static function ascii_lowercase( string $text ): string {
+ return strtr( $text, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz' );
+ }
+
+ /**
+ * Returns the ASCII uppercase version of a given string.
+ *
+ * Only the ASCII lowercase letters a-z are folded onto their uppercase
+ * counterparts; all other bytes are left unchanged. This differs from
+ * `strtoupper()`, which before PHP 8.2 folds bytes according to the
+ * current process locale.
+ *
+ * @since 7.1.0
+ *
+ * @access private
+ *
+ * @see https://infra.spec.whatwg.org/#ascii-uppercase
+ *
+ * @param string $text Text to fold.
+ * @return string ASCII-uppercase version of the given text.
+ */
+ public static function ascii_uppercase( string $text ): string {
+ return strtr( $text, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' );
+ }
+
/**
* Indicates if an attribute value starts with a given raw string value.
*
@@ -40,7 +150,7 @@ public static function attribute_starts_with( $haystack, $search_text, $case_sen
while ( $search_at < $search_length && $haystack_at < $haystack_end ) {
$chars_match = $loose_case
- ? strtolower( $haystack[ $haystack_at ] ) === strtolower( $search_text[ $search_at ] )
+ ? self::matches_ascii_case_insensitively( $haystack, $search_text[ $search_at ], $haystack_at )
: $haystack[ $haystack_at ] === $search_text[ $search_at ];
$is_introducer = '&' === $haystack[ $haystack_at ];
@@ -61,7 +171,10 @@ public static function attribute_starts_with( $haystack, $search_text, $case_sen
}
// If there is a character reference, then the decoded value must exactly match what follows in the search string.
- if ( 0 !== substr_compare( $search_text, $next_chunk, $search_at, strlen( $next_chunk ), $loose_case ) ) {
+ $chunk_matches = $loose_case
+ ? self::matches_ascii_case_insensitively( $search_text, $next_chunk, $search_at )
+ : 0 === substr_compare( $search_text, $next_chunk, $search_at, strlen( $next_chunk ) );
+ if ( ! $chunk_matches ) {
return false;
}
diff --git a/src/wp-includes/html-api/class-wp-html-doctype-info.php b/src/wp-includes/html-api/class-wp-html-doctype-info.php
index f448aabde3ad9..843bd9060b372 100644
--- a/src/wp-includes/html-api/class-wp-html-doctype-info.php
+++ b/src/wp-includes/html-api/class-wp-html-doctype-info.php
@@ -232,8 +232,8 @@ private function __construct(
* > The system identifier and public identifier strings must be compared...
* > in an ASCII case-insensitive manner.
*/
- $public_identifier = null === $public_identifier ? '' : strtolower( $public_identifier );
- $system_identifier = null === $system_identifier ? '' : strtolower( $system_identifier );
+ $public_identifier = null === $public_identifier ? '' : WP_HTML_Decoder::ascii_lowercase( $public_identifier );
+ $system_identifier = null === $system_identifier ? '' : WP_HTML_Decoder::ascii_lowercase( $system_identifier );
/*
* > The public identifier is set to…
@@ -436,7 +436,7 @@ public static function from_doctype_token( string $doctype_html ): ?self {
*/
if (
$end < 9 ||
- 0 !== substr_compare( $doctype_html, ' case-insensitive match for the word "PUBLIC", then consume those characters
* > and switch to the after DOCTYPE public keyword state.
*/
- if ( 0 === substr_compare( $doctype_html, 'PUBLIC', $at, 6, true ) ) {
+ if ( WP_HTML_Decoder::matches_ascii_case_insensitively( $doctype_html, 'PUBLIC', $at ) ) {
$at += 6;
$at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
if ( $at >= $end ) {
@@ -528,7 +528,7 @@ public static function from_doctype_token( string $doctype_html ): ?self {
* > case-insensitive match for the word "SYSTEM", then consume those characters and switch
* > to the after DOCTYPE system keyword state.
*/
- if ( 0 === substr_compare( $doctype_html, 'SYSTEM', $at, 6, true ) ) {
+ if ( WP_HTML_Decoder::matches_ascii_case_insensitively( $doctype_html, 'SYSTEM', $at ) ) {
$at += 6;
$at += strspn( $doctype_html, " \t\n\f\r", $at, $end - $at );
if ( $at >= $end ) {
diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php
index 5c99db6d5eb4e..c13e24fd1c1f1 100644
--- a/src/wp-includes/html-api/class-wp-html-open-elements.php
+++ b/src/wp-includes/html-api/class-wp-html-open-elements.php
@@ -226,7 +226,12 @@ public function current_node_is( string $identity ): bool {
return (
$current_node_name === $identity ||
( '#doctype' === $identity && 'html' === $current_node_name ) ||
- ( '#tag' === $identity && ctype_upper( $current_node_name ) )
+ /*
+ * Elements are identified by their uppercase ASCII names. This check
+ * avoids `ctype_upper()`, whose classification of bytes outside of
+ * ASCII depends on the process locale.
+ */
+ ( '#tag' === $identity && strlen( $current_node_name ) === strspn( $current_node_name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ) )
);
}
diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php
index 6513db35c1243..995c3513ddc5b 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -715,7 +715,7 @@ public function next_tag( $query = null ): bool {
}
if ( isset( $query['tag_name'] ) ) {
- $query['tag_name'] = strtoupper( $query['tag_name'] );
+ $query['tag_name'] = WP_HTML_Decoder::ascii_uppercase( $query['tag_name'] );
}
$needs_class = ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) )
@@ -934,13 +934,13 @@ public function matches_breadcrumbs( $breadcrumbs ): bool {
// Start at the last crumb.
$crumb = end( $breadcrumbs );
- if ( '*' !== $crumb && $this->get_tag() !== strtoupper( $crumb ) ) {
+ if ( '*' !== $crumb && $this->get_tag() !== WP_HTML_Decoder::ascii_uppercase( $crumb ) ) {
return false;
}
for ( $i = count( $this->breadcrumbs ) - 1; $i >= 0; $i-- ) {
$node = $this->breadcrumbs[ $i ];
- $crumb = strtoupper( current( $breadcrumbs ) );
+ $crumb = WP_HTML_Decoder::ascii_uppercase( current( $breadcrumbs ) );
if ( '*' !== $crumb && $node !== $crumb ) {
return false;
@@ -1413,7 +1413,7 @@ public function serialize_token(): string {
$tag_name = str_replace( "\x00", "\u{FFFD}", $this->get_tag() );
$in_html = 'html' === $this->get_namespace();
- $qualified_name = $in_html ? strtolower( $tag_name ) : $this->get_qualified_tag_name();
+ $qualified_name = $in_html ? WP_HTML_Decoder::ascii_lowercase( $tag_name ) : $this->get_qualified_tag_name();
$qualified_name = str_replace( "\x00", "\u{FFFD}", $qualified_name );
if ( $this->is_tag_closer() ) {
@@ -1897,7 +1897,7 @@ private function step_in_head(): bool {
if (
is_string( $http_equiv ) &&
is_string( $content ) &&
- 0 === strcasecmp( $http_equiv, 'Content-Type' )
+ 'content-type' === WP_HTML_Decoder::ascii_lowercase( $http_equiv )
) {
$this->bail( 'Cannot yet process META tags with http-equiv Content-Type to determine encoding.' );
}
@@ -3015,7 +3015,7 @@ private function step_in_body(): bool {
* > string "hidden", then: set the frameset-ok flag to "not ok".
*/
$type_attribute = $this->get_attribute( 'type' );
- if ( ! is_string( $type_attribute ) || 'hidden' !== strtolower( $type_attribute ) ) {
+ if ( ! is_string( $type_attribute ) || 'hidden' !== WP_HTML_Decoder::ascii_lowercase( $type_attribute ) ) {
$this->state->frameset_ok = false;
}
@@ -3539,7 +3539,7 @@ private function step_in_table(): bool {
*/
case '+INPUT':
$type_attribute = $this->get_attribute( 'type' );
- if ( ! is_string( $type_attribute ) || 'hidden' !== strtolower( $type_attribute ) ) {
+ if ( ! is_string( $type_attribute ) || 'hidden' !== WP_HTML_Decoder::ascii_lowercase( $type_attribute ) ) {
goto anything_else;
}
// @todo Indicate a parse error once it's possible.
@@ -5133,7 +5133,10 @@ private function step_in_foreign_content(): bool {
* > of the token, pop elements from the stack of open elements until node has
* > been popped from the stack, and then return.
*/
- if ( 0 === strcasecmp( $node->node_name, $tag_name ) ) {
+ if (
+ strlen( $node->node_name ) === strlen( $tag_name ) &&
+ WP_HTML_Decoder::matches_ascii_case_insensitively( $node->node_name, $tag_name )
+ ) {
foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
$this->state->stack_of_open_elements->pop();
if ( $node === $item ) {
@@ -6534,14 +6537,13 @@ private function is_html_integration_point(): bool {
}
$encoding = $this->get_attribute( 'encoding' );
+ if ( ! is_string( $encoding ) ) {
+ return false;
+ }
- return (
- is_string( $encoding ) &&
- (
- 0 === strcasecmp( $encoding, 'application/xhtml+xml' ) ||
- 0 === strcasecmp( $encoding, 'text/html' )
- )
- );
+ $encoding = WP_HTML_Decoder::ascii_lowercase( $encoding );
+
+ return 'application/xhtml+xml' === $encoding || 'text/html' === $encoding;
}
$this->bail( 'Should not have reached end of HTML Integration Point detection: check HTML API code.' );
@@ -6561,10 +6563,10 @@ private function is_html_integration_point(): bool {
*/
public static function is_special( $tag_name ): bool {
if ( is_string( $tag_name ) ) {
- $tag_name = strtoupper( $tag_name );
+ $tag_name = WP_HTML_Decoder::ascii_uppercase( $tag_name );
} else {
$tag_name = 'html' === $tag_name->namespace
- ? strtoupper( $tag_name->node_name )
+ ? WP_HTML_Decoder::ascii_uppercase( $tag_name->node_name )
: "{$tag_name->namespace} {$tag_name->node_name}";
}
@@ -6681,7 +6683,7 @@ public static function is_special( $tag_name ): bool {
* @return bool Whether the given tag is an HTML Void Element.
*/
public static function is_void( $tag_name ): bool {
- $tag_name = strtoupper( $tag_name );
+ $tag_name = WP_HTML_Decoder::ascii_uppercase( $tag_name );
return (
'AREA' === $tag_name ||
@@ -6738,7 +6740,7 @@ protected static function get_encoding( string $label ): ?string {
* > If label is an ASCII case-insensitive match for any of the labels listed in the
* > table below, then return the corresponding encoding; otherwise return failure.
*/
- switch ( strtolower( $label ) ) {
+ switch ( WP_HTML_Decoder::ascii_lowercase( $label ) ) {
case 'unicode-1-1-utf-8':
case 'unicode11utf8':
case 'unicode20utf8':
diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php
index a3dac33fc55ca..f51e9d2d70a3a 100644
--- a/src/wp-includes/html-api/class-wp-html-tag-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php
@@ -1219,7 +1219,7 @@ public function class_list() {
$name = substr( $class, $at, $length );
if ( $is_quirks ) {
- $name = strtolower( $name );
+ $name = WP_HTML_Decoder::ascii_lowercase( $name );
}
$at += $length;
@@ -1257,7 +1257,9 @@ public function has_class( $wanted_class ): ?bool {
foreach ( $this->class_list() as $class_name ) {
if (
strlen( $class_name ) === $wanted_length &&
- 0 === substr_compare( $class_name, $wanted_class, 0, strlen( $wanted_class ), $case_insensitive )
+ ( $case_insensitive
+ ? WP_HTML_Decoder::matches_ascii_case_insensitively( $class_name, $wanted_class )
+ : $class_name === $wanted_class )
) {
return true;
}
@@ -1448,10 +1450,7 @@ private function skip_rcdata( string $tag_name ): bool {
* normalization could not be part of a tag name.
*/
for ( $i = 0; $i < $tag_length; $i++ ) {
- $tag_char = $tag_name[ $i ];
- $html_char = $html[ $at + $i ];
-
- if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) {
+ if ( ! WP_HTML_Decoder::matches_ascii_case_insensitively( $html, $tag_name[ $i ], $at + $i ) ) {
$at += $i;
continue 2;
}
@@ -2267,7 +2266,7 @@ private function parse_next_attribute(): bool {
*
* @see https://html.spec.whatwg.org/#attribute-name-state
*/
- $comparable_name = strtolower( str_replace( "\x00", "\u{FFFD}", $attribute_name ) );
+ $comparable_name = WP_HTML_Decoder::ascii_lowercase( str_replace( "\x00", "\u{FFFD}", $attribute_name ) );
// If an attribute is listed many times, only use the first declaration and ignore the rest.
if ( ! isset( $this->attributes[ $comparable_name ] ) ) {
@@ -2446,7 +2445,7 @@ private function class_name_updates_to_attributes_updates(): void {
if ( $is_quirks ) {
foreach ( $this->classname_updates as $updated_name => $action ) {
if ( self::REMOVE_CLASS === $action ) {
- $to_remove[] = strtolower( $updated_name );
+ $to_remove[] = WP_HTML_Decoder::ascii_lowercase( $updated_name );
}
}
} else {
@@ -2473,7 +2472,7 @@ private function class_name_updates_to_attributes_updates(): void {
}
$name = substr( $existing_class, $at, $name_length );
- $comparable_class_name = $is_quirks ? strtolower( $name ) : $name;
+ $comparable_class_name = $is_quirks ? WP_HTML_Decoder::ascii_lowercase( $name ) : $name;
$at += $name_length;
// If this class is marked for removal, remove it and move on to the next one.
@@ -2509,7 +2508,7 @@ private function class_name_updates_to_attributes_updates(): void {
// Add new classes by appending those which haven't already been seen.
foreach ( $this->classname_updates as $name => $operation ) {
- $comparable_name = $is_quirks ? strtolower( $name ) : $name;
+ $comparable_name = $is_quirks ? WP_HTML_Decoder::ascii_lowercase( $name ) : $name;
if ( self::ADD_CLASS === $operation && ! in_array( $comparable_name, $seen, true ) ) {
$modified = true;
@@ -2809,7 +2808,7 @@ public function get_attribute( $name ) {
return null;
}
- $comparable = strtolower( $name );
+ $comparable = WP_HTML_Decoder::ascii_lowercase( $name );
/*
* For every attribute other than `class` it's possible to perform a quick check if
@@ -2912,7 +2911,7 @@ public function get_attribute_names_with_prefix( $prefix ): ?array {
return null;
}
- $comparable = strtolower( $prefix );
+ $comparable = WP_HTML_Decoder::ascii_lowercase( $prefix );
$matches = array();
foreach ( array_keys( $this->attributes ) as $attr_name ) {
@@ -2958,7 +2957,7 @@ public function get_tag(): ?string {
$tag_name = str_replace( "\x00", "\u{FFFD}", substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ) );
if ( self::STATE_MATCHED_TAG === $this->parser_state ) {
- return strtoupper( $tag_name );
+ return WP_HTML_Decoder::ascii_uppercase( $tag_name );
}
if (
@@ -2989,7 +2988,7 @@ public function get_qualified_tag_name(): ?string {
return $tag_name;
}
- $lower_tag_name = strtolower( $tag_name );
+ $lower_tag_name = WP_HTML_Decoder::ascii_lowercase( $tag_name );
if ( 'math' === $this->get_namespace() ) {
return $lower_tag_name;
}
@@ -3138,7 +3137,7 @@ public function get_qualified_attribute_name( $attribute_name ): ?string {
}
$namespace = $this->get_namespace();
- $lower_name = strtolower( $attribute_name );
+ $lower_name = WP_HTML_Decoder::ascii_lowercase( $attribute_name );
if ( 'math' === $namespace && 'definitionurl' === $lower_name ) {
return 'definitionURL';
@@ -3911,9 +3910,10 @@ public function set_modifiable_text( string $plaintext_content ): bool {
* HTML structure is rejected here. It’s the responsibility of calling code to
* perform whatever semantic escaping is necessary to avoid problematic strings.
*/
+ $comparable_content = WP_HTML_Decoder::ascii_lowercase( $plaintext_content );
if (
- false !== stripos( $plaintext_content, '' => array( '', 'Just a ' ),
'Non-JS SCRIPT with ', '