Make WordPress Core

Changeset 61485


Ignore:
Timestamp:
01/15/2026 11:11:45 AM (5 weeks ago)
Author:
jonsurrell
Message:

Script Loader: Use HTML API to generate SCRIPT tags.

Script tags have complicated and unintuitive parsing rules that make them difficult to author correctly. The HTML API automatically escapes script tag contents as necessary and will set attributes correctly. Using the HTML API to generate SCRIPT tags improves safety when working with SCRIPT tags, resolving a class of issues that have manifested repeatedly.

Changeset [61418] applied the HTML API to generate style tags in a similar way.

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

Props jonsurrell, dmsnell, westonruter.
Fixes #64500. See #64419, #40737, #62797, #63851, #51159.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/script-loader.php

    r61479 r61485  
    28812881    $attributes = apply_filters( 'wp_script_attributes', $attributes );
    28822882
    2883     return sprintf( "<script%s></script>\n", wp_sanitize_script_attributes( $attributes ) );
     2883    $processor = new WP_HTML_Tag_Processor( '<script></script>' );
     2884    $processor->next_tag();
     2885    foreach ( $attributes as $name => $value ) {
     2886        /*
     2887         * Lexical variations of an attribute name may represent the
     2888         * same attribute in HTML, therefore it’s possible that the
     2889         * input array might contain duplicate attributes even though
     2890         * it’s keyed on their name. Calling code should rewrite an
     2891         * attribute’s value rather than sending a duplicate attribute.
     2892         *
     2893         * Example:
     2894         *
     2895         *     array( 'id' => 'main', 'ID' => 'nav' )
     2896         *
     2897         * In this example, there are two keys both describing the `id`
     2898         * attribute. PHP array iteration is in key-insertion order so
     2899         * the 'id' value will be set in the SCRIPT tag.
     2900         */
     2901        if ( null !== $processor->get_attribute( $name ) ) {
     2902            continue;
     2903        }
     2904
     2905        $processor->set_attribute( $name, $value ?? true );
     2906    }
     2907    return "{$processor->get_updated_html()}\n";
    28842908}
    28852909
     
    29022926 *
    29032927 * It is possible to inject attributes in the `<script>` tag via the {@see 'wp_inline_script_attributes'} filter.
    2904  * Automatically injects type attribute if needed.
     2928 *
     2929 * If the `$data` is unsafe to embed in a `<script>` tag, an empty script tag with the provided
     2930 * attributes will be returned. JavaScript and JSON contents can be escaped, so this is only likely
     2931 * to be a problem with unusual content types.
     2932 *
     2933 * Example:
     2934 *
     2935 *     // The dangerous JavaScript in this example will be safely escaped.
     2936 *     // A string with the script tag and the desired contents will be returned.
     2937 *     wp_get_inline_script_tag( 'console.log( "</script>" );' );
     2938 *
     2939 *     // This data is unsafe and `text/plain` cannot be escaped.
     2940 *     // The following will return `""` to indicate failure:
     2941 *     wp_get_inline_script_tag( '</script>', array( 'type' => 'text/plain' ) );
    29052942 *
    29062943 * @since 5.7.0
     2944 * @since 7.0.0 Returns an empty string if the data cannot be safely embedded in a script tag.
    29072945 *
    29082946 * @param string                     $data       Data for script tag: JavaScript, importmap, speculationrules, etc.
    29092947 * @param array<string, string|bool> $attributes Optional. Key-value pairs representing `<script>` tag attributes.
    2910  * @return string String containing inline JavaScript code wrapped around `<script>` tag.
     2948 * @return string HTML script tag containing the provided $data or the empty string `""` if the data cannot be safely embedded in a script tag.
    29112949 */
    29122950function wp_get_inline_script_tag( $data, $attributes = array() ) {
     
    29252963    $attributes = apply_filters( 'wp_inline_script_attributes', $attributes, $data );
    29262964
    2927     return sprintf( "<script%s>%s</script>\n", wp_sanitize_script_attributes( $attributes ), $data );
     2965    $processor = new WP_HTML_Tag_Processor( '<script></script>' );
     2966    $processor->next_tag();
     2967    foreach ( $attributes as $name => $value ) {
     2968        /*
     2969         * Lexical variations of an attribute name may represent the
     2970         * same attribute in HTML, therefore it’s possible that the
     2971         * input array might contain duplicate attributes even though
     2972         * it’s keyed on their name. Calling code should rewrite an
     2973         * attribute’s value rather than sending a duplicate attribute.
     2974         *
     2975         * Example:
     2976         *
     2977         *     array( 'id' => 'main', 'ID' => 'nav' )
     2978         *
     2979         * In this example, there are two keys both describing the `id`
     2980         * attribute. PHP array iteration is in key-insertion order so
     2981         * the 'id' value will be set in the SCRIPT tag.
     2982         */
     2983        if ( null !== $processor->get_attribute( $name ) ) {
     2984            continue;
     2985        }
     2986
     2987        $processor->set_attribute( $name, $value ?? true );
     2988    }
     2989
     2990    if ( ! $processor->set_modifiable_text( $data ) ) {
     2991        _doing_it_wrong(
     2992            __FUNCTION__,
     2993            __( 'Unable to set inline script data.' ),
     2994            '7.0.0'
     2995        );
     2996        return '';
     2997    }
     2998
     2999    return "{$processor->get_updated_html()}\n";
    29283000}
    29293001
  • trunk/tests/phpunit/tests/dependencies/wpInlineScriptTag.php

    r61482 r61485  
    165165        );
    166166    }
     167
     168    /**
     169     * Test failure conditions setting inline script tag contents.
     170     *
     171     * @ticket 64500
     172     */
     173    public function test_script_tag_dangerous_unescapeable_contents() {
     174        $this->setExpectedIncorrectUsage( 'wp_get_inline_script_tag' );
     175        /*
     176         * </script> cannot be printed inside a script tag
     177         * the `example/example` type is an unknown type with no known escaping rules.
     178         * The only choice is to abort.
     179         */
     180        $result = wp_get_inline_script_tag(
     181            '</script>',
     182            array( 'type' => 'example/example' )
     183        );
     184        $this->assertSame( '', $result );
     185    }
    167186}
Note: See TracChangeset for help on using the changeset viewer.