From cdb218b2007693262f2ea2d9717ec0ff55620b80 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Thu, 1 Feb 2024 00:41:40 +0000 Subject: [PATCH] HTML API: Fix void tag nesting with next_token When `next_token()` was introduced, it introduced a regression in the HTML Processor whereby void tags remain on the stack of open elements when they shouldn't. This led to invalid values returned from `get_breadcrumbs()`. The reason was that calling `next_token()` works through a different code path than the HTML Processor runs everything else. To solve this, its sub-classed `next_token()` called `step( self::REPROCESS_CURRENT_TOKEN )` so that the proper HTML accounting takes place. Unfortunately that same reprocessing code path skipped the step whereby void and self-closing elements are popped from the stack of open elements. In this patch, that step is run with a third mode for `step()`, which is the new `self::PROCESS_CURRENT_TOKEN`. This mode acts as if `self::PROCESS_NEXT_NODE` were called, except it doesn't advance the parser. Developed in https://github.com/WordPress/wordpress-develop/pull/5975 Discussed in https://core.trac.wordpress.org/ticket/60382 Follow-up to [57348] Props dmsnell, jonsurrell Fixes #60382 git-svn-id: https://develop.svn.wordpress.org/trunk@57507 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 15 +++++- .../tests/html-api/wpHtmlProcessor.php | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) 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 4bde9e1c099c8..6b0879cde892f 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -431,7 +431,7 @@ public function next_token() { $found_a_token = parent::next_token(); if ( '#tag' === $this->get_token_type() ) { - $this->step( self::REPROCESS_CURRENT_NODE ); + $this->step( self::PROCESS_CURRENT_NODE ); } return $found_a_token; @@ -513,7 +513,7 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { return false; } - if ( self::PROCESS_NEXT_NODE === $node_to_process ) { + if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) { /* * Void elements still hop onto the stack of open elements even though * there's no corresponding closing tag. This is important for managing @@ -532,7 +532,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { if ( $top_node && self::is_void( $top_node->node_name ) ) { $this->state->stack_of_open_elements->pop(); } + } + if ( self::PROCESS_NEXT_NODE === $node_to_process ) { while ( parent::next_token() && '#tag' !== $this->get_token_type() ) { continue; } @@ -1781,6 +1783,15 @@ public static function is_void( $tag_name ) { */ const REPROCESS_CURRENT_NODE = 'reprocess-current-node'; + /** + * Indicates that the current HTML token should be processed without advancing the parser. + * + * @since 6.5.0 + * + * @var string + */ + const PROCESS_CURRENT_NODE = 'process-current-node'; + /** * Indicates that the parser encountered unsupported markup and has bailed. * diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 109f384e25e3c..60d368efae1cc 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -188,6 +188,59 @@ public function test_cannot_nest_void_tags( $tag_name ) { ); } + /** + * Ensure non-nesting tags do not nest when processing tokens. + * + * @ticket 60382 + * + * @dataProvider data_void_tags + * + * @param string $tag_name Name of void tag under test. + */ + public function test_cannot_nest_void_tags_next_token( $tag_name ) { + $processor = WP_HTML_Processor::create_fragment( "<{$tag_name}>
" ); + + /* + * This HTML represents the same as the following HTML, + * assuming that it were provided `` as the tag: + * + * + * + * + *
+ * + * + */ + + $found_tag = $processor->next_token(); + + if ( WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error() ) { + $this->markTestSkipped( "Tag {$tag_name} is not supported." ); + } + + $this->assertTrue( + $found_tag, + "Could not find first {$tag_name}." + ); + + $this->assertSame( + array( 'HTML', 'BODY', $tag_name ), + $processor->get_breadcrumbs(), + 'Found incorrect nesting of first element.' + ); + + $this->assertTrue( + $processor->next_token(), + 'Should have found the DIV as the second tag.' + ); + + $this->assertSame( + array( 'HTML', 'BODY', 'DIV' ), + $processor->get_breadcrumbs(), + "DIV should have been a sibling of the {$tag_name}." + ); + } + /** * Data provider. *