diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 28772b6534337..e3cb75ce60b28 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3605,6 +3605,8 @@ function wp_print_script_tag( $attributes ) { * * @since 5.7.0 * @since 7.0.0 Returns an empty string if the data cannot be safely embedded in a script tag. + * @since 7.2.0 The {@see 'wp_inline_script_attributes'} filter receives the script text as the browser + * parses it from the printed tag, and is not applied when the data cannot be embedded. * * @param string $data Data for script tag: JavaScript, importmap, speculationrules, etc. * @param array $attributes Optional. Key-value pairs representing `' ); + $processor->next_tag(); + foreach ( $attributes as $name => $value ) { + /* + * Lexical variations of an attribute name may represent the + * same attribute in HTML, therefore it’s possible that the + * input array might contain duplicate attributes even though + * it’s keyed on their name. Calling code should rewrite an + * attribute’s value rather than sending a duplicate attribute. + * + * Example: + * + * array( 'id' => 'main', 'ID' => 'nav' ) + * + * In this example, there are two keys both describing the `id` + * attribute. PHP array iteration is in key-insertion order so + * the 'id' value will be set in the SCRIPT tag. + */ + if ( null !== $processor->get_attribute( $name ) ) { + continue; + } + + $processor->set_attribute( $name, $value ?? true ); + } + + // The `type` and `language` attributes decide how the text is escaped, so they are set first. + return $processor->set_modifiable_text( $data ) ? $processor : null; + }; + + $processor = $build_tag( $attributes ); + if ( null === $processor ) { + return ''; + } + /** * Filters attributes to be added to a script tag. * * @since 5.7.0 + * @since 7.2.0 `$data` is the script text as the browser parses it from the printed tag. * * @param array $attributes Key-value pairs representing `' ); - $processor->next_tag(); - foreach ( $attributes as $name => $value ) { - /* - * Lexical variations of an attribute name may represent the - * same attribute in HTML, therefore it’s possible that the - * input array might contain duplicate attributes even though - * it’s keyed on their name. Calling code should rewrite an - * attribute’s value rather than sending a duplicate attribute. - * - * Example: - * - * array( 'id' => 'main', 'ID' => 'nav' ) - * - * In this example, there are two keys both describing the `id` - * attribute. PHP array iteration is in key-insertion order so - * the 'id' value will be set in the SCRIPT tag. - */ - if ( null !== $processor->get_attribute( $name ) ) { - continue; + // The filtered array replaces the attributes, removals included, so the tag is built again. + if ( $filtered_attributes !== $attributes ) { + $processor = $build_tag( $filtered_attributes ); + if ( null === $processor ) { + return ''; } - - $processor->set_attribute( $name, $value ?? true ); - } - - if ( ! $processor->set_modifiable_text( $data ) ) { - return ''; } return "{$processor->get_updated_html()}\n"; diff --git a/tests/phpunit/tests/dependencies/wpInlineScriptTag.php b/tests/phpunit/tests/dependencies/wpInlineScriptTag.php index 590872ee3fd6c..31d3395823dc4 100644 --- a/tests/phpunit/tests/dependencies/wpInlineScriptTag.php +++ b/tests/phpunit/tests/dependencies/wpInlineScriptTag.php @@ -184,4 +184,128 @@ public function test_script_tag_dangerous_unescapeable_contents() { ); $this->assertSame( '', $result ); } + + /** + * Test that the filter receives the script text as it is printed, with escaping applied. + * + * @ticket 66308 + */ + public function test_filter_receives_escaped_script_text() { + $filtered_data = null; + add_filter( + 'wp_inline_script_attributes', + static function ( $attributes, $data ) use ( &$filtered_data ) { + $filtered_data = $data; + return $attributes; + }, + 10, + 2 + ); + + $tag = wp_get_inline_script_tag( 'console.log( "" );' ); + + $this->assertSame( "\n" . 'console.log( "" );' . "\n", $filtered_data ); + $this->assertSame( '' . "\n", $tag ); + } + + /** + * Test that the filter receives the script text with newlines normalized as a browser parses them. + * + * @ticket 66308 + */ + public function test_filter_receives_script_text_with_normalized_newlines() { + $filtered_data = null; + add_filter( + 'wp_inline_script_attributes', + static function ( $attributes, $data ) use ( &$filtered_data ) { + $filtered_data = $data; + return $attributes; + }, + 10, + 2 + ); + + wp_get_inline_script_tag( "first();\r\nsecond();\rthird();" ); + + $this->assertSame( "\nfirst();\nsecond();\nthird();\n", $filtered_data ); + } + + /** + * Test that the printed tag has exactly the attributes returned by the filter. + * + * @ticket 66308 + */ + public function test_filter_replaces_attributes() { + add_filter( + 'wp_inline_script_attributes', + static function ( $attributes ) { + unset( $attributes['nomodule'] ); + $attributes['async'] = true; + return $attributes; + } + ); + + $expected = <<<'HTML' + + +HTML; + + $this->assertEqualHTML( + $expected, + wp_get_inline_script_tag( + '"script data";', + array( + 'id' => 'example', + 'nomodule' => true, + ) + ) + ); + } + + /** + * Test that a filter setting an unrecognized type on data that cannot be escaped for it returns no tag. + * + * @ticket 66308 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text + */ + public function test_filter_setting_unrecognized_type_on_dangerous_contents_returns_empty_string() { + add_filter( + 'wp_inline_script_attributes', + static function ( $attributes ) { + $attributes['type'] = 'example/example'; + return $attributes; + } + ); + + $this->assertSame( '', wp_get_inline_script_tag( 'console.log( "" );' ) ); + } + + /** + * Test that the filter does not run when no tag is printed. + * + * @ticket 66308 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_modifiable_text + */ + public function test_filter_not_called_for_dangerous_unescapeable_contents() { + $filter_calls = 0; + add_filter( + 'wp_inline_script_attributes', + static function ( $attributes ) use ( &$filter_calls ) { + ++$filter_calls; + return $attributes; + } + ); + + $result = wp_get_inline_script_tag( + '', + array( 'type' => 'example/example' ) + ); + + $this->assertSame( '', $result ); + $this->assertSame( 0, $filter_calls ); + } }