Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 49 additions & 28 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string|bool> $attributes Optional. Key-value pairs representing `<script>` tag attributes.
Expand All @@ -3613,45 +3615,64 @@ function wp_print_script_tag( $attributes ) {
function wp_get_inline_script_tag( $data, $attributes = array() ) {
$data = "\n" . trim( $data, "\n\r " ) . "\n";

// Returns the tag built from the given attributes, or null if the data cannot be embedded with them.
$build_tag = static function ( $attributes ) use ( $data ) {
$processor = new WP_HTML_Tag_Processor( '<script></script>' );
$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<string, string|bool> $attributes Key-value pairs representing `<script>` tag attributes.
* Only the attribute name is added to the `<script>` tag for
* entries with a boolean value, and that are true.
* @param string $data Inline data.
* @param string $data Script text as the browser parses it from the printed tag,
* escaped for the `type` and `language` in `$attributes`.
* A callback that changes `type` or `language` can change
* the escaping of the printed text.
*/
$attributes = apply_filters( 'wp_inline_script_attributes', $attributes, $data );
$filtered_attributes = apply_filters( 'wp_inline_script_attributes', $attributes, $processor->get_modifiable_text() );

$processor = new WP_HTML_Tag_Processor( '<script></script>' );
$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";
Expand Down
124 changes: 124 additions & 0 deletions tests/phpunit/tests/dependencies/wpInlineScriptTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( "</script>" );' );

$this->assertSame( "\n" . 'console.log( "</\u0073cript>" );' . "\n", $filtered_data );
$this->assertSame( '<script>' . "\n" . 'console.log( "</\u0073cript>" );' . "\n" . '</script>' . "\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'
<script id="example" async>
"script data";
</script>

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( "</script>" );' ) );
}

/**
* 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(
'</script>',
array( 'type' => 'example/example' )
);

$this->assertSame( '', $result );
$this->assertSame( 0, $filter_calls );
}
}
Loading