Skip to content
Draft
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
68 changes: 45 additions & 23 deletions src/wp-includes/html-api/class-wp-html-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3256,38 +3256,60 @@ private function step_in_body(): bool {
/*
* > 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();
}
/**
* Applies the "any other end tag" parsing instructions for the IN BODY insertion mode.
*
* @since 7.1.0
* @ignore
*
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#parsing-main-inbody
* @see WP_HTML_Processor::step_in_body
*
* @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 "any other end tag" IN BODY processing. Check HTML API code.' );
// This unnecessary return prevents tools from inaccurately reporting type errors.
return false;
}
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 @@ -265,6 +265,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 @@
'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' ) ),

Check warning on line 580 in tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php

View workflow job for this annotation

GitHub Actions / Coding standards / PHP checks

Array double arrow not aligned correctly; expected 10 space(s) between "'Redundant A closer after sibling A'" and double arrow, but found 11.
'Implied A tag closer on A tag descendent' => array( '<a><span><a>', 4, 'A', 'close', array( 'HTML', 'BODY' ) ),
);
}
Expand Down
67 changes: 67 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,73 @@ 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( "<span></{$tag_name}>" );
$this->assertTrue( $processor->next_token(), 'Failed to find the SPAN opener before an unexpected end tag.' );
$this->assertSame( 'SPAN', $processor->get_tag(), "Expected to start test on SPAN element but found {$processor->get_tag()} instead." );
$this->assertFalse( $processor->next_token(), "Expected unexpected {$tag_name} end tag followed by EOF to return false." );
}

/**
* 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