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
89 changes: 54 additions & 35 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2913,8 +2913,7 @@ private function step_in_body(): bool {
case '-STRONG':
case '-TT':
case '-U':
$this->run_adoption_agency_algorithm();
return true;
return $this->run_adoption_agency_algorithm();

/*
* > A start tag whose tag name is one of: "applet", "marquee", "object"
Expand Down Expand Up @@ -3246,41 +3245,57 @@ private function step_in_body(): bool {
$this->insert_html_element( $this->state->current_token );
return true;
} else {
/*
* > Any other end tag
*/
return $this->step_in_body_any_other_end_tag();
}

/*
* Find the corresponding tag opener in the stack of open elements, if
* it exists before reaching a special element, which provides a kind
* of boundary in the stack. For example, a `</custom-tag>` should not
* close anything beyond its containing `P` or `DIV` element.
*/
foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
break;
}
$this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
// This unnecessary return prevents tools from inaccurately reporting type errors.
return false;
}

if ( self::is_special( $node ) ) {
// This is a parse error, ignore the token.
return $this->step();
}
/**
* Parses an "any other end tag" token in the "in body" insertion mode.
*
* @since 7.1.0
* @ignore
*
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @return bool Whether an element was found.
*/
private function step_in_body_any_other_end_tag(): bool {
$token_name = $this->get_token_name();

/*
* Find the corresponding tag opener in the stack of open elements, if
* it exists before reaching a special element, which provides a kind
* of boundary in the stack. For example, a `</custom-tag>` should not
* close anything beyond its containing `P` or `DIV` element.
*/
foreach ( $this->state->stack_of_open_elements->walk_up() as $node ) {
if ( 'html' === $node->namespace && $token_name === $node->node_name ) {
break;
}

$this->generate_implied_end_tags( $token_name );
if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
// @todo Record parse error: this error doesn't impact parsing.
if ( self::is_special( $node ) ) {
// This is a parse error, ignore the token.
return $this->step();
}
}

foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
$this->state->stack_of_open_elements->pop();
if ( $node === $item ) {
return true;
}
$this->generate_implied_end_tags( $token_name );
if ( $node !== $this->state->stack_of_open_elements->current_node() ) {
// @todo Record parse error: this error doesn't impact parsing.
}

foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
$this->state->stack_of_open_elements->pop();
if ( $node === $item ) {
return true;
}
}

$this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' );
$this->bail( 'Should not have been able to reach end of IN BODY "any other end tag" processing. Check HTML API code.' );
// This unnecessary return prevents tools from inaccurately reporting type errors.
return false;
}
Expand Down Expand Up @@ -6216,8 +6231,10 @@ private function reset_insertion_mode_appropriately(): void {
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
*
* @return bool Whether an element was found.
*/
private function run_adoption_agency_algorithm(): void {
private function run_adoption_agency_algorithm(): bool {
$budget = 1000;
$subject = $this->get_tag();
$current_node = $this->state->stack_of_open_elements->current_node();
Expand All @@ -6229,13 +6246,13 @@ private function run_adoption_agency_algorithm(): void {
! $this->state->active_formatting_elements->contains_node( $current_node )
) {
$this->state->stack_of_open_elements->pop();
return;
return true;
}

$outer_loop_counter = 0;
while ( $budget-- > 0 ) {
if ( $outer_loop_counter++ >= 8 ) {
return;
return true;
}

/*
Expand All @@ -6258,18 +6275,18 @@ private function run_adoption_agency_algorithm(): void {

// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
$this->bail( 'Cannot run adoption agency when "any other end tag" is required.' );
return $this->step_in_body_any_other_end_tag();
}

// > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
return;
return true;
}

// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
return;
return true;
}

/*
Expand Down Expand Up @@ -6305,7 +6322,7 @@ private function run_adoption_agency_algorithm(): void {

if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
return;
return true;
}
}
}
Expand All @@ -6314,6 +6331,8 @@ private function run_adoption_agency_algorithm(): void {
}

$this->bail( 'Cannot run adoption agency when looping required.' );
// This unnecessary return prevents tools from inaccurately reporting type errors.
return false;
}

/**
Expand Down
30 changes: 30 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,36 @@ public function test_unexpected_closing_tags_are_removed() {
);
}

/**
* Ensures that unexpected closing formatting tags are ignored.
*
* @ticket 65372
*
* @dataProvider data_unexpected_closing_formatting_tags
*
* @param string $html HTML containing an unexpected closing formatting tag.
* @param string $expected Expected normalized output.
*/
public function test_unexpected_closing_formatting_tags_are_ignored( string $html, string $expected ) {
$this->assertSame(
$expected,
WP_HTML_Processor::normalize( $html ),
'Should have ignored unexpected closing formatting tags.'
);
}

/**
* Data provider.
*
* @return array[]
*/
public static function data_unexpected_closing_formatting_tags() {
return array(
'Unexpected A end tag' => array( 'one</a>two', 'onetwo' ),
'Unexpected B end tag' => array( 'one</b>two', 'onetwo' ),
);
}

/**
* Ensures that self-closing elements in foreign content retain their self-closing flag.
*
Expand Down
60 changes: 60 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlProcessorSemanticRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,66 @@ public function test_in_body_any_other_end_tag_with_unclosed_non_special_element
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'DIV' ), $processor->get_breadcrumbs(), 'Failed to produce expected DOM nesting: SPAN should be closed and DIV should be its sibling.' );
}

/**
* Verifies that when the adoption agency algorithm finds no matching
* active formatting element, it acts like "any other end tag".
*
* @covers WP_HTML_Processor::step_in_body
*
* @ticket 65372
*
* @dataProvider data_in_body_adoption_agency_falls_back_to_any_other_end_tag
*
* @param string $formatting_tag_name Formatting tag name with no active formatting element.
*/
public function test_in_body_adoption_agency_falls_back_to_any_other_end_tag( string $formatting_tag_name ) {
$processor = WP_HTML_Processor::create_fragment( "<div><span></{$formatting_tag_name}><code target></code></span></div>" );

$processor->next_tag( 'SPAN' );
$this->assertSame( 'SPAN', $processor->get_tag(), "Expected to start test on SPAN element but found {$processor->get_tag()} instead." );
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN' ), $processor->get_breadcrumbs(), 'Failed to produce expected DOM nesting.' );

$this->assertTrue( $processor->next_tag( 'CODE' ), "Failed to ignore unexpected {$formatting_tag_name} closer and advance to CODE opener." );
$this->assertSame( 'CODE', $processor->get_tag(), "Expected to find CODE element, but found {$processor->get_tag()} instead." );
$this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN', 'CODE' ), $processor->get_breadcrumbs(), 'Failed to keep SPAN open after unexpected formatting closer.' );
}

/**
* Verifies that the adoption agency fallback preserves the "any other end tag"
* step result when the ignored token is followed by EOF.
*
* @covers WP_HTML_Processor::step_in_body
*
* @ticket 65372
*
* @dataProvider data_in_body_adoption_agency_falls_back_to_any_other_end_tag
*
* @param string $formatting_tag_name Formatting tag name with no active formatting element.
*/
public function test_in_body_adoption_agency_fallback_preserves_any_other_end_tag_step_result( string $formatting_tag_name ) {
$ordinary_processor = WP_HTML_Processor::create_fragment( '<span></x>' );
$this->assertTrue( $ordinary_processor->step(), 'Failed to find the SPAN opener before an ordinary unexpected end tag.' );
$this->assertSame( 'SPAN', $ordinary_processor->get_tag(), "Expected to start test on SPAN element but found {$ordinary_processor->get_tag()} instead." );
$this->assertFalse( $ordinary_processor->step(), 'Expected ordinary unexpected end tag followed by EOF to return false.' );

$formatting_processor = WP_HTML_Processor::create_fragment( "<span></{$formatting_tag_name}>" );
$this->assertTrue( $formatting_processor->step(), 'Failed to find the SPAN opener before an unexpected formatting end tag.' );
$this->assertSame( 'SPAN', $formatting_processor->get_tag(), "Expected to start test on SPAN element but found {$formatting_processor->get_tag()} instead." );
$this->assertFalse( $formatting_processor->step(), 'Expected unexpected formatting end tag followed by EOF to return false.' );
}

/**
* Data provider.
*
* @return array[]
*/
public static function data_in_body_adoption_agency_falls_back_to_any_other_end_tag() {
return array(
'Unexpected A end tag' => array( 'a' ),
'Unexpected B end tag' => array( 'b' ),
);
}

/**
* Ensures that closing `</br>` tags are appropriately treated as opening tags with no attributes.
*
Expand Down
Loading