Skip to content
Open
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
86 changes: 85 additions & 1 deletion src/wp-includes/class-wp-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,82 @@ public function print_extra_script( $handle, $display = true ) {
return true;
}

/**
* Gets data associated with a registered script.
*
* @since 7.1.0
*
* @param string $handle The script's registered handle.
* @return string Script data HTML tag, or empty string when no data exists.
*/
private function get_script_data( $handle ) {
/**
* Filters data associated with a given script.
*
* Scripts may require data that is required for initialization or is essential
* to have immediately available on page load. These are suitable use cases for
* this data.
*
* The dynamic portion of the hook name, `$handle`, refers to the script handle
* that the data is associated with.
*
* This is best suited to pass essential data that must be available to the script
* for initialization or immediately on page load. It does not replace the REST API
* or fetching data from the client.
*
* Example:
*
* add_filter(
* 'script_data_my-handle',
* function ( array $data ): array {
* $data['dataForClient'] = 'ok';
* return $data;
* }
* );
*
* If the filter returns no data (an empty array), nothing will be embedded in the page.
*
* The data for a given script, if provided, will be JSON serialized in a script tag
* with an ID of the form `wp-script-data-{$handle}`.
*
* The data can be read on the client with a pattern like this:
*
* Example:
*
* const dataContainer = document.getElementById( 'wp-script-data-my-handle' );
* let data = {};
* if ( dataContainer ) {
* try {
* data = JSON.parse( dataContainer.textContent );
* } catch {}
* }
* // data.dataForClient === 'ok';
* initMyScriptWithData( data );
*
* @since 7.1.0
*
* @param array $data The data associated with the script.
*/
$data = apply_filters( "script_data_{$handle}", array() );

if ( ! is_array( $data ) || array() === $data ) {
return '';
}

$json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
if ( ! is_utf8_charset() ) {
$json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
}

return wp_get_inline_script_tag(
(string) wp_json_encode( $data, $json_encode_flags ),
array(
'type' => 'application/json',
'id' => "wp-script-data-{$handle}",
)
);
}

/**
* Checks whether all dependents of a given handle are in the footer.
*
Expand Down Expand Up @@ -337,6 +413,7 @@ public function do_item( $handle, $group = false ) {
return false;
}

$script_data = $this->get_script_data( $handle );
$before_script = $this->get_inline_script_tag( $handle, 'before' );
$after_script = $this->get_inline_script_tag( $handle, 'after' );

Expand Down Expand Up @@ -377,7 +454,13 @@ public function do_item( $handle, $group = false ) {
*/
$filtered_src = apply_filters( 'script_loader_src', $src, $handle );

if (
if ( $script_data ) {
$this->do_concat = false;

// Have to print the so-far concatenated scripts right away to maintain the right order.
_print_scripts();
$this->reset();
} elseif (
is_string( $filtered_src )
&& $this->in_default_dir( $filtered_src )
&& ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) )
Expand All @@ -398,6 +481,7 @@ public function do_item( $handle, $group = false ) {
}
}

echo $script_data;
$this->print_extra_script( $handle );

// A single item may alias a set of items, by having dependencies, but no source.
Expand Down
198 changes: 198 additions & 0 deletions tests/phpunit/tests/dependencies/scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -2660,6 +2660,204 @@ public function test_wp_add_inline_script_localized_data_is_added_first() {
$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* @ticket 58873
*/
public function test_script_data_filter_prints_data_before_localized_data() {
wp_enqueue_script( 'test-example', 'example.com', array(), null );
wp_localize_script( 'test-example', 'testExample', array( 'foo' => 'bar' ) );
add_filter(
'script_data_test-example',
static function ( $data ) {
$data['clientData'] = 'ok';
return $data;
}
);

$expected = "<script type='application/json' id='wp-script-data-test-example'>\n{\"clientData\":\"ok\"}\n</script>\n";
$expected .= "<script id='test-example-js-extra'>\nvar testExample = {\"foo\":\"bar\"};\n//# sourceURL=test-example-js-extra\n</script>\n";
$expected .= "<script src='http://example.com' id='test-example-js'></script>\n";

$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* @ticket 58873
*/
public function test_script_data_filter_does_not_print_empty_data() {
wp_enqueue_script( 'test-example', 'example.com', array(), null );
add_filter(
'script_data_test-example',
static function ( $data ) {
return $data;
}
);

$expected = "<script src='http://example.com' id='test-example-js'></script>\n";

$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* @ticket 58873
*
* @dataProvider data_invalid_script_data
*
* @param mixed $data Data to return in filter.
*/
public function test_script_data_filter_does_not_print_invalid_data( $data ) {
wp_enqueue_script( 'test-example', 'example.com', array(), null );
add_filter(
'script_data_test-example',
static function () use ( $data ) {
return $data;
}
);

$expected = "<script src='http://example.com' id='test-example-js'></script>\n";

$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* Data provider.
*
* @return array
*/
public static function data_invalid_script_data(): array {
return array(
'null' => array( null ),
'stdClass' => array( new stdClass() ),
'number 1' => array( 1 ),
'string' => array( 'string' ),
);
}

/**
* @ticket 58873
*
* @dataProvider data_script_data_encoding
*
* @param string $input Raw input string.
* @param string $expected Expected output string.
* @param string $charset Blog charset option.
*/
public function test_script_data_filter_encoding( $input, $expected, $charset ) {
add_filter(
'pre_option_blog_charset',
static function () use ( $charset ) {
return $charset;
}
);

wp_enqueue_script( 'test-example', 'example.com', array(), null );
add_filter(
'script_data_test-example',
static function ( $data ) use ( $input ) {
$data[''] = $input;
return $data;
}
);

$expected = "<script type='application/json' id='wp-script-data-test-example'>\n{\"\":\"{$expected}\"}\n</script>\n";
$expected .= "<script src='http://example.com' id='test-example-js'></script>\n";

$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* @ticket 58873
*/
public function test_script_data_filter_prevents_concat() {
global $wp_scripts, $wp_version;

$wp_scripts->do_concat = true;
$wp_scripts->default_dirs = array( $this->default_scripts_dir );

wp_enqueue_script( 'one', $this->default_scripts_dir . 'one.js' );
wp_enqueue_script( 'two', $this->default_scripts_dir . 'two.js' );
add_filter(
'script_data_two',
static function ( $data ) {
$data['clientData'] = 'ok';
return $data;
}
);

$expected = "<script src='/wp-admin/load-scripts.php?c=0&amp;load%5Bchunk_0%5D=one&amp;ver={$wp_version}'></script>\n";
$expected .= "<script type='application/json' id='wp-script-data-two'>\n{\"clientData\":\"ok\"}\n</script>\n";
$expected .= "<script src='{$this->default_scripts_dir}two.js?ver={$wp_version}' id='two-js'></script>\n";

$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* @ticket 58873
*/
public function test_script_data_filter_for_external_script_flushes_concat_before_printing() {
global $wp_scripts, $wp_version;

$wp_scripts->do_concat = true;
$wp_scripts->default_dirs = array( $this->default_scripts_dir );

wp_enqueue_script( 'one', $this->default_scripts_dir . 'one.js' );
wp_enqueue_script( 'two', 'https://example.com/two.js', array(), null );
add_filter(
'script_data_two',
static function ( $data ) {
$data['clientData'] = 'ok';
return $data;
}
);

$expected = "<script src='/wp-admin/load-scripts.php?c=0&amp;load%5Bchunk_0%5D=one&amp;ver={$wp_version}'></script>\n";
$expected .= "<script type='application/json' id='wp-script-data-two'>\n{\"clientData\":\"ok\"}\n</script>\n";
$expected .= "<script src='https://example.com/two.js' id='two-js'></script>\n";

$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
}

/**
* Data provider.
*
* @return array
*/
public static function data_script_data_encoding(): array {
return array(
// UTF-8.
'Solidus' => array( '/', '/', 'UTF-8' ),
'Double quote' => array( '"', '\\"', 'UTF-8' ),
'Single quote' => array( '\'', '\'', 'UTF-8' ),
'Less than' => array( '<', '\u003C', 'UTF-8' ),
'Greater than' => array( '>', '\u003E', 'UTF-8' ),
'Ampersand' => array( '&', '&', 'UTF-8' ),
'Newline' => array( "\n", "\\n", 'UTF-8' ),
'Tab' => array( "\t", "\\t", 'UTF-8' ),
'Form feed' => array( "\f", "\\f", 'UTF-8' ),
'Carriage return' => array( "\r", "\\r", 'UTF-8' ),
'Line separator' => array( "\u{2028}", "\u{2028}", 'UTF-8' ),
'Paragraph separator' => array( "\u{2029}", "\u{2029}", 'UTF-8' ),
'Flag of England' => array( "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", 'UTF-8' ),
'Malicious script closer' => array( '</script>', '\u003C/script\u003E', 'UTF-8' ),
'Entity-encoded malicious script closer' => array( '&lt;/script&gt;', '&lt;/script&gt;', 'UTF-8' ),

// Non UTF-8.
'Solidus non-utf8' => array( '/', '/', 'iso-8859-1' ),
'Less than non-utf8' => array( '<', '\u003C', 'iso-8859-1' ),
'Greater than non-utf8' => array( '>', '\u003E', 'iso-8859-1' ),
'Ampersand non-utf8' => array( '&', '&', 'iso-8859-1' ),
'Newline non-utf8' => array( "\n", "\\n", 'iso-8859-1' ),
'Tab non-utf8' => array( "\t", "\\t", 'iso-8859-1' ),
'Form feed non-utf8' => array( "\f", "\\f", 'iso-8859-1' ),
'Carriage return non-utf8' => array( "\r", "\\r", 'iso-8859-1' ),
'Line separator non-utf8' => array( "\u{2028}", "\u2028", 'iso-8859-1' ),
'Paragraph separator non-utf8' => array( "\u{2029}", "\u2029", 'iso-8859-1' ),
'Flag of England non-utf8' => array( "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", "\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f", 'iso-8859-1' ),
'Malicious script closer non-utf8' => array( '</script>', '\u003C/script\u003E', 'iso-8859-1' ),
'Entity-encoded malicious script closer non-utf8' => array( '&lt;/script&gt;', '&lt;/script&gt;', 'iso-8859-1' ),
);
}

/**
* @ticket 14853
*/
Expand Down
Loading