Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -989,8 +989,6 @@ public function expects_closer( ?WP_HTML_Token $node = null ): ?bool {
*
* @since 6.4.0
*
* @throws Exception When unable to allocate a bookmark for the next token in the input HTML document.
*
* @see self::PROCESS_NEXT_NODE
* @see self::REPROCESS_CURRENT_NODE
*
Expand Down Expand Up @@ -1040,8 +1038,13 @@ 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 ) {
$bookmark_name = $this->bookmark_token();
if ( false === $bookmark_name ) {
return false;
}

$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
Expand Down Expand Up @@ -1600,7 +1603,9 @@ private function step_before_html(): bool {
* > Switch the insertion mode to "before head", then reprocess the token.
*/
before_html_anything_else:
$this->insert_virtual_node( 'HTML' );
if ( ! $this->insert_virtual_node( 'HTML' ) ) {
return false;
}
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_BEFORE_HEAD;
return $this->step( self::REPROCESS_CURRENT_NODE );
}
Expand Down Expand Up @@ -1697,7 +1702,11 @@ private function step_before_head(): bool {
* > Insert an HTML element for a "head" start tag token with no attributes.
*/
before_head_anything_else:
$this->state->head_element = $this->insert_virtual_node( 'HEAD' );
$head_element = $this->insert_virtual_node( 'HEAD' );
if ( ! $head_element ) {
return false;
}
$this->state->head_element = $head_element;
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_HEAD;
return $this->step( self::REPROCESS_CURRENT_NODE );
}
Expand Down Expand Up @@ -2166,7 +2175,9 @@ private function step_after_head(): bool {
* > Insert an HTML element for a "body" start tag token with no attributes.
*/
after_head_anything_else:
$this->insert_virtual_node( 'BODY' );
if ( ! $this->insert_virtual_node( 'BODY' ) ) {
return false;
}
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY;
return $this->step( self::REPROCESS_CURRENT_NODE );
}
Expand Down Expand Up @@ -3318,7 +3329,9 @@ private function step_in_table(): bool {
* > Insert an HTML element for a "colgroup" start tag token with no attributes,
* > then switch the insertion mode to "in column group".
*/
$this->insert_virtual_node( 'COLGROUP' );
if ( ! $this->insert_virtual_node( 'COLGROUP' ) ) {
return false;
}
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_COLUMN_GROUP;
return $this->step( self::REPROCESS_CURRENT_NODE );

Expand All @@ -3344,7 +3357,9 @@ private function step_in_table(): bool {
* > Insert an HTML element for a "tbody" start tag token with no attributes,
* > then switch the insertion mode to "in table body".
*/
$this->insert_virtual_node( 'TBODY' );
if ( ! $this->insert_virtual_node( 'TBODY' ) ) {
return false;
}
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_TABLE_BODY;
return $this->step( self::REPROCESS_CURRENT_NODE );

Expand Down Expand Up @@ -3699,7 +3714,9 @@ private function step_in_table_body(): bool {
case '+TD':
// @todo Indicate a parse error once it's possible.
$this->state->stack_of_open_elements->clear_to_table_body_context();
$this->insert_virtual_node( 'TR' );
if ( ! $this->insert_virtual_node( 'TR' ) ) {
return false;
}
$this->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_ROW;
return $this->step( self::REPROCESS_CURRENT_NODE );

Expand Down Expand Up @@ -5100,14 +5117,12 @@ private function step_in_foreign_content(): bool {
* @since 6.4.0
* @since 6.5.0 Renamed from bookmark_tag() to bookmark_token().
*
* @throws Exception When unable to allocate requested bookmark.
*
* @return string|false Name of created bookmark, or false if unable to create.
*/
private function bookmark_token() {
if ( ! parent::set_bookmark( ++$this->bookmark_counter ) ) {
$this->last_error = self::ERROR_EXCEEDED_MAX_BOOKMARKS;
throw new Exception( 'could not allocate bookmark' );
return false;
}

return "{$this->bookmark_counter}";
Expand Down Expand Up @@ -6290,11 +6305,14 @@ private function insert_foreign_element( WP_HTML_Token $token, bool $only_add_to
* @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.
* @return WP_HTML_Token Newly-created virtual token.
* @return WP_HTML_Token|false Newly-created virtual token or false on failure.
*/
private function insert_virtual_node( $token_name, $bookmark_name = null ): WP_HTML_Token {
private function insert_virtual_node( $token_name, $bookmark_name = null ) {
$here = $this->bookmarks[ $this->state->current_token->bookmark_name ];
$name = $bookmark_name ?? $this->bookmark_token();
if ( false === $name ) {
return false;
}

$this->bookmarks[ $name ] = new WP_HTML_Span( $here->start, 0 );

Expand Down
65 changes: 64 additions & 1 deletion tests/phpunit/tests/html-api/wpHtmlProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <DIV> is irrelevant but illustrates the case-insentivity.
Expand All @@ -1079,4 +1079,67 @@ public function test_next_tag_lowercase_tag_name() {
$processor = WP_HTML_Processor::create_fragment( '<svg><RECT>' );
$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( '<i>', WP_HTML_Processor::MAX_BOOKMARKS * 2 );
$processor = WP_HTML_Processor::create_fragment( $html );

// The fragment parser starts with a few context tokens already bookmarked.
$reached_tokens = ( fn() => count( $this->bookmarks ) )->call( $processor );
while ( $processor->next_token() ) {
++$reached_tokens;
}
$this->assertSame( WP_HTML_Processor::MAX_BOOKMARKS, $reached_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() {
$html = str_repeat( '<table><td>', WP_HTML_Processor::MAX_BOOKMARKS * 2 );
$processor = WP_HTML_Processor::create_fragment( $html );

// The fragment parser starts with a few context tokens already bookmarked.
$reached_tokens = ( fn() => count( $this->bookmarks ) )->call( $processor );
while ( $processor->next_token() ) {
++$reached_tokens;
}

/*
* This test has some variability depending on how the virtual tokens align.
* It will produce 1 real, 2 virtual, 1 real.
*
* "<table><td><table><td>…" produces:
* └─TABLE
* └─TBODY (virtual)
* └─TR (virtual)
* └─TD
* └─TABLE
* └─TBODY (virtual)
* └─TR (virtual)
* └─TD
* └─…
*/
$this->assertGreaterThanOrEqual( WP_HTML_Processor::MAX_BOOKMARKS - 1, $reached_tokens );
$this->assertLessThanOrEqual( WP_HTML_Processor::MAX_BOOKMARKS + 1, $reached_tokens );
$this->assertSame(
WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS,
$processor->get_last_error(),
'Failed to report exceeded-max-bookmarks error.'
);
}
}
Loading