Make WordPress Core

Changeset 63513


Ignore:
Timestamp:
09/07/2026 08:45:50 AM (5 weeks ago)
Author:
jonsurrell
Message:

HTML API: Allow raw text which cannot close its own element.

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

Props khokansardar, jonsurrell, shailu25.
Fixes #65824.

Location:
trunk
Files:
2 edited

Legend:

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

    r63463 r63513  
    40934093                                 * HTML structure is rejected here. It’s the responsibility of calling code to
    40944094                                 * perform whatever semantic escaping is necessary to avoid problematic strings.
     4095                                 *
     4096                                 * Both the start tag `<script` and the end tag `</script` are rejected. It’s
     4097                                 * easy to assume that only an end tag can alter the HTML structure, but a
     4098                                 * start tag which follows `<!--` moves the tokenizer into the double-escaped
     4099                                 * states, where a later `</script>` no longer closes the element.
     4100                                 *
     4101                                 * In both cases the tag name ends only at one of the characters matched
     4102                                 * below, so text such as `</scriptx>` cannot change that structure and is
     4103                                 * safe to set.
     4104                                 *
     4105                                 * @link https://html.spec.whatwg.org/#script-data-end-tag-name-state
     4106                                 * @link https://html.spec.whatwg.org/#script-data-double-escape-start-state
    40954107                                 */
    4096                                 if (
    4097                                         false !== stripos( $plaintext_content, '<script' ) ||
    4098                                         false !== stripos( $plaintext_content, '</script' )
    4099                                 ) {
     4108                                if ( 1 === preg_match( '~</?script[ \t\f\r\n/>]~i', $plaintext_content ) ) {
    41004109                                        _doing_it_wrong(
    41014110                                                __METHOD__,
    … …  
    41174126                        case 'XMP':
    41184127                                $tag_name = $this->get_tag();
    4119                                 if ( false !== stripos( $plaintext_content, "</{$tag_name}" ) ) {
     4128
     4129                                /*
     4130                                 * A tag name ends only at one of the characters matched below, so text
     4131                                 * such as `</xmp-tag>` cannot close the element and is safe to set.
     4132                                 *
     4133                                 * @link https://html.spec.whatwg.org/#rawtext-end-tag-name-state
     4134                                 */
     4135                                if ( 1 === preg_match( '~</' . preg_quote( $tag_name, '~' ) . '[ \t\f\r\n/>]~i', $plaintext_content ) ) {
    41204136                                        _doing_it_wrong(
    41214137                                                __METHOD__,
  • trunk/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php

    r63418 r63513  
    661661         * @ticket 61617
    662662         * @ticket 62797
     663         * @ticket 65824
    663664         *
    664665         * @dataProvider data_unallowed_modifiable_text_updates
    … …  
    715716                        'Non-JS SCRIPT with <script attributes>'  => array( '<script language="text">Replace me</script>', '<!-- <script sneaky>after' ),
    716717                        'Non-JS SCRIPT with </script attributes>' => array( '<script language="text">Replace me</script>', 'before</script sneaky>after' ),
     718                        'XMP with </xmp/>'                        => array( '<xmp>Replace me</xmp>', 'Also closed by </xmp/>' ),
     719                        'XMP with </xmp\f>'                       => array( '<xmp>Replace me</xmp>', "Also closed by </xmp\f>" ),
     720                        'Non-JS SCRIPT with </script\r>'          => array( '<script language="text">Replace me</script>', "before</script\r>after" ),
     721                );
     722        }
     723
     724        /**
     725         * Ensures that raw text which resembles a closing tag, but cannot close its
     726         * element, is allowed as modifiable text.
     727         *
     728         * @ticket 65824
     729         *
     730         * @dataProvider data_raw_text_resembling_a_closing_tag
     731         *
     732         * @param string $html     HTML whose first tag holds the raw text to replace.
     733         * @param string $update   Text resembling, but not forming, that element's closing tag.
     734         * @param string $expected Expected document after the update.
     735         */
     736        public function test_allows_raw_text_which_cannot_close_its_element( string $html, string $update, string $expected ): void {
     737                $processor = new WP_HTML_Tag_Processor( $html );
     738                $processor->next_tag();
     739
     740                $this->assertTrue(
     741                        $processor->set_modifiable_text( $update ),
     742                        'Should have allowed text which cannot close the element.'
     743                );
     744
     745                $this->assertSame(
     746                        $expected,
     747                        $processor->get_updated_html(),
     748                        'Should have updated the document as expected.'
     749                );
     750
     751                $reparsed = new WP_HTML_Tag_Processor( $expected );
     752                $reparsed->next_tag();
     753
     754                $this->assertSame(
     755                        $update,
     756                        $reparsed->get_modifiable_text(),
     757                        'Should have preserved the text when re-parsing the updated document.'
     758                );
     759        }
     760
     761        /**
     762         * Data provider.
     763         *
     764         * @return array[]
     765         */
     766        public static function data_raw_text_resembling_a_closing_tag(): array {
     767                return array(
     768                        'IFRAME with </iframely>'          => array( '<iframe>Replace me</iframe>', 'Just a </iframely>', '<iframe>Just a </iframely></iframe>' ),
     769                        'NOEMBED with </NOEMBEDDED>'       => array( '<noembed>Replace me</noembed>', 'Just a </NOEMBEDDED>', '<noembed>Just a </NOEMBEDDED></noembed>' ),
     770                        'NOFRAMES with </noframes->'       => array( '<noframes>Replace me</noframes>', 'before</noframes->after', '<noframes>before</noframes->after</noframes>' ),
     771                        'XMP with </xmp-tag>'              => array( '<xmp>Replace me</xmp>', 'Just a </xmp-tag>', '<xmp>Just a </xmp-tag></xmp>' ),
     772                        'XMP ending in </xmp'              => array( '<xmp>Replace me</xmp>', 'Trailing </xmp', '<xmp>Trailing </xmp</xmp>' ),
     773                        'Non-JS SCRIPT with <scriptish>'   => array( '<script type="text/plain">Replace me</script>', '<!-- <scriptish> -->', '<script type="text/plain"><!-- <scriptish> --></script>' ),
     774                        'Non-JS SCRIPT with </scriptx>'    => array( '<script type="text/plain">Replace me</script>', 'Just a </scriptx>', '<script type="text/plain">Just a </scriptx></script>' ),
     775                        'Non-JS SCRIPT ending in <script'  => array( '<script type="text/plain">Replace me</script>', '<!-- <script', '<script type="text/plain"><!-- <script</script>' ),
     776                        'Non-JS SCRIPT ending in </script' => array( '<script type="text/plain">Replace me</script>', 'Trailing </script', '<script type="text/plain">Trailing </script</script>' ),
    717777                );
    718778        }
Note: See TracChangeset for help on using the changeset viewer.