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 8a6335378781a..f9a449c3c6fb7 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1042,8 +1042,17 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool { $token_name = $this->get_token_name(); if ( self::REPROCESS_CURRENT_NODE !== $node_to_process ) { + try { + $bookmark_name = $this->bookmark_token(); + } catch ( Exception $e ) { + if ( self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->last_error ) { + return false; + } + throw $e; + } + $this->state->current_token = new WP_HTML_Token( - $this->bookmark_token(), + $bookmark_name, $token_name, $this->has_self_closing_flag(), $this->release_internal_bookmark_on_destruct @@ -1153,6 +1162,12 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ): bool { * otherwise might involve messier calling and return conventions. */ return false; + } catch ( Exception $e ) { + if ( self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->last_error ) { + return false; + } + // Rethrow any other exceptions for higher-level handling. + throw $e; } } @@ -6315,6 +6330,8 @@ private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to * * @since 6.7.0 * + * @throws Exception When unable to allocate a bookmark for the next token in the input HTML document. + * * @param string $token_name Name of token to create and insert into the stack of open elements. * @param string|null $bookmark_name Optional. Name to give bookmark for created virtual node. * Defaults to auto-creating a bookmark name. diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index 13e0728ca912a..a89014282df73 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -1068,7 +1068,7 @@ public function test_ensure_next_token_method_extensibility( $html, $expected_to /** * Ensure that lowercased tag_name query matches tags case-insensitively. * - * @group 62427 + * @ticket 62427 */ public function test_next_tag_lowercase_tag_name() { // The upper case
is irrelevant but illustrates the case-insentivity. @@ -1079,4 +1079,106 @@ public function test_next_tag_lowercase_tag_name() { $processor = WP_HTML_Processor::create_fragment( '' ); $this->assertTrue( $processor->next_tag( array( 'tag_name' => 'rect' ) ) ); } + + /** + * Ensure that the processor does not throw errors in cases of extreme HTML nesting. + * + * @ticket 64394 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark + */ + public function test_deep_nesting_fails_process_without_error() { + $html = str_repeat( '', WP_HTML_Processor::MAX_BOOKMARKS * 2 ); + $processor = WP_HTML_Processor::create_fragment( $html ); + + while ( $processor->next_token() ) { + // Process tokens. + } + + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + } + + /** + * @ticket 64394 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark + */ + public function test_deep_nesting_fails_processing_virtual_tokens_without_error() { + /* + * This test has some variability depending on how the virtual tokens align. + * In order to ensure that bookmarks are exhausted on a virtual token + * without throwing an error, 3 documents are parsed with different "offsets" + * to ensure that the bookmarks are exhaused on a virtual token in at least one of the runs. + * + * "
…" produces: + * └─TABLE (real) + * └─TBODY (virtual) + * └─TR (virtual) + * └─TD (real) + * └─TABLE (real) + * └─TBODY (virtual) + * └─TR (virtual) + * └─TD (real) + * └─… + */ + $html_table_td = str_repeat( '
', WP_HTML_Processor::MAX_BOOKMARKS * 2 ); + + // Offset 0 + $processor = WP_HTML_Processor::create_fragment( $html_table_td ); + while ( $processor->next_token() ) { + // Process tokens. + } + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + + // Offset 1 + $processor = WP_HTML_Processor::create_fragment( "
{$html_table_td}" ); + while ( $processor->next_token() ) { + // Process tokens. + } + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + + // Offset 2 + $processor = WP_HTML_Processor::create_fragment( "
{$html_table_td}" ); + while ( $processor->next_token() ) { + // Process tokens. + } + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + } + + /** + * @ticket 64394 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark + */ + public function test_prevents_unbounded_bookmarking() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag(); + + // This might fail before the MAX_BOOKMARK limit, which is okay. + foreach ( range( 0, WP_HTML_Processor::MAX_BOOKMARKS ) as $n ) { + if ( ! $processor->set_bookmark( "{$n}" ) ) { + break; + } + } + + $this->assertFalse( + $processor->set_bookmark( 'beyond the limit' ) + ); + } }