Make WordPress Core

Changeset 62595


Ignore:
Timestamp:
06/30/2026 06:46:16 PM (4 months ago)
Author:
jonsurrell
Message:

HTML API: Ensure slash in attribute value is not a self-closing flag.

A trailing slash in an unquoted attribute value was incorrectly treated as a self-closing flag. For example, <div id=test/> is a tag with the id attribute value test/, not a self-closing tag.

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

Props jonsurrell, dmsnell.
See #65372.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/html-api/class-wp-html-tag-processor.php

    r62509 r62595  
    627627         */
    628628        private $token_length;
     629
     630        /**
     631         * Whether the current tag token has the self-closing flag.
     632         *
     633         * @since 7.1.0
     634         *
     635         * @var bool
     636         */
     637        private $has_self_closing_flag = false;
    629638
    630639        /**
    … …  
    10751084                 * tag sequence.
    10761085                 */
    1077                 $tag_name_starts_at   = $this->tag_name_starts_at;
    1078                 $tag_name_length      = $this->tag_name_length;
    1079                 $tag_ends_at          = $this->token_starts_at + $this->token_length;
    1080                 $attributes           = $this->attributes;
    1081                 $duplicate_attributes = $this->duplicate_attributes;
     1086                $tag_name_starts_at    = $this->tag_name_starts_at;
     1087                $tag_name_length       = $this->tag_name_length;
     1088                $tag_ends_at           = $this->token_starts_at + $this->token_length;
     1089                $has_self_closing_flag = $this->has_self_closing_flag;
     1090                $attributes            = $this->attributes;
     1091                $duplicate_attributes  = $this->duplicate_attributes;
    10821092
    10831093                // Find the closing tag if necessary.
    … …  
    11291139                 * the inner content of the tag.
    11301140                 */
    1131                 $this->token_starts_at      = $was_at;
    1132                 $this->token_length         = $this->bytes_already_parsed - $this->token_starts_at;
    1133                 $this->text_starts_at       = $tag_ends_at;
    1134                 $this->text_length          = $this->tag_name_starts_at - $this->text_starts_at;
    1135                 $this->tag_name_starts_at   = $tag_name_starts_at;
    1136                 $this->tag_name_length      = $tag_name_length;
    1137                 $this->attributes           = $attributes;
    1138                 $this->duplicate_attributes = $duplicate_attributes;
     1141                $this->token_starts_at       = $was_at;
     1142                $this->token_length          = $this->bytes_already_parsed - $this->token_starts_at;
     1143                $this->text_starts_at        = $tag_ends_at;
     1144                $this->text_length           = $this->tag_name_starts_at - $this->text_starts_at;
     1145                $this->tag_name_starts_at    = $tag_name_starts_at;
     1146                $this->tag_name_length       = $tag_name_length;
     1147                $this->has_self_closing_flag = $has_self_closing_flag;
     1148                $this->attributes            = $attributes;
     1149                $this->duplicate_attributes  = $duplicate_attributes;
    11391150
    11401151                return true;
    … …  
    21412152
    21422153                // Skip whitespace and slashes.
    2143                 $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
     2154                $skipped_length              = strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
     2155                $this->bytes_already_parsed += $skipped_length;
    21442156                if ( $this->bytes_already_parsed >= $doc_length ) {
    21452157                        $this->parser_state = self::STATE_INCOMPLETE_INPUT;
    21462158
     2159                        return false;
     2160                }
     2161
     2162                /**
     2163                 * This block serves two purposes:
     2164                 *
     2165                 * - A fast path for common tag-ending `>`.
     2166                 * - A check for the self-closing flag which must appear as `/>`.
     2167                 *
     2168                 * In a tag like `<g attr=/>`, `/` is the attribute value, not a self-closing
     2169                 * flag. When it appears in this form, the parser has already consumed the
     2170                 * attribute value, `$skipped_length` is 0, and this checks below correctly
     2171                 * identify whether there is a self-closing flag.
     2172                 *
     2173                 * Note: Both start and end tags may have the self-closing flag.
     2174                 */
     2175                if ( '>' === $this->html[ $this->bytes_already_parsed ] ) {
     2176                        if ( $skipped_length > 0 && '/' === $this->html[ $this->bytes_already_parsed - 1 ] ) {
     2177                                $this->has_self_closing_flag = true;
     2178                        }
    21472179                        return false;
    21482180                }
    … …  
    23252357                $this->token_starts_at          = null;
    23262358                $this->token_length             = null;
     2359                $this->has_self_closing_flag    = false;
    23272360                $this->tag_name_starts_at       = null;
    23282361                $this->tag_name_length          = null;
    … …  
    33333366                }
    33343367
    3335                 /*
    3336                  * The self-closing flag is the solidus at the _end_ of the tag, not the beginning.
    3337                  *
    3338                  * Example:
    3339                  *
    3340                  *     <figure />
    3341                  *             ^ this appears one character before the end of the closing ">".
    3342                  */
    3343                 return '/' === $this->html[ $this->token_starts_at + $this->token_length - 2 ];
     3368                return $this->has_self_closing_flag;
    33443369        }
    33453370
  • trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php

    r62575 r62595  
    582582                $this->assertSame( 'MATH', $processor->get_tag() );
    583583                $this->assertTrue( $processor->expects_closer() );
     584        }
     585
     586        /**
     587         * Ensures a slash-only unquoted attribute value does not close foreign content.
     588         *
     589         * @ticket 65372
     590         */
     591        public function test_unquoted_slash_attribute_does_not_self_close_foreign_content(): void {
     592                $processor = WP_HTML_Processor::create_fragment( '<math><mi a=/>math:mi is not self-closing, it has [a="/"] attribute.' );
     593
     594                $this->assertTrue( $processor->next_tag( 'MI' ), 'Failed to find the MI tag: check test setup.' );
     595                $this->assertSame( '/', $processor->get_attribute( 'a' ), 'Failed to treat the slash as the unquoted attribute value.' );
     596                $this->assertFalse(
     597                        $processor->has_self_closing_flag(),
     598                        'Failed to avoid interpreting the slash-only unquoted attribute value as a self-closing flag.'
     599                );
     600
     601                $this->assertTrue( $processor->next_token(), 'Failed to find text following the MI tag: check test setup.' );
     602                $this->assertSame(
     603                        array( 'HTML', 'BODY', 'MATH', 'MI', '#text' ),
     604                        $processor->get_breadcrumbs(),
     605                        'Failed to keep text following the MI tag inside the MI element.'
     606                );
    584607        }
    585608
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php

    r62519 r62595  
    112112                        // These involve syntax peculiarities.
    113113                        'Self-closing flag after extra spaces'       => array( '<div      />', true ),
    114                         'Self-closing flag after attribute'          => array( '<div id=test/>', true ),
     114                        'Self-closing flag after attribute'          => array( '<div id=test />', true ),
     115                        'Slash inside unquoted attribute value'      => array( '<div id=test/>', false ),
     116                        'Slash only unquoted attribute value'        => array( '<div attr=/>', false ),
     117                        'Attribute "=" with value ""'                => array( '<div =/>', false ),
    115118                        'Self-closing flag after quoted attribute'   => array( '<div id="test"/>', true ),
    116119                        'Self-closing flag after boolean attribute'  => array( '<div enabled/>', true ),
    117                         'Boolean attribute that looks like a self-closer' => array( '<div / >', false ),
     120                        'Ignored "/" and whitespace'                => array( '<div / >', false ),
    118121                );
    119122        }
Note: See TracChangeset for help on using the changeset viewer.