Make WordPress Core

Changeset 63913


Ignore:
Timestamp:
09/24/2026 05:53:51 AM (4 days ago)
Author:
jonsurrell
Message:

Block Processor: Ensure block delimiters align with single HTML comments.

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/13680

Props jonsurrell, dmsnell.
See #66138.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block-processor.php

    r63911 r63913  
    980980                         * even though it allows for matching invalid JSON content.
    981981                         *
     982                         * The delimiter must also be a single complete HTML comment.
     983                         *
    982984                         * <!-- /wp:core/paragraph {"dropCap":true} /-⃨-⃨>⃨
    983985                         */
    984                         $comment_closing_at = strpos( $text, '-->', $json_at );
    985                         if ( false === $comment_closing_at ) {
     986                        $after_comment_end = $this->find_html_comment_end( $comment_opening_at, $end );
     987
     988                        /*
     989                         * The reported end of the comment could be after the end of the document if
     990                         * no actual end was found, so differentiate a comment ending at the end of
     991                         * the document from documents with missing comment ends.
     992                         */
     993                        if ( $after_comment_end >= $end && ! str_ends_with( $text, '-->' ) && ! str_ends_with( $text, '--!>' ) ) {
    986994                                goto incomplete;
    987995                        }
     996
     997                        /*
     998                         * Only normative comment closers are recognized block delimiters,
     999                         * so skip past any HTML comments ending in `--!>`.
     1000                         */
     1001                        if ( '!' === $text[ $after_comment_end - 2 ] ) {
     1002                                $at = $after_comment_end;
     1003                                continue;
     1004                        }
     1005
     1006                        $comment_closing_at = $after_comment_end - 3;
    9881007
    9891008                        // <!-- /wp:core/paragraph {"dropCap":true} /⃨-->
    … …  
    10081027                                }
    10091028
    1010                                 $at = $this->find_html_comment_end( $comment_opening_at, $end );
     1029                                $at = $after_comment_end;
    10111030                                continue;
    10121031                        }
    … …  
    10161035                         *
    10171036                         * @todo It’s likely faster to scan forward instead of in reverse.
     1037                         * @todo Skip ahead with `strcspn()` and decide only on syntax characters.
    10181038                         *
    10191039                         * <!-- /wp:core/paragraph {"dropCap":true}⃨ ⃨/-->
    … …  
    10371057
    10381058                                        default:
    1039                                                 ++$at;
     1059                                                $at = $after_comment_end;
    10401060                                                continue 3;
    10411061                                }
    … …  
    10471067                         */
    10481068                        if ( 0 === $json_length || 0 === $after_json_whitespace_length ) {
    1049                                 $at = $this->find_html_comment_end( $comment_opening_at, $end );
     1069                                $at = $after_comment_end;
    10501070                                continue;
    10511071                        }
    … …  
    10581078                // The end of the document was reached without a match.
    10591079                if ( self::MATCHED !== $this->state ) {
     1080                        // Stop at top-level free-form HTML at the end of the document.
     1081                        if ( $after_prev_delimiter < $end ) {
     1082                                $this->state                    = self::HTML_SPAN;
     1083                                $this->after_previous_delimiter = $after_prev_delimiter;
     1084                                $this->matched_delimiter_at     = $end;
     1085                                $this->matched_delimiter_length = 0;
     1086                                $this->open_blocks_at[]         = $after_prev_delimiter;
     1087                                $this->open_blocks_length[]     = 0;
     1088                                $this->was_void                 = true;
     1089
     1090                                return true;
     1091                        }
     1092
    10601093                        $this->state = self::COMPLETE;
    10611094                        return false;
    … …  
    13371370                        '>' === $text[ $comment_starting_at + 2 + $span_of_dashes ]
    13381371                ) {
    1339                         return $comment_starting_at + $span_of_dashes + 1;
     1372                        return $comment_starting_at + 2 + $span_of_dashes + 1;
    13401373                }
    13411374
  • trunk/tests/phpunit/tests/block-processor/wpBlockProcessor.php

    r63911 r63913  
    311311
    312312        /**
     313         * Verifies that HTML comments which end a document but which are not block
     314         * delimiters are incorporated into the final HTML span.
     315         *
     316         * @ticket 66138
     317         *
     318         * @dataProvider data_terminal_non_delimiter_comments
     319         *
     320         * @param string $html Input ending in a non-delimiter HTML comment.
     321         */
     322        public function test_preserves_terminal_non_delimiter_comments_as_html( string $html ): void {
     323                $processor = new WP_Block_Processor( $html );
     324
     325                $this->assertTrue(
     326                        $processor->next_token(),
     327                        'Should have found a single HTML span but found nothing: check test setup.'
     328                );
     329
     330                $this->assertTrue(
     331                        $processor->is_html(),
     332                        'Should have matched an HTML span.'
     333                );
     334
     335                $this->assertSame(
     336                        $html,
     337                        $processor->get_html_content(),
     338                        'Should have preserved the complete terminal HTML span.'
     339                );
     340
     341                $this->assertFalse(
     342                        $processor->next_token(),
     343                        'Should have only found one token, an HTML comment, but found more: check test setup.'
     344                );
     345
     346                $this->assertNull(
     347                        $processor->get_last_error(),
     348                        'Should have finished without error.'
     349                );
     350        }
     351
     352        /**
     353         * Data provider.
     354         *
     355         * @return array<string, array{0: string}>
     356         */
     357        public static function data_terminal_non_delimiter_comments(): array {
     358                return array(
     359                        'Ordinary comment'           => array( 'content<!-- x -->' ),
     360                        'Abrupt empty comment'       => array( 'content<!-->' ),
     361                        'Rejected JSON attributes'   => array( 'content<!-- wp:a {x -->' ),
     362                        'Exclamation comment ending' => array( 'content<!-- wp:a --!>' ),
     363                );
     364        }
     365
     366        /**
    313367         * Verifies that incomplete HTML comments which could not produce delimiters
    314368         * are not considered incomplete input by the processor.
    … …  
    536590                        $processor->next_delimiter(),
    537591                        "Should have failed to find block delimiter but found '{$processor->get_block_type()}' instead."
     592                );
     593        }
     594
     595        /**
     596         * Verifies that a delimiter does not span the end of the comment it started in.
     597         *
     598         * An HTML comment ends at its first `-->` or `--!>`. The scan looked only for
     599         * `-->`, so a comment ended by `--!>` was stepped over and a later `-->` was
     600         * taken as the end of the delimiter, producing a delimiter which spanned two
     601         * comments.
     602         *
     603         * @ticket 66138
     604         *
     605         * @dataProvider data_delimiters_and_comment_endings
     606         *
     607         * @param string   $html        Document to scan.
     608         * @param string[] $block_types Printable block type of every delimiter in the document, in order.
     609         */
     610        public function test_delimiter_does_not_span_a_comment_ending( string $html, array $block_types ): void {
     611                $processor = new WP_Block_Processor( $html );
     612
     613                $found = array();
     614                while ( $processor->next_delimiter() ) {
     615                        $found[] = $processor->get_printable_block_type();
     616                }
     617
     618                $this->assertSame(
     619                        $block_types,
     620                        $found,
     621                        'Should have found only the delimiters which are in the document.'
     622                );
     623        }
     624
     625        /**
     626         * Verifies that a comment ended by `--!>` at the end of the document is not
     627         * reported as incomplete input: the comment is closed, so no delimiter could
     628         * be completed by further input.
     629         *
     630         * @ticket 66138
     631         */
     632        public function test_exclamation_ending_at_end_of_document_is_not_incomplete_input(): void {
     633                $processor = new WP_Block_Processor( '<!-- wp:a {"k":"x --!>' );
     634
     635                $this->assertFalse(
     636                        $processor->next_delimiter(),
     637                        "Should have found no delimiter but found a '{$processor->get_block_type()}' instead."
     638                );
     639
     640                $this->assertNull(
     641                        $processor->get_last_error(),
     642                        'Should have completed without reporting an error.'
     643                );
     644        }
     645
     646        /**
     647         * Data provider.
     648         *
     649         * @return array<string, array{0: string, 1: string[]}>
     650         */
     651        public static function data_delimiters_and_comment_endings(): array {
     652                return array(
     653                        // The comment ended at `--!>`, so the later `-->` is not the delimiter's end.
     654                        'Spans an exclamation ending'      => array( '<!-- wp:a {"k":"x --!><!-- y"} -->', array() ),
     655                        'Spans two exclamation endings'    => array( '<!-- wp:a {"k":"x --!><!-- y --!><!-- z"} -->', array() ),
     656                        'Exclamation ending at the end'    => array( '<!-- wp:a {"k":"x --!>', array() ),
     657                        'Repeated exclamation endings'     => array( str_repeat( '<!-- wp:a {"k":" --!>', 100 ) . '"} -->', array() ),
     658
     659                        // Rejected for other reasons, asserted so the comment ending is not what rescues them.
     660                        'Exclamation ending, no JSON'      => array( '<!-- wp:a --!><!-- "} -->', array() ),
     661                        'Opening inside a dashed comment'  => array( '<!---<!-- wp:a {"k":1} -->', array() ),
     662
     663                        // Delimiters which stay inside one comment are unaffected.
     664                        'Plain delimiter'                  => array( '<!-- wp:a -->', array( 'core/a' ) ),
     665                        'Delimiter with attributes'        => array( '<!-- wp:a {"k":1} -->', array( 'core/a' ) ),
     666                        'Void delimiter'                   => array( '<!-- wp:a {"k":1} /-->', array( 'core/a' ) ),
     667                        'Hyphens inside the attributes'    => array( '<!-- wp:a {"k":"x--y"} -->', array( 'core/a' ) ),
     668                        'Dash run inside the attributes'   => array( '<!-- wp:a {"k":"x-----y"} -->', array( 'core/a' ) ),
     669                        'Dash run then bang'               => array( '<!-- wp:a {"k":"x---!y"} -->', array( 'core/a' ) ),
     670                        'Bang then dashes'                 => array( '<!-- wp:a {"k":"x--!-y"} -->', array( 'core/a' ) ),
     671                        'Dashes before the closer'         => array( '<!-- wp:a {"k":"x--"} -->', array( 'core/a' ) ),
     672                        'Dash run before a space closer'   => array( '<!-- wp:a {"k":"x"} --- -->', array() ),
     673                        'Delimiter after a closed comment' => array( '<!-- x --!><!-- wp:a {"k":1} -->', array( 'core/a' ) ),
     674                );
     675        }
     676
     677        /**
     678         * Verifies that a comment whose JSON attributes are rejected is skipped whole.
     679         *
     680         * A block delimiter is a single HTML comment. When the content after the JSON
     681         * attributes rules out a delimiter, the scan resumes after that comment, not one
     682         * byte later, so a `<!--` inside the comment's text is not treated as the start
     683         * of another delimiter.
     684         *
     685         * @ticket 66138
     686         *
     687         * @dataProvider data_comments_with_rejected_json_attributes
     688         *
     689         * @param string   $html        Document to scan.
     690         * @param string[] $block_types Printable block type of every delimiter in the document, in order.
     691         */
     692        public function test_skips_the_whole_comment_when_the_json_attributes_are_rejected( string $html, array $block_types ): void {
     693                $processor = new WP_Block_Processor( $html );
     694
     695                $found = array();
     696                while ( $processor->next_delimiter() ) {
     697                        $found[] = $processor->get_printable_block_type();
     698                }
     699
     700                $this->assertSame(
     701                        $block_types,
     702                        $found,
     703                        'Should have found only the delimiters which are in the document.'
     704                );
     705        }
     706
     707        /**
     708         * Data provider.
     709         *
     710         * @return array<string, array{0: string, 1: string[]}>
     711         */
     712        public static function data_comments_with_rejected_json_attributes(): array {
     713                return array(
     714                        // A `<!--` inside the comment's text does not start a delimiter.
     715                        'Opening inside the comment'      => array( '<!-- wp:a {x <!-- wp:b -->', array() ),
     716                        'Void opening inside the comment' => array( '<!-- wp:a {x <!-- wp:b /-->', array() ),
     717                        'Closer inside the comment'       => array( '<!-- wp:a {x <!-- /wp:b -->', array() ),
     718
     719                        // A delimiter in a later comment is still found.
     720                        'Delimiter in the next comment'   => array( '<!-- wp:a {x --><!-- wp:b -->', array( 'core/b' ) ),
     721                        'Content after the JSON'          => array( '<!-- wp:a {"k":1} x --><!-- wp:b -->', array( 'core/b' ) ),
     722                        'Valid delimiter first'           => array( '<!-- wp:a {"k":1} --><!-- wp:b {y -->', array( 'core/a' ) ),
    538723                );
    539724        }
Note: See TracChangeset for help on using the changeset viewer.