Skip to content
21 changes: 11 additions & 10 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2921,8 +2921,7 @@
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 @@ -6246,8 +6245,10 @@
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
*
* @return bool Whether the current token was handled without exhausting input.
*/
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 @@ -6259,13 +6260,13 @@
! $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 @@ -6286,20 +6287,20 @@
}
}

// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
// > If there is no such element, then act as described in the "any other end tag" entry above and return.
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();

Check warning on line 6292 in src/wp-includes/html-api/class-wp-html-processor.php

View workflow job for this annotation

GitHub Actions / PHP static analysis / Run PHP static analysis

Call to an undefined method WP_HTML_Processor::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 @@ -6335,7 +6336,7 @@

if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
return;
return true;
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,47 @@ public function test_unexpected_closing_tags_are_removed() {
);
}

/**
* Ensures that unexpected closing formatting tags are ignored.
*
* @ticket 65383
*
* @dataProvider data_formatting_tag_names
*
* @param string $formatting_tag_name Formatting tag name with no active formatting element.
*/
public function test_unexpected_closing_formatting_tags_are_ignored( string $formatting_tag_name ) {
$this->assertSame(
'onetwo',
WP_HTML_Processor::normalize( "one</{$formatting_tag_name}>two" ),
"Should have ignored unexpected {$formatting_tag_name} closer."
);
}

/**
* Data provider.
*
* @return array<string, array{0: string}>
*/
public static function data_formatting_tag_names(): array {
return array(
'A tag' => array( 'a' ),
'B tag' => array( 'b' ),
'BIG tag' => array( 'big' ),
'CODE tag' => array( 'code' ),
'EM tag' => array( 'em' ),
'FONT tag' => array( 'font' ),
'I tag' => array( 'i' ),
'NOBR tag' => array( 'nobr' ),
'S tag' => array( 's' ),
'SMALL tag' => array( 'small' ),
'STRIKE tag' => array( 'strike' ),
'STRONG tag' => array( 'strong' ),
'TT tag' => array( 'tt' ),
'U tag' => array( 'u' ),
);
}

/**
* Ensures that self-closing elements in foreign content retain their self-closing flag.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ public static function data_virtual_nodes_breadcrumbs() {
'Implied P tag opener on unmatched closer' => array( '</p>', 1, 'P', 'open', array( 'HTML', 'BODY', 'P' ) ),
'Implied heading tag closer on heading child' => array( '<h1><h2>', 2, 'H1', 'close', array( 'HTML', 'BODY' ) ),
'Implied A tag closer on A tag child' => array( '<a><a>', 2, 'A', 'close', array( 'HTML', 'BODY' ) ),
'Redundant A closer after sibling A' => array( '<a><a></a></a>', 4, 'A', 'close', array( 'HTML', 'BODY' ) ),
'Implied A tag closer on A tag descendent' => array( '<a><span><a>', 4, 'A', 'close', array( 'HTML', 'BODY' ) ),
);
}
Expand Down
86 changes: 86 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,92 @@ 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 65383
*
* @dataProvider data_in_body_adoption_agency_fallback_end_tags
*
* @param string $formatting_tag_name Formatting tag name with no active formatting element.
*/
public function test_in_body_adoption_agency_fallback_ignores_unexpected_formatting_end_tag( string $formatting_tag_name ) {
$processor = WP_HTML_Processor::create_fragment( "<div><span></{$formatting_tag_name}><code target></code></span></div>" );

$this->assertTrue( $processor->next_tag( 'SPAN' ), 'Failed to find the SPAN opener before an unexpected formatting end tag.' );
$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 before unexpected formatting closer.' );

$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 65383
*
* @dataProvider data_in_body_adoption_agency_fallback_end_tags
*
* @param string $tag_name Formatting tag name with no active formatting element.
*/
public function test_in_body_adoption_agency_fallback_preserves_ignored_end_tag_step_result( string $tag_name ): void {
$processor = WP_HTML_Processor::create_fragment( "</{$tag_name}>" );
$this->assertFalse( $processor->step(), "Expected unexpected {$tag_name} end tag followed by EOF to return false." );
}

/**
* Verifies that when the adoption agency algorithm returns after removing
* a formatting element from the active formatting elements list, it does
* not report the current token as EOF.
*
* @covers WP_HTML_Processor::step_in_body
*
* @ticket 65383
*
* @dataProvider data_in_body_adoption_agency_fallback_end_tags
*
* @param string $tag_name Formatting tag name with no open element.
*/
public function test_in_body_adoption_agency_removes_inactive_formatting_element_and_continues( string $tag_name ): void {
$processor = WP_HTML_Processor::create_fragment( "<p><{$tag_name}></p></{$tag_name}><span target></span>" );

$this->assertTrue( $processor->next_tag( $tag_name ), "Failed to find the {$tag_name} opener before it is popped by the P closer." );
$this->assertTrue( $processor->next_tag( 'SPAN' ), "Failed to advance past the inactive {$tag_name} closer to the following SPAN opener." );
$this->assertSame( array( 'HTML', 'BODY', 'SPAN' ), $processor->get_breadcrumbs(), "Expected SPAN to be a BODY child after the inactive {$tag_name} closer." );
}

/**
* Data provider.
*
* @return array<string, array{0: string}>
*/
public static function data_in_body_adoption_agency_fallback_end_tags(): array {
return array(
'A tag' => array( 'a' ),
'B tag' => array( 'b' ),
'BIG tag' => array( 'big' ),
'CODE tag' => array( 'code' ),
'EM tag' => array( 'em' ),
'FONT tag' => array( 'font' ),
'I tag' => array( 'i' ),
'NOBR tag' => array( 'nobr' ),
'S tag' => array( 's' ),
'SMALL tag' => array( 'small' ),
'STRIKE tag' => array( 'strike' ),
'STRONG tag' => array( 'strong' ),
'TT tag' => array( 'tt' ),
'U tag' => array( 'u' ),
);
}

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