Skip to content

Commit 37bee55

Browse files
committed
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 #10639. Props jonsurrell, dmsnell, westonruter. Fixes #64500. See #64419, #40737, #62797, #63851, #51159. git-svn-id: https://develop.svn.wordpress.org/trunk@61485 602fd350-edb4-49c9-b593-d223f7449a82
1 parent a8924f5 commit 37bee55

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

src/wp-includes/script-loader.php

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,7 +2880,31 @@ function wp_get_script_tag( $attributes ) {
28802880
*/
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

28862910
/**
@@ -2901,13 +2925,27 @@ function wp_print_script_tag( $attributes ) {
29012925
* Constructs an inline script tag.
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
*/
29122950
function wp_get_inline_script_tag( $data, $attributes = array() ) {
29132951
$data = "\n" . trim( $data, "\n\r " ) . "\n";
@@ -2924,7 +2962,41 @@ function wp_get_inline_script_tag( $data, $attributes = array() ) {
29242962
*/
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

29303002
/**

tests/phpunit/tests/dependencies/wpInlineScriptTag.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,23 @@ public function test_script_tag_repeat_attributes() {
164164
),
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
}

0 commit comments

Comments
 (0)