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 7e047c4808081..4a6abf65846d6 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 @@ -1232,12 +1232,12 @@ public function class_list() { $at += $length; /* - * It's expected that the number of class names for a given tag is relatively small. - * Given this, it is probably faster overall to scan an array for a value rather - * than to use the class name as a key and check if it's a key of $seen. + * `$seen` is keyed by class name. PHP stores a canonical integer string + * such as "123" as the integer key 123, and no other string maps to that + * key, so distinct names stay distinct. */ - if ( ! in_array( $name, $seen, true ) ) { - $seen[] = $name; + if ( ! isset( $seen[ $name ] ) ) { + $seen[ $name ] = true; yield $name; } @@ -2502,19 +2502,24 @@ private function class_name_updates_to_attributes_updates(): void { */ $modified = false; + /* + * `$seen` and `$to_remove` are keyed by class name. PHP stores a canonical + * integer string such as "123" as the integer key 123, and no other string + * maps to that key, so distinct names stay distinct. + */ $seen = array(); $to_remove = array(); $is_quirks = self::QUIRKS_MODE === $this->compat_mode; if ( $is_quirks ) { foreach ( $this->classname_updates as $updated_name => $action ) { if ( self::REMOVE_CLASS === $action ) { - $to_remove[] = strtolower( $updated_name ); + $to_remove[ strtolower( $updated_name ) ] = true; } } } else { foreach ( $this->classname_updates as $updated_name => $action ) { if ( self::REMOVE_CLASS === $action ) { - $to_remove[] = $updated_name; + $to_remove[ $updated_name ] = true; } } } @@ -2539,17 +2544,17 @@ private function class_name_updates_to_attributes_updates(): void { $at += $name_length; // If this class is marked for removal, remove it and move on to the next one. - if ( in_array( $comparable_class_name, $to_remove, true ) ) { + if ( isset( $to_remove[ $comparable_class_name ] ) ) { $modified = true; continue; } // If a class has already been seen then skip it; it should not be added twice. - if ( in_array( $comparable_class_name, $seen, true ) ) { + if ( isset( $seen[ $comparable_class_name ] ) ) { continue; } - $seen[] = $comparable_class_name; + $seen[ $comparable_class_name ] = true; /* * Otherwise, append it to the new "class" attribute value. @@ -2572,7 +2577,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; - if ( self::ADD_CLASS === $operation && ! in_array( $comparable_name, $seen, true ) ) { + if ( self::ADD_CLASS === $operation && ! isset( $seen[ $comparable_name ] ) ) { $modified = true; $class .= strlen( $class ) > 0 ? ' ' : ''; diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index 1bfaa01bf0453..0647fb634d1d5 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -2691,6 +2691,7 @@ public function test_class_list_null_bytes_replaced() { /** * @ticket 59209 * @ticket 65846 + * @ticket 65967 * * @covers WP_HTML_Tag_Processor::class_list * @@ -2742,6 +2743,8 @@ public static function data_class_list(): array { 'Decimal reference without semicolon' => array( "
", array( 'a', 'b' ) ), 'Hex reference without semicolon' => array( "
", array( 'a', 'g' ) ), 'Bare ampersand' => array( "
", array( '&' ) ), + 'Numeric-looking names stay distinct' => array( "
", array( '123', '0123', '1e3', '-0', '0' ) ), + 'Numeric duplicates collapse' => array( "
", array( '123' ) ), ); } @@ -2800,6 +2803,33 @@ public static function data_html_with_variations_of_class_values_and_sought_clas ); } + /** + * Ensures that add_class() and remove_class() match class names which are + * canonical integer strings, such as "123", and only those. + * + * @ticket 65967 + * + * @covers WP_HTML_Tag_Processor::add_class + * @covers WP_HTML_Tag_Processor::remove_class + */ + public function test_add_class_and_remove_class_match_numeric_class_names() { + $processor = new WP_HTML_Tag_Processor( '
' ); + $processor->next_tag(); + $processor->remove_class( '123' ); + + $this->assertSame( '
', $processor->get_updated_html(), 'Failed to remove class name "123".' ); + + $processor = new WP_HTML_Tag_Processor( '
' ); + $processor->next_tag(); + $processor->add_class( '123' ); + + $this->assertSame( '
', $processor->get_updated_html(), 'Added a second copy of class name "123".' ); + + $processor->remove_class( '0123' ); + + $this->assertSame( '
', $processor->get_updated_html(), 'Did not remove class name "0123" alone.' ); + } + /** * Ensures that the invalid comment closing syntax "--!>" properly closes a comment. *