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
72 changes: 48 additions & 24 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,55 @@ private function step_in_body(): bool {
/*
* > Any other end tag
*/
return $this->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();
}
/**
* In body insertion mode's "any other end tag" logic can be invoked from different places
* and may require additional processing.
*
* @param $should_step bool Set to `false` to prevent advancing the parser in case the token
* is ignored.
* @return bool Whether an element was found.
*/
private function in_body_any_other_end_tag( bool $should_step = true ): 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 $should_step ? $this->step() : false;
}
}

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 @@ -6265,7 +6282,14 @@ 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.' );
/*
* The Adoption Agency Algorithm is not responsible for advancing the parser state,
* so `in_body_any_other_end_tag` is invoked with `$should_step = false` argument.
* This algorithm will return to the context that called it, which is responsible
* for advancing the parser.
*/
$this->in_body_any_other_end_tag( false );
return;
}

// > 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.
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 65372
*
* @dataProvider data_unexpected_closing_formatting_tags
*
* @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[]
*/
public static function data_unexpected_closing_formatting_tags() {
return array(
'Unexpected A end tag' => array( 'a' ),
'Unexpected B end tag' => array( 'b' ),
'Unexpected BIG end tag' => array( 'big' ),
'Unexpected CODE end tag' => array( 'code' ),
'Unexpected EM end tag' => array( 'em' ),
'Unexpected FONT end tag' => array( 'font' ),
'Unexpected I end tag' => array( 'i' ),
'Unexpected NOBR end tag' => array( 'nobr' ),
'Unexpected S end tag' => array( 's' ),
'Unexpected SMALL end tag' => array( 'small' ),
'Unexpected STRIKE end tag' => array( 'strike' ),
'Unexpected STRONG end tag' => array( 'strong' ),
'Unexpected TT end tag' => array( 'tt' ),
'Unexpected U end 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' ) ),
'Explicit 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
72 changes: 72 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,78 @@ 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_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 65372
*
* @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_preserves_ignored_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_tag_name} end tag followed by EOF to return false." );
}

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

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