Make WordPress Core

Changeset 56748


Ignore:
Timestamp:
09/29/2023 07:45:53 PM (12 months ago)
Author:
westonruter
Message:

Script Loader: Harden removal of script tag wrappers.

  • Add wp_remove_surrounding_empty_script_tags() to more precisely remove script tag wrappers and warn when doing it wrong.
  • Add clarifying comments for XML escaping logic in wp_get_inline_script_tag().
  • Leverage WP_HTML_Tag_Processor in test_remove_frameless_preview_messenger_channel.
  • Reuse assertEqualMarkup in test_blocking_dependent_with_delayed_dependency.
  • Normalize whitespace in parse_markup_fragment for assertEqualMarkup.

Follow-up to [56687].
Props dmsnell, westonruter, flixos90.
See #58664.

Location:
trunk
Files:
1 added
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/phpcs.xml.dist

    r56741 r56748  
    172172                <element value="nodeName"/>
    173173                <element value="nodeType"/>
     174                <element value="nodeValue"/>
    174175                <element value="parentNode"/>
    175176                <element value="preserveWhiteSpace"/>
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r56687 r56748  
    475475            </script>
    476476            <?php
    477             $message .= wp_get_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     477            $message .= wp_get_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    478478        }
    479479
     
    21102110        </script>
    21112111        <?php
    2112         wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     2112        wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    21132113    }
    21142114
     
    22312231        </script>
    22322232        <?php
    2233         wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     2233        wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    22342234    }
    22352235
     
    50205020        </script>
    50215021        <?php
    5022         wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     5022        wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    50235023    }
    50245024
  • trunk/src/wp-includes/functions.php

    r56743 r56748  
    76207620    </script>
    76217621    <?php
    7622     wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     7622    wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    76237623}
    76247624
  • trunk/src/wp-includes/script-loader.php

    r56739 r56748  
    28542854    }
    28552855
    2856     // Ensure markup is XHTML compatible if not HTML5.
     2856    /*
     2857     * XHTML extracts the contents of the SCRIPT element and then the XML parser
     2858     * decodes character references and other syntax elements. This can lead to
     2859     * misinterpretation of the script contents or invalid XHTML documents.
     2860     *
     2861     * Wrapping the contents in a CDATA section instructs the XML parser not to
     2862     * transform the contents of the SCRIPT element before passing them to the
     2863     * JavaScript engine.
     2864     *
     2865     * Example:
     2866     *
     2867     *     <script>console.log('&hellip;');</script>
     2868     *
     2869     *     In an HTML document this would print "&hellip;" to the console,
     2870     *     but in an XHTML document it would print "…" to the console.
     2871     *
     2872     *     <script>console.log('An image is <img> in HTML');</script>
     2873     *
     2874     *     In an HTML document this would print "An image is <img> in HTML",
     2875     *     but it's an invalid XHTML document because it interprets the `<img>`
     2876     *     as an empty tag missing its closing `/`.
     2877     *
     2878     * @see https://www.w3.org/TR/xhtml1/#h-4.8
     2879     */
    28572880    if ( ! $is_html5 ) {
    2858         $javascript = str_replace( ']]>', ']]]]><![CDATA[>', $javascript ); // Escape any existing CDATA section.
     2881        /*
     2882         * If the string `]]>` exists within the JavaScript it would break
     2883         * out of any wrapping CDATA section added here, so to start, it's
     2884         * necessary to escape that sequence which requires splitting the
     2885         * content into two CDATA sections wherever it's found.
     2886         *
     2887         * Note: it's only necessary to escape the closing `]]>` because
     2888         * an additional `<![CDATA[` leaves the contents unchanged.
     2889         */
     2890        $javascript = str_replace( ']]>', ']]]]><![CDATA[>', $javascript );
     2891
     2892        // Wrap the entire escaped script inside a CDATA section.
    28592893        $javascript = sprintf( "/* <![CDATA[ */\n%s\n/* ]]> */", $javascript );
    28602894    }
     
    33003334    return $editor_settings;
    33013335}
     3336
     3337/**
     3338 * Removes leading and trailing _empty_ script tags.
     3339 *
     3340 * This is a helper meant to be used for literal script tag construction
     3341 * within `wp_get_inline_script_tag()` or `wp_print_inline_script_tag()`.
     3342 * It removes the literal values of "<script>" and "</script>" from
     3343 * around an inline script after trimming whitespace. Typlically this
     3344 * is used in conjunction with output buffering, where `ob_get_clean()`
     3345 * is passed as the `$contents` argument.
     3346 *
     3347 * Example:
     3348 *
     3349 *     // Strips exact literal empty SCRIPT tags.
     3350 *     $js = '<script>sayHello();</script>;
     3351 *     'sayHello();' === wp_remove_surrounding_empty_script_tags( $js );
     3352 *
     3353 *     // Otherwise if anything is different it warns in the JS console.
     3354 *     $js = '<script type="text/javascript">console.log( "hi" );</script>';
     3355 *     'console.error( ... )' === wp_remove_surrounding_empty_script_tags( $js );
     3356 *
     3357 * @private
     3358 * @since 6.4.0
     3359 *
     3360 * @see wp_print_inline_script_tag()
     3361 * @see wp_get_inline_script_tag()
     3362 *
     3363 * @param string $contents Script body with manually created SCRIPT tag literals.
     3364 * @return string Script body without surrounding script tag literals, or
     3365 *                original contents if both exact literals aren't present.
     3366 */
     3367function wp_remove_surrounding_empty_script_tags( $contents ) {
     3368    $contents = trim( $contents );
     3369    $opener   = '<SCRIPT>';
     3370    $closer   = '</SCRIPT>';
     3371
     3372    if (
     3373        strlen( $contents ) > strlen( $opener ) + strlen( $closer ) &&
     3374        strtoupper( substr( $contents, 0, strlen( $opener ) ) ) === $opener &&
     3375        strtoupper( substr( $contents, -strlen( $closer ) ) ) === $closer
     3376    ) {
     3377        return substr( $contents, strlen( $opener ), -strlen( $closer ) );
     3378    } else {
     3379        $error_message = __( 'Expected string to start with script tag (without attributes) and end with script tag, with optional whitespace.' );
     3380        _doing_it_wrong( __FUNCTION__, $error_message, '6.4' );
     3381        return sprintf( 'console.error(%s)', wp_json_encode( __( 'Function wp_remove_surrounding_empty_script_tags() used incorrectly in PHP.' ) . ' ' . $error_message ) );
     3382    }
     3383}
  • trunk/src/wp-includes/theme-templates.php

    r56687 r56748  
    206206    </script>
    207207    <?php
    208     $skip_link_script = str_replace( array( '<script>', '</script>' ), '', ob_get_clean() );
     208    $skip_link_script = wp_remove_surrounding_empty_script_tags( ob_get_clean() );
    209209    $script_handle    = 'wp-block-template-skip-link';
    210210    wp_register_script( $script_handle, false );
  • trunk/src/wp-includes/theme.php

    r56690 r56748  
    38013801    </script>
    38023802    <?php
    3803     wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     3803    wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    38043804}
    38053805
  • trunk/src/wp-includes/widgets/class-wp-widget-archives.php

    r56687 r56748  
    121121</script>
    122122            <?php
    123             wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     123            wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    124124        } else {
    125125            $format = current_theme_supports( 'html5', 'navigation-widgets' ) ? 'html5' : 'xhtml';
  • trunk/src/wp-includes/widgets/class-wp-widget-categories.php

    r56687 r56748  
    109109
    110110            <?php
    111             wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     111            wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    112112        } else {
    113113            $format = current_theme_supports( 'html5', 'navigation-widgets' ) ? 'html5' : 'xhtml';
  • trunk/src/wp-login.php

    r56687 r56748  
    106106        <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
    107107        <?php
    108         wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     108        wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    109109    }
    110110
     
    420420        ob_start();
    421421        ?>
    422         <script>
     422        <script type="text/javascript">
    423423        try{document.getElementById('<?php echo $input_id; ?>').focus();}catch(e){}
    424424        if(typeof wpOnload==='function')wpOnload();
    425425        </script>
    426426        <?php
    427         wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     427        wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    428428    }
    429429
     
    13631363                    <script>setTimeout( function(){ new wp.customize.Messenger({ url: '<?php echo wp_customize_url(); ?>', channel: 'login' }).send('login') }, 1000 );</script>
    13641364                    <?php
    1365                     wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     1365                    wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    13661366                }
    13671367
     
    16281628            </script>
    16291629            <?php
    1630             wp_print_inline_script_tag( str_replace( array( '<script>', '</script>' ), '', ob_get_clean() ) );
     1630            wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
    16311631        }
    16321632
  • trunk/tests/phpunit/tests/customize/manager.php

    r56695 r56748  
    31363136        ob_start();
    31373137        $manager->remove_frameless_preview_messenger_channel();
    3138         $output = ob_get_clean();
    3139         $this->assertStringContainsString( '<script', $output );
     3138        $processor = new WP_HTML_Tag_Processor( ob_get_clean() );
     3139        $this->assertTrue( $processor->next_tag( 'script' ), 'Failed to find expected SCRIPT element in output.' );
    31403140    }
    31413141
  • trunk/tests/phpunit/tests/dependencies/scripts.php

    r56687 r56748  
    261261        wp_enqueue_script( 'dependent-script-a3', '/dependent-script-a3.js', array( 'main-script-a3' ), null );
    262262        $output   = get_echo( 'wp_print_scripts' );
    263         $expected = str_replace( "'", '"', "<script type='text/javascript' src='/main-script-a3.js' id='main-script-a3-js' data-wp-strategy='{$strategy}'></script>" );
    264         $this->assertStringContainsString( $expected, $output, 'Blocking dependents must force delayed dependencies to become blocking.' );
     263        $expected = <<<JS
     264            <script type='text/javascript' src='/main-script-a3.js' id='main-script-a3-js' data-wp-strategy='{$strategy}'></script>
     265            <script id="dependent-script-a3-js" src="/dependent-script-a3.js" type="text/javascript"></script>
     266JS;
     267        $this->assertEqualMarkup( $expected, $output, 'Blocking dependents must force delayed dependencies to become blocking.' );
    265268    }
    266269
     
    29983001        }
    29993002
     3003        // Normalize other whitespace nodes.
     3004        $xpath = new DOMXPath( $dom );
     3005        foreach ( $xpath->query( '//text()' ) as $node ) {
     3006            /** @var DOMText $node */
     3007            if ( preg_match( '/^\s+$/', $node->nodeValue ) ) {
     3008                $node->nodeValue = ' ';
     3009            }
     3010        }
     3011
    30003012        return $dom;
    30013013    }
Note: See TracChangeset for help on using the changeset viewer.