From 661399edae6f53eb131fde4fe8d4d30e1cc01a89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:11:05 +0000 Subject: [PATCH 01/12] Initial plan From 2c0960c87d4bd85d86d17f6e7972239a7acabb64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:15:52 +0000 Subject: [PATCH 02/12] Add script_data_{$handle} filter for classic scripts Co-authored-by: sirreal <841763+sirreal@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 17 +++ tests/phpunit/tests/dependencies/scripts.php | 135 +++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index a30b09249fd52..1bbd6d66ca012 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -634,6 +634,23 @@ public function localize( $handle, $object_name, $l10n ) { } } + /** + * Filters data associated with a given script. + * + * The dynamic portion of the hook name, `$handle`, refers to the script handle. + * + * This filter allows developers to modify the data passed to a script via + * wp_localize_script() before it is output. This is analogous to the + * `script_module_data_{$module_id}` filter for script modules. + * + * @since 6.8.0 + * + * @param array|string $l10n The data to be localized. + * @param string $object_name The JavaScript object name. + * @param string $handle The script handle. + */ + $l10n = apply_filters( "script_data_{$handle}", $l10n, $object_name, $handle ); + $script = "var $object_name = " . wp_json_encode( $l10n, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . ';'; if ( ! empty( $after ) ) { diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index a3c8b92695f4f..55d1b5d7d0129 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4122,4 +4122,139 @@ public function test_wp_scripts_doing_it_wrong_for_missing_dependencies() { 'Expected _doing_it_wrong() notice to indicate missing dependencies for enqueued script.' ); } + + /** + * Tests that the script_data_{$handle} filter allows modifying localized script data. + * + * @ticket TBD + * @covers WP_Scripts::localize + */ + public function test_script_data_filter_modifies_localized_data() { + wp_enqueue_script( 'test-script', '/test.js', array(), null ); + wp_localize_script( 'test-script', 'testData', array( 'foo' => 'bar' ) ); + + add_filter( + 'script_data_test-script', + function ( $l10n, $object_name, $handle ) { + $this->assertSame( 'testData', $object_name ); + $this->assertSame( 'test-script', $handle ); + $this->assertIsArray( $l10n ); + $this->assertSame( 'bar', $l10n['foo'] ); + $l10n['baz'] = 'qux'; + return $l10n; + }, + 10, + 3 + ); + + $output = get_echo( 'wp_print_scripts' ); + + $this->assertStringContainsString( '"foo":"bar"', $output ); + $this->assertStringContainsString( '"baz":"qux"', $output ); + } + + /** + * Tests that the script_data_{$handle} filter receives correct parameters. + * + * @ticket TBD + * @covers WP_Scripts::localize + */ + public function test_script_data_filter_receives_correct_parameters() { + wp_enqueue_script( 'test-handle', '/test.js', array(), null ); + wp_localize_script( 'test-handle', 'myObject', array( 'key' => 'value' ) ); + + $filter_called = false; + add_filter( + 'script_data_test-handle', + function ( $l10n, $object_name, $handle ) use ( &$filter_called ) { + $filter_called = true; + $this->assertSame( array( 'key' => 'value' ), $l10n ); + $this->assertSame( 'myObject', $object_name ); + $this->assertSame( 'test-handle', $handle ); + return $l10n; + }, + 10, + 3 + ); + + get_echo( 'wp_print_scripts' ); + + $this->assertTrue( $filter_called, 'Filter should have been called' ); + } + + /** + * Tests that the script_data_{$handle} filter works with multiple localizations. + * + * @ticket TBD + * @covers WP_Scripts::localize + */ + public function test_script_data_filter_with_multiple_localizations() { + wp_enqueue_script( 'test-script', '/test.js', array(), null ); + wp_localize_script( 'test-script', 'data1', array( 'a' => '1' ) ); + wp_localize_script( 'test-script', 'data2', array( 'b' => '2' ) ); + + $filter_call_count = 0; + add_filter( + 'script_data_test-script', + function ( $l10n ) use ( &$filter_call_count ) { + $filter_call_count++; + $l10n['modified'] = 'yes'; + return $l10n; + } + ); + + $output = get_echo( 'wp_print_scripts' ); + + $this->assertSame( 2, $filter_call_count, 'Filter should be called twice for two localizations' ); + $this->assertStringContainsString( '"modified":"yes"', $output ); + } + + /** + * Tests that the script_data_{$handle} filter can return the data unmodified. + * + * @ticket TBD + * @covers WP_Scripts::localize + */ + public function test_script_data_filter_returns_data_unmodified() { + wp_enqueue_script( 'test-script', '/test.js', array(), null ); + wp_localize_script( 'test-script', 'testData', array( 'foo' => 'bar' ) ); + + add_filter( + 'script_data_test-script', + function ( $l10n ) { + // Return data unmodified. + return $l10n; + } + ); + + $output = get_echo( 'wp_print_scripts' ); + + $this->assertStringContainsString( 'var testData = {"foo":"bar"};', $output ); + } + + /** + * Tests that the script_data_{$handle} filter works correctly with jquery handle remapping. + * + * @ticket TBD + * @covers WP_Scripts::localize + */ + public function test_script_data_filter_with_jquery_handle() { + wp_enqueue_script( 'jquery' ); + wp_localize_script( 'jquery', 'jqueryData', array( 'test' => 'value' ) ); + + $filter_called = false; + add_filter( + 'script_data_jquery-core', + function ( $l10n ) use ( &$filter_called ) { + $filter_called = true; + $l10n['filtered'] = 'true'; + return $l10n; + } + ); + + $output = get_echo( 'wp_print_scripts' ); + + $this->assertTrue( $filter_called, 'Filter should be called for jquery-core handle' ); + $this->assertStringContainsString( '"filtered":"true"', $output ); + } } From f4afe132d99b799e435498ef5d9507a0f3e51c49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:20:44 +0000 Subject: [PATCH 03/12] Fix test docblocks - remove TBD ticket annotations Co-authored-by: sirreal <841763+sirreal@users.noreply.github.com> --- tests/phpunit/tests/dependencies/scripts.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 55d1b5d7d0129..afdac92106b72 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4126,7 +4126,6 @@ public function test_wp_scripts_doing_it_wrong_for_missing_dependencies() { /** * Tests that the script_data_{$handle} filter allows modifying localized script data. * - * @ticket TBD * @covers WP_Scripts::localize */ public function test_script_data_filter_modifies_localized_data() { @@ -4156,7 +4155,6 @@ function ( $l10n, $object_name, $handle ) { /** * Tests that the script_data_{$handle} filter receives correct parameters. * - * @ticket TBD * @covers WP_Scripts::localize */ public function test_script_data_filter_receives_correct_parameters() { @@ -4185,7 +4183,6 @@ function ( $l10n, $object_name, $handle ) use ( &$filter_called ) { /** * Tests that the script_data_{$handle} filter works with multiple localizations. * - * @ticket TBD * @covers WP_Scripts::localize */ public function test_script_data_filter_with_multiple_localizations() { @@ -4212,7 +4209,6 @@ function ( $l10n ) use ( &$filter_call_count ) { /** * Tests that the script_data_{$handle} filter can return the data unmodified. * - * @ticket TBD * @covers WP_Scripts::localize */ public function test_script_data_filter_returns_data_unmodified() { @@ -4235,7 +4231,6 @@ function ( $l10n ) { /** * Tests that the script_data_{$handle} filter works correctly with jquery handle remapping. * - * @ticket TBD * @covers WP_Scripts::localize */ public function test_script_data_filter_with_jquery_handle() { From d3cd233401d1c9dd531fc5e020b079dcc376c02c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:29:28 +0000 Subject: [PATCH 04/12] Implement script_data_{$handle} filter correctly - outputs JSON script tags separate from wp_localize_script Co-authored-by: sirreal <841763+sirreal@users.noreply.github.com> --- src/wp-admin/includes/admin-filters.php | 1 + src/wp-includes/class-wp-scripts.php | 133 ++++++++++++-- src/wp-includes/default-filters.php | 1 + src/wp-includes/functions.wp-scripts.php | 17 ++ tests/phpunit/tests/dependencies/scripts.php | 177 +++++++++++-------- 5 files changed, 243 insertions(+), 86 deletions(-) diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 6776f5898ad58..e48cac473ef4d 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -59,6 +59,7 @@ add_action( 'admin_print_scripts', 'print_emoji_detection_script' ); add_action( 'admin_print_scripts', 'print_head_scripts', 20 ); add_action( 'admin_print_footer_scripts', '_wp_footer_scripts' ); +add_action( 'admin_print_footer_scripts', 'wp_print_script_data', 21 ); add_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' ); add_action( 'admin_print_styles', 'print_emoji_styles' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_emoji_styles(). add_action( 'admin_print_styles', 'print_admin_styles', 20 ); diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 1bbd6d66ca012..e710b371ae5e6 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -634,23 +634,6 @@ public function localize( $handle, $object_name, $l10n ) { } } - /** - * Filters data associated with a given script. - * - * The dynamic portion of the hook name, `$handle`, refers to the script handle. - * - * This filter allows developers to modify the data passed to a script via - * wp_localize_script() before it is output. This is analogous to the - * `script_module_data_{$module_id}` filter for script modules. - * - * @since 6.8.0 - * - * @param array|string $l10n The data to be localized. - * @param string $object_name The JavaScript object name. - * @param string $handle The script handle. - */ - $l10n = apply_filters( "script_data_{$handle}", $l10n, $object_name, $handle ); - $script = "var $object_name = " . wp_json_encode( $l10n, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . ';'; if ( ! empty( $after ) ) { @@ -1199,4 +1182,120 @@ protected function get_dependency_warning_message( $handle, $missing_dependency_ implode( ', ', $missing_dependency_handles ) ); } + + /** + * Prints data associated with scripts. + * + * The data will be embedded in the page HTML and can be read by scripts on page load. + * + * Data can be associated with a script via the {@see "script_data_{$handle}"} filter. + * + * The data for a script will be serialized as JSON in a script tag with an ID of the + * form `wp-script-data-{$handle}`. + * + * @since 6.8.0 + */ + public function print_script_data() { + $scripts = array(); + + // Collect all enqueued scripts and their dependencies. + foreach ( array_unique( $this->queue ) as $handle ) { + $scripts[ $handle ] = true; + } + + foreach ( array_keys( $scripts ) as $handle ) { + /** + * Filters data associated with a given script. + * + * The dynamic portion of the hook name, `$handle`, refers to the script handle. + * + * 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. + * + * 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-script-handle', + * function ( array $data ): array { + * $data['myData'] = array( + * 'option' => get_option( 'my_option' ), + * ); + * 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-script-handle' ); + * let data = {}; + * if ( dataContainer ) { + * try { + * data = JSON.parse( dataContainer.textContent ); + * } catch {} + * } + * initMyScriptWithData( data ); + * + * @since 6.8.0 + * + * @param array $data The data associated with the script. + */ + $data = apply_filters( "script_data_{$handle}", array() ); + + if ( is_array( $data ) && array() !== $data ) { + /* + * This data will be printed as JSON inside a script tag like this: + * + * + * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script>`. + * + * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. + * - JSON_UNESCAPED_SLASHES: Don't escape /. + * + * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: + * + * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). + * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when + * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was + * before PHP 7.1 without this constant. Available as of PHP 7.1.0. + * + * The JSON specification requires encoding in UTF-8, so if the generated HTML page + * is not encoded in UTF-8 then it's not safe to include those literals. They must + * be escaped to avoid encoding issues. + * + * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. + * @see https://www.php.net/manual/en/json.constants.php for details on these constants. + * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. + */ + $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; + } + + wp_print_inline_script_tag( + (string) wp_json_encode( + $data, + $json_encode_flags + ), + array( + 'type' => 'application/json', + 'id' => "wp-script-data-{$handle}", + ) + ); + } + } + } } diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 68dccd979f2fe..0eac638ddcce7 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -361,6 +361,7 @@ add_action( 'wp_head', 'wp_site_icon', 99 ); add_action( 'wp_footer', 'wp_print_speculation_rules' ); add_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); +add_action( 'wp_footer', 'wp_print_script_data', 21 ); add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 ); add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); add_action( 'init', '_register_core_block_patterns_and_categories' ); diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index f86b456d5f69a..c34b8bca92df4 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -450,3 +450,20 @@ function wp_script_is( $handle, $status = 'enqueued' ) { function wp_script_add_data( $handle, $key, $value ) { return wp_scripts()->add_data( $handle, $key, $value ); } + +/** + * Prints data associated with enqueued scripts. + * + * @since 6.8.0 + * + * @see WP_Scripts::print_script_data() + */ +function wp_print_script_data() { + global $wp_scripts; + + if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { + return; + } + + $wp_scripts->print_script_data(); +} diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index afdac92106b72..3e7be7234baf0 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4124,132 +4124,171 @@ public function test_wp_scripts_doing_it_wrong_for_missing_dependencies() { } /** - * Tests that the script_data_{$handle} filter allows modifying localized script data. + * Tests that the script_data_{$handle} filter outputs JSON script tags. * - * @covers WP_Scripts::localize + * @covers WP_Scripts::print_script_data */ - public function test_script_data_filter_modifies_localized_data() { + public function test_script_data_filter_outputs_json_script_tag() { wp_enqueue_script( 'test-script', '/test.js', array(), null ); - wp_localize_script( 'test-script', 'testData', array( 'foo' => 'bar' ) ); add_filter( 'script_data_test-script', - function ( $l10n, $object_name, $handle ) { - $this->assertSame( 'testData', $object_name ); - $this->assertSame( 'test-script', $handle ); - $this->assertIsArray( $l10n ); - $this->assertSame( 'bar', $l10n['foo'] ); - $l10n['baz'] = 'qux'; - return $l10n; - }, - 10, - 3 + function ( $data ) { + $data['foo'] = 'bar'; + return $data; + } ); - $output = get_echo( 'wp_print_scripts' ); + $output = get_echo( 'wp_print_script_data' ); + $this->assertStringContainsString( ' + * + * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script>`. + * + * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. + * - JSON_UNESCAPED_SLASHES: Don't escape /. + * + * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: + * + * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). + * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when + * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was + * before PHP 7.1 without this constant. Available as of PHP 7.1.0. + * + * The JSON specification requires encoding in UTF-8, so if the generated HTML page + * is not encoded in UTF-8 then it's not safe to include those literals. They must + * be escaped to avoid encoding issues. + * + * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. + * @see https://www.php.net/manual/en/json.constants.php for details on these constants. + * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. + */ + $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; } - foreach ( array_keys( $scripts ) as $handle ) { + foreach ( array_unique( $this->queue ) as $handle ) { /** * Filters data associated with a given script. * @@ -1253,38 +1280,7 @@ public function print_script_data() { */ $data = apply_filters( "script_data_{$handle}", array() ); - if ( is_array( $data ) && array() !== $data ) { - /* - * This data will be printed as JSON inside a script tag like this: - * - * - * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script>`. - * - * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. - * - JSON_UNESCAPED_SLASHES: Don't escape /. - * - * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: - * - * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). - * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when - * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was - * before PHP 7.1 without this constant. Available as of PHP 7.1.0. - * - * The JSON specification requires encoding in UTF-8, so if the generated HTML page - * is not encoded in UTF-8 then it's not safe to include those literals. They must - * be escaped to avoid encoding issues. - * - * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. - * @see https://www.php.net/manual/en/json.constants.php for details on these constants. - * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. - */ - $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; - } - + if ( is_array( $data ) && ! empty( $data ) ) { wp_print_inline_script_tag( (string) wp_json_encode( $data, From 14e4ff92009298da32662e6da7d035d3d883bea6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:33:50 +0000 Subject: [PATCH 06/12] Remove redundant is_array check and improve test naming Co-authored-by: sirreal <841763+sirreal@users.noreply.github.com> --- src/wp-includes/class-wp-scripts.php | 2 +- tests/phpunit/tests/dependencies/scripts.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 034355a6a3309..affc55adac562 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -1280,7 +1280,7 @@ public function print_script_data() { */ $data = apply_filters( "script_data_{$handle}", array() ); - if ( is_array( $data ) && ! empty( $data ) ) { + if ( ! empty( $data ) ) { wp_print_inline_script_tag( (string) wp_json_encode( $data, diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 3e7be7234baf0..e57418c3c957b 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4124,11 +4124,11 @@ public function test_wp_scripts_doing_it_wrong_for_missing_dependencies() { } /** - * Tests that the script_data_{$handle} filter outputs JSON script tags. + * Tests that print_script_data outputs JSON script tags. * * @covers WP_Scripts::print_script_data */ - public function test_script_data_filter_outputs_json_script_tag() { + public function test_print_script_data_outputs_json_script_tag() { wp_enqueue_script( 'test-script', '/test.js', array(), null ); add_filter( From 16c9b3fb2d2731f2b24d2b27e122ae6b87e1272b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:46:51 +0000 Subject: [PATCH 07/12] Refactor: Integrate script_data filter directly into do_item() method - Removed separate print_script_data() method and wp_print_script_data() wrapper - Removed action hooks from wp_footer and admin_print_footer_scripts - Filter now runs during script processing in do_item() - Data script tag is output immediately before each script tag - Updated all tests to use wp_print_scripts instead of wp_print_script_data - Added test to verify data tag appears before script tag Co-authored-by: sirreal <841763+sirreal@users.noreply.github.com> --- src/wp-admin/includes/admin-filters.php | 1 - src/wp-includes/class-wp-scripts.php | 207 +++++++++---------- src/wp-includes/default-filters.php | 1 - src/wp-includes/functions.wp-scripts.php | 17 -- tests/phpunit/tests/dependencies/scripts.php | 54 +++-- 5 files changed, 120 insertions(+), 160 deletions(-) diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index e48cac473ef4d..6776f5898ad58 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -59,7 +59,6 @@ add_action( 'admin_print_scripts', 'print_emoji_detection_script' ); add_action( 'admin_print_scripts', 'print_head_scripts', 20 ); add_action( 'admin_print_footer_scripts', '_wp_footer_scripts' ); -add_action( 'admin_print_footer_scripts', 'wp_print_script_data', 21 ); add_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' ); add_action( 'admin_print_styles', 'print_emoji_styles' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_emoji_styles(). add_action( 'admin_print_styles', 'print_admin_styles', 20 ); diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index affc55adac562..15796241193c4 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -452,7 +452,101 @@ public function do_item( $handle, $group = false ) { $attr['data-wp-fetchpriority'] = $original_fetchpriority; } - $tag = $translations . $before_script; + /** + * 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. + * + * 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-script-handle', + * function ( array $data ): array { + * $data['myConfig'] = array( 'key' => 'value' ); + * 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}` and type `application/json`. + * + * The data can be read on the client with a pattern like this: + * + * Example: + * + * const dataContainer = document.getElementById( 'wp-script-data-my-script-handle' ); + * let data = {}; + * if ( dataContainer ) { + * try { + * data = JSON.parse( dataContainer.textContent ); + * } catch {} + * } + * // data.myConfig.key === 'value'; + * initMyScriptWithData( data ); + * + * @since 6.8.0 + * + * @param array $data The data associated with the script. + */ + $script_data = apply_filters( "script_data_{$handle}", array() ); + + $data_tag = ''; + if ( ! empty( $script_data ) ) { + /* + * This data will be printed as JSON inside a script tag like this: + * + * + * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script>`. + * + * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. + * - JSON_UNESCAPED_SLASHES: Don't escape /. + * + * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: + * + * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). + * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when + * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was + * before PHP 7.1 without this constant. Available as of PHP 7.1.0. + * + * The JSON specification requires encoding in UTF-8, so if the generated HTML page + * is not encoded in UTF-8 then it's not safe to include those literals. They must + * be escaped to avoid encoding issues. + * + * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. + * @see https://www.php.net/manual/en/json.constants.php for details on these constants. + * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. + */ + $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; + } + + $data_tag = wp_print_inline_script_tag( + (string) wp_json_encode( + $script_data, + $json_encode_flags + ), + array( + 'type' => 'application/json', + 'id' => "wp-script-data-{$handle}", + ), + false + ); + } + + $tag = $translations . $before_script . $data_tag; $tag .= wp_get_script_tag( $attr ); $tag .= $after_script; @@ -1183,115 +1277,4 @@ protected function get_dependency_warning_message( $handle, $missing_dependency_ ); } - /** - * Prints data associated with scripts. - * - * The data will be embedded in the page HTML and can be read by scripts on page load. - * - * Data can be associated with a script via the {@see "script_data_{$handle}"} filter. - * - * The data for a script will be serialized as JSON in a script tag with an ID of the - * form `wp-script-data-{$handle}`. - * - * @since 6.8.0 - */ - public function print_script_data() { - /* - * Determine JSON encoding flags once, outside the loop. - * The charset won't change during iteration. - * - * This data will be printed as JSON inside a script tag like this: - * - * - * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script>`. - * - * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. - * - JSON_UNESCAPED_SLASHES: Don't escape /. - * - * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: - * - * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). - * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when - * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was - * before PHP 7.1 without this constant. Available as of PHP 7.1.0. - * - * The JSON specification requires encoding in UTF-8, so if the generated HTML page - * is not encoded in UTF-8 then it's not safe to include those literals. They must - * be escaped to avoid encoding issues. - * - * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. - * @see https://www.php.net/manual/en/json.constants.php for details on these constants. - * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. - */ - $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; - } - - foreach ( array_unique( $this->queue ) as $handle ) { - /** - * Filters data associated with a given script. - * - * The dynamic portion of the hook name, `$handle`, refers to the script handle. - * - * 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. - * - * 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-script-handle', - * function ( array $data ): array { - * $data['myData'] = array( - * 'option' => get_option( 'my_option' ), - * ); - * 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-script-handle' ); - * let data = {}; - * if ( dataContainer ) { - * try { - * data = JSON.parse( dataContainer.textContent ); - * } catch {} - * } - * initMyScriptWithData( data ); - * - * @since 6.8.0 - * - * @param array $data The data associated with the script. - */ - $data = apply_filters( "script_data_{$handle}", array() ); - - if ( ! empty( $data ) ) { - wp_print_inline_script_tag( - (string) wp_json_encode( - $data, - $json_encode_flags - ), - array( - 'type' => 'application/json', - 'id' => "wp-script-data-{$handle}", - ) - ); - } - } - } } diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 0eac638ddcce7..68dccd979f2fe 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -361,7 +361,6 @@ add_action( 'wp_head', 'wp_site_icon', 99 ); add_action( 'wp_footer', 'wp_print_speculation_rules' ); add_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); -add_action( 'wp_footer', 'wp_print_script_data', 21 ); add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 ); add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); add_action( 'init', '_register_core_block_patterns_and_categories' ); diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index c34b8bca92df4..f86b456d5f69a 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -450,20 +450,3 @@ function wp_script_is( $handle, $status = 'enqueued' ) { function wp_script_add_data( $handle, $key, $value ) { return wp_scripts()->add_data( $handle, $key, $value ); } - -/** - * Prints data associated with enqueued scripts. - * - * @since 6.8.0 - * - * @see WP_Scripts::print_script_data() - */ -function wp_print_script_data() { - global $wp_scripts; - - if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { - return; - } - - $wp_scripts->print_script_data(); -} diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index e57418c3c957b..44f097eba68fb 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4124,11 +4124,11 @@ public function test_wp_scripts_doing_it_wrong_for_missing_dependencies() { } /** - * Tests that print_script_data outputs JSON script tags. + * Tests that script_data_{$handle} filter outputs JSON script tags before the script. * - * @covers WP_Scripts::print_script_data + * @covers WP_Scripts::do_item */ - public function test_print_script_data_outputs_json_script_tag() { + public function test_script_data_filter_outputs_json_script_tag() { wp_enqueue_script( 'test-script', '/test.js', array(), null ); add_filter( @@ -4139,7 +4139,7 @@ function ( $data ) { } ); - $output = get_echo( 'wp_print_script_data' ); + $output = get_echo( 'wp_print_scripts' ); $this->assertStringContainsString( '