From f95491ba6c7d6d47e0742ed3ed3b54a0647d3738 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Mon, 25 Sep 2023 19:15:29 +0000 Subject: [PATCH] HTML API: Remove all duplicate copies of an attribute when removing. When encountering an HTML tag with duplicate copies of an attribute the tag processor ignores the duplicate values, according to the specification. However, when removing an attribute it must remove all copies of that attribute lest one of the duplicates becomes the primary and it appears as if no attributes were removed. In this patch we're adding tests that will be used to ensure that all attribute copies are removed from a tag when one is request to be removed. **Before** {{{#!php ' ); $p->next_tag(); $p->remove_attribute( 'id' ); $p->get_updated_html(); //
}}} **After** {{{#!php ' ); $p->next_tag(); $p->remove_attribute( 'id' ); $p->get_updated_html(); //
}}} Previously we have been overlooking duplicate attributes since they don't have an impact on what parses into the DOM. However, as one unit test affirmed (asserting the presence of the bug in the tag processor) when removing an attribute where duplicates exist this meant we ended up changing the value of an attribute instead of removing it. In this patch we're tracking the text spans of the parsed duplicate attributes so that ''if'' we attempt to remove them then we'll have the appropriate information necessary to do so. When an attribute isn't removed we'll simply forget about the tracked duplicates. This involves some overhead for normal operation ''when'' in fact there are duplicate attributes on a tag, but that overhead is minimal in the form of integer pairs of indices for each duplicated attribute. Props dmsnell, zieladam. Merges [56684] to the 6.3 branch. Fixes #58119. Built from https://develop.svn.wordpress.org/branches/6.3@56685 git-svn-id: http://core.svn.wordpress.org/branches/6.3@56197 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- .../html-api/class-wp-html-tag-processor.php | 51 +++++++++++++++++-- wp-includes/version.php | 2 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/wp-includes/html-api/class-wp-html-tag-processor.php b/wp-includes/html-api/class-wp-html-tag-processor.php index 4ff4b8d124..95db0bf0ea 100644 --- a/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/wp-includes/html-api/class-wp-html-tag-processor.php @@ -406,6 +406,16 @@ class WP_HTML_Tag_Processor { */ private $attributes = array(); + /** + * Tracks spans of duplicate attributes on a given tag, used for removing + * all copies of an attribute when calling `remove_attribute()`. + * + * @since 6.3.2 + * + * @var (WP_HTML_Span[])[]|null + */ + private $duplicate_attributes = null; + /** * Which class names to add or remove from a tag. * @@ -1286,6 +1296,25 @@ class WP_HTML_Tag_Processor { $attribute_end, ! $has_value ); + + return true; + } + + /* + * Track the duplicate attributes so if we remove it, all disappear together. + * + * While `$this->duplicated_attributes` could always be stored as an `array()`, + * which would simplify the logic here, storing a `null` and only allocating + * an array when encountering duplicates avoids needless allocations in the + * normative case of parsing tags with no duplicate attributes. + */ + $duplicate_span = new WP_HTML_Span( $attribute_start, $attribute_end ); + if ( null === $this->duplicate_attributes ) { + $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); + } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { + $this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span ); + } else { + $this->duplicate_attributes[ $comparable_name ][] = $duplicate_span; } return true; @@ -1307,11 +1336,12 @@ class WP_HTML_Tag_Processor { */ private function after_tag() { $this->get_updated_html(); - $this->tag_name_starts_at = null; - $this->tag_name_length = null; - $this->tag_ends_at = null; - $this->is_closing_tag = null; - $this->attributes = array(); + $this->tag_name_starts_at = null; + $this->tag_name_length = null; + $this->tag_ends_at = null; + $this->is_closing_tag = null; + $this->attributes = array(); + $this->duplicate_attributes = null; } /** @@ -2080,6 +2110,17 @@ class WP_HTML_Tag_Processor { '' ); + // Removes any duplicated attributes if they were also present. + if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { + foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { + $this->lexical_updates[] = new WP_HTML_Text_Replacement( + $attribute_token->start, + $attribute_token->end, + '' + ); + } + } + return true; } diff --git a/wp-includes/version.php b/wp-includes/version.php index d6a74fdffc..42c46a983d 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.3.2-alpha-56624'; +$wp_version = '6.3.2-alpha-56685'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.