From 53546027d923a072ee5537cf531d3e7a585038dd Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 16:44:13 +0200 Subject: [PATCH 1/9] Add classic script data filter output --- src/wp-includes/class-wp-scripts.php | 32 ++++++++++++++++++++ tests/phpunit/tests/dependencies/scripts.php | 21 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index e48658a1e7f7c..f1f3d7834000a 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -249,6 +249,37 @@ public function print_extra_script( $handle, $display = true ) { return true; } + /** + * Prints data associated with a registered script. + * + * @since 7.1.0 + * + * @param string $handle The script's registered handle. + */ + private function print_script_data( $handle ) { + /** + * Filters data associated with a given script. + * + * The dynamic portion of the hook name, `$handle`, refers to the script handle + * that the data is associated with. + * + * @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 ) { + wp_print_inline_script_tag( + (string) wp_json_encode( $data ), + array( + 'type' => 'application/json', + 'id' => "wp-script-data-{$handle}", + ) + ); + } + } + /** * Checks whether all dependents of a given handle are in the footer. * @@ -398,6 +429,7 @@ public function do_item( $handle, $group = false ) { } } + $this->print_script_data( $handle ); $this->print_extra_script( $handle ); // A single item may alias a set of items, by having dependencies, but no source. diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 73c60dcffa8c0..28e6150983538 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -2660,6 +2660,27 @@ 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 = "\n"; + $expected .= "\n"; + $expected .= "\n"; + + $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); + } + /** * @ticket 14853 */ From 65e31871d134a473d5f3290faf3cca0b4ed4ef38 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 16:47:21 +0200 Subject: [PATCH 2/9] Test empty classic script data output --- tests/phpunit/tests/dependencies/scripts.php | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 28e6150983538..a888cce95bb6f 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -2681,6 +2681,58 @@ static function ( $data ) { $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 = "\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 = "\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 14853 */ From 20d82d4ffcb634a265ced258e08900eb2abd8912 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 16:52:47 +0200 Subject: [PATCH 3/9] Match script data JSON encoding --- src/wp-includes/class-wp-scripts.php | 7 +- tests/phpunit/tests/dependencies/scripts.php | 73 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index f1f3d7834000a..fec13a9295bc3 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -270,8 +270,13 @@ private function print_script_data( $handle ) { $data = apply_filters( "script_data_{$handle}", array() ); if ( is_array( $data ) && array() !== $data ) { + $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 ), + (string) wp_json_encode( $data, $json_encode_flags ), array( 'type' => 'application/json', 'id' => "wp-script-data-{$handle}", diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index a888cce95bb6f..6b54e57245c96 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -2733,6 +2733,79 @@ public static function data_invalid_script_data(): array { ); } + /** + * @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 = "\n"; + $expected .= "\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( '', '\u003C/script\u003E', 'UTF-8' ), + 'Entity-encoded malicious script closer' => array( '</script>', '</script>', '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( '', '\u003C/script\u003E', 'iso-8859-1' ), + 'Entity-encoded malicious script closer non-utf8' => array( '</script>', '</script>', 'iso-8859-1' ), + ); + } + /** * @ticket 14853 */ From 9de557387e0d940a7f9c0fbdf888092fedbd82d1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 16:59:04 +0200 Subject: [PATCH 4/9] Prevent concat for classic script data --- src/wp-includes/class-wp-scripts.php | 36 +++++++++++--------- tests/phpunit/tests/dependencies/scripts.php | 26 ++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index fec13a9295bc3..0e6a87940161e 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -250,13 +250,14 @@ public function print_extra_script( $handle, $display = true ) { } /** - * Prints data associated with a registered script. + * 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 print_script_data( $handle ) { + private function get_script_data( $handle ) { /** * Filters data associated with a given script. * @@ -269,20 +270,22 @@ private function print_script_data( $handle ) { */ $data = apply_filters( "script_data_{$handle}", array() ); - if ( is_array( $data ) && array() !== $data ) { - $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 ) || array() === $data ) { + return ''; + } - wp_print_inline_script_tag( - (string) wp_json_encode( $data, $json_encode_flags ), - array( - 'type' => 'application/json', - 'id' => "wp-script-data-{$handle}", - ) - ); + $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}", + ) + ); } /** @@ -373,6 +376,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' ); @@ -416,7 +420,7 @@ public function do_item( $handle, $group = false ) { if ( is_string( $filtered_src ) && $this->in_default_dir( $filtered_src ) - && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) + && ( $script_data || $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) ) { $this->do_concat = false; @@ -434,7 +438,7 @@ public function do_item( $handle, $group = false ) { } } - $this->print_script_data( $handle ); + echo $script_data; $this->print_extra_script( $handle ); // A single item may alias a set of items, by having dependencies, but no source. diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 6b54e57245c96..0486148ddb11d 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -2765,6 +2765,32 @@ static function ( $data ) use ( $input ) { $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 = "\n"; + $expected .= "\n"; + $expected .= "\n"; + + $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); + } + /** * Data provider. * From bc388f12ef668bf400317eb817291d34ffc286a7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 17:04:48 +0200 Subject: [PATCH 5/9] Document classic script data filter --- src/wp-includes/class-wp-scripts.php | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 0e6a87940161e..59ac5fd4f3e4f 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -261,9 +261,46 @@ 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. From 14b834fb955071025128c608db3163740fa241d1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 17:19:20 +0200 Subject: [PATCH 6/9] Flush concat before external script data --- src/wp-includes/class-wp-scripts.php | 10 ++++++-- tests/phpunit/tests/dependencies/scripts.php | 26 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 59ac5fd4f3e4f..ef427d6380b72 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -454,10 +454,16 @@ 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 ) - && ( $script_data || $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) + && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) ) { $this->do_concat = false; diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 0486148ddb11d..1fb2e465a378d 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -2791,6 +2791,32 @@ static function ( $data ) { $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 = "\n"; + $expected .= "\n"; + $expected .= "\n"; + + $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); + } + /** * Data provider. * From aed8c333fceb5edef75af05ae2f8a3b2f82d34b8 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 1 Jul 2026 11:45:49 +0200 Subject: [PATCH 7/9] Rename script data filter to script client data --- src/wp-includes/class-wp-scripts.php | 66 +++++++++--------- tests/phpunit/tests/dependencies/scripts.php | 70 ++++++++++---------- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index ef427d6380b72..25f06c15e0b17 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -250,64 +250,64 @@ public function print_extra_script( $handle, $display = true ) { } /** - * Gets data associated with a registered script. + * Gets client 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. + * @return string Client data script tag, or empty string when no client data exists. */ - private function get_script_data( $handle ) { + private function get_script_client_data_tag( $handle ) { /** - * Filters data associated with a given script. + * Filters client 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 + * Scripts may require client data that is required for initialization or is + * essential to have 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. + * that the client 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. + * This is best suited to pass essential client data that must be available to the + * script for initialization or immediately on page load. It does not replace the + * REST API or client-side data fetching. * * Example: * * add_filter( - * 'script_data_my-handle', - * function ( array $data ): array { - * $data['dataForClient'] = 'ok'; - * return $data; + * 'script_client_data_my-handle', + * function ( array $client_data ): array { + * $client_data['dataForClient'] = 'ok'; + * return $client_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 client data for a given script, if provided, will be JSON serialized in a + * script tag with an ID of the form `wp-script-client-data-{$handle}`. * - * The data can be read on the client with a pattern like this: + * The client data can be read with a pattern like this: * * Example: * - * const dataContainer = document.getElementById( 'wp-script-data-my-handle' ); - * let data = {}; - * if ( dataContainer ) { + * const clientDataContainer = document.getElementById( 'wp-script-client-data-my-handle' ); + * let clientData = {}; + * if ( clientDataContainer ) { * try { - * data = JSON.parse( dataContainer.textContent ); + * clientData = JSON.parse( clientDataContainer.textContent ); * } catch {} * } - * // data.dataForClient === 'ok'; - * initMyScriptWithData( data ); + * // clientData.dataForClient === 'ok'; + * initMyScriptWithData( clientData ); * * @since 7.1.0 * - * @param array $data The data associated with the script. + * @param array $client_data The client data associated with the script. */ - $data = apply_filters( "script_data_{$handle}", array() ); + $client_data = apply_filters( "script_client_data_{$handle}", array() ); - if ( ! is_array( $data ) || array() === $data ) { + if ( ! is_array( $client_data ) || array() === $client_data ) { return ''; } @@ -317,10 +317,10 @@ private function get_script_data( $handle ) { } return wp_get_inline_script_tag( - (string) wp_json_encode( $data, $json_encode_flags ), + (string) wp_json_encode( $client_data, $json_encode_flags ), array( 'type' => 'application/json', - 'id' => "wp-script-data-{$handle}", + 'id' => "wp-script-client-data-{$handle}", ) ); } @@ -413,9 +413,9 @@ 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' ); + $client_data_tag = $this->get_script_client_data_tag( $handle ); + $before_script = $this->get_inline_script_tag( $handle, 'before' ); + $after_script = $this->get_inline_script_tag( $handle, 'after' ); if ( $before_script || $after_script ) { $inline_script_tag = $before_script . $after_script; @@ -454,7 +454,7 @@ public function do_item( $handle, $group = false ) { */ $filtered_src = apply_filters( 'script_loader_src', $src, $handle ); - if ( $script_data ) { + if ( $client_data_tag ) { $this->do_concat = false; // Have to print the so-far concatenated scripts right away to maintain the right order. @@ -481,7 +481,7 @@ public function do_item( $handle, $group = false ) { } } - echo $script_data; + echo $client_data_tag; $this->print_extra_script( $handle ); // A single item may alias a set of items, by having dependencies, but no source. diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 1fb2e465a378d..e3818fdb78a6f 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -2663,18 +2663,18 @@ public function test_wp_add_inline_script_localized_data_is_added_first() { /** * @ticket 58873 */ - public function test_script_data_filter_prints_data_before_localized_data() { + public function test_script_client_data_filter_prints_client_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; + 'script_client_data_test-example', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; } ); - $expected = "\n"; + $expected = "\n"; $expected .= "\n"; $expected .= "\n"; @@ -2684,12 +2684,12 @@ static function ( $data ) { /** * @ticket 58873 */ - public function test_script_data_filter_does_not_print_empty_data() { + public function test_script_client_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; + 'script_client_data_test-example', + static function ( $client_data ) { + return $client_data; } ); @@ -2701,14 +2701,14 @@ static function ( $data ) { /** * @ticket 58873 * - * @dataProvider data_invalid_script_data + * @dataProvider data_invalid_script_client_data * - * @param mixed $data Data to return in filter. + * @param mixed $data Client data to return in filter. */ - public function test_script_data_filter_does_not_print_invalid_data( $data ) { + public function test_script_client_data_filter_does_not_print_invalid_data( $data ) { wp_enqueue_script( 'test-example', 'example.com', array(), null ); add_filter( - 'script_data_test-example', + 'script_client_data_test-example', static function () use ( $data ) { return $data; } @@ -2724,7 +2724,7 @@ static function () use ( $data ) { * * @return array */ - public static function data_invalid_script_data(): array { + public static function data_invalid_script_client_data(): array { return array( 'null' => array( null ), 'stdClass' => array( new stdClass() ), @@ -2736,13 +2736,13 @@ public static function data_invalid_script_data(): array { /** * @ticket 58873 * - * @dataProvider data_script_data_encoding + * @dataProvider data_script_client_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 ) { + public function test_script_client_data_filter_encoding( $input, $expected, $charset ) { add_filter( 'pre_option_blog_charset', static function () use ( $charset ) { @@ -2752,14 +2752,14 @@ static function () use ( $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; + 'script_client_data_test-example', + static function ( $client_data ) use ( $input ) { + $client_data[''] = $input; + return $client_data; } ); - $expected = "\n"; + $expected = "\n"; $expected .= "\n"; $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); @@ -2768,7 +2768,7 @@ static function ( $data ) use ( $input ) { /** * @ticket 58873 */ - public function test_script_data_filter_prevents_concat() { + public function test_script_client_data_filter_prevents_concat() { global $wp_scripts, $wp_version; $wp_scripts->do_concat = true; @@ -2777,15 +2777,15 @@ public function test_script_data_filter_prevents_concat() { 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; + 'script_client_data_two', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; } ); $expected = "\n"; - $expected .= "\n"; + $expected .= "\n"; $expected .= "\n"; $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); @@ -2794,7 +2794,7 @@ static function ( $data ) { /** * @ticket 58873 */ - public function test_script_data_filter_for_external_script_flushes_concat_before_printing() { + public function test_script_client_data_filter_for_external_script_flushes_concat_before_printing() { global $wp_scripts, $wp_version; $wp_scripts->do_concat = true; @@ -2803,15 +2803,15 @@ public function test_script_data_filter_for_external_script_flushes_concat_befor 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; + 'script_client_data_two', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; } ); $expected = "\n"; - $expected .= "\n"; + $expected .= "\n"; $expected .= "\n"; $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); @@ -2822,7 +2822,7 @@ static function ( $data ) { * * @return array */ - public static function data_script_data_encoding(): array { + public static function data_script_client_data_encoding(): array { return array( // UTF-8. 'Solidus' => array( '/', '/', 'UTF-8' ), From 6160b87191fb09d65d028241df16856dcbb68a97 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 1 Jul 2026 12:31:13 +0200 Subject: [PATCH 8/9] Allow script client data in concatenated output --- src/wp-includes/class-wp-scripts.php | 38 +++++++++++++++------------- src/wp-includes/script-loader.php | 8 ++++++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 25f06c15e0b17..ac010c5ae2d50 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -95,6 +95,14 @@ class WP_Scripts extends WP_Dependencies { */ public $print_code = ''; + /** + * Holds client data HTML markup if concatenation is enabled. + * + * @since 7.1.0 + * @var string + */ + public $print_client_data = ''; + /** * Holds a list of script handles which are not in the default directory * if concatenation is enabled. @@ -454,13 +462,7 @@ public function do_item( $handle, $group = false ) { */ $filtered_src = apply_filters( 'script_loader_src', $src, $handle ); - if ( $client_data_tag ) { - $this->do_concat = false; - - // Have to print the so-far concatenated scripts right away to maintain the right order. - _print_scripts(); - $this->reset(); - } elseif ( + if ( is_string( $filtered_src ) && $this->in_default_dir( $filtered_src ) && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) @@ -471,9 +473,10 @@ public function do_item( $handle, $group = false ) { _print_scripts(); $this->reset(); } elseif ( $this->in_default_dir( $filtered_src ) ) { - $this->print_code .= $this->print_extra_script( $handle, false ); - $this->concat .= "$handle,"; - $this->concat_version .= "$handle$ver"; + $this->print_client_data .= $client_data_tag; + $this->print_code .= $this->print_extra_script( $handle, false ); + $this->concat .= "$handle,"; + $this->concat_version .= "$handle$ver"; return true; } else { $this->ext_handles .= "$handle,"; @@ -1305,13 +1308,14 @@ private function has_inline_script( $handle, $position = null ) { * @since 2.8.0 */ public function reset() { - $this->do_concat = false; - $this->print_code = ''; - $this->concat = ''; - $this->concat_version = ''; - $this->print_html = ''; - $this->ext_version = ''; - $this->ext_handles = ''; + $this->do_concat = false; + $this->print_code = ''; + $this->print_client_data = ''; + $this->concat = ''; + $this->concat_version = ''; + $this->print_html = ''; + $this->ext_version = ''; + $this->ext_handles = ''; } /** diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 299e8dc9b750f..9e83769cd34e9 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2229,6 +2229,14 @@ function _print_scripts() { $concat = trim( $wp_scripts->concat, ', ' ); if ( $concat ) { + /* + * Client data is inert JSON. When scripts are concatenated, print it + * before executable inline code and the bundle that may consume it. + */ + if ( ! empty( $wp_scripts->print_client_data ) ) { + echo $wp_scripts->print_client_data; + } + if ( ! empty( $wp_scripts->print_code ) ) { echo "\n\n"; - $expected .= "\n"; - $expected .= "\n"; + $expected = "\n"; + $expected .= "\n"; - $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); + $actual = get_echo( + static function () { + wp_print_scripts(); + _print_scripts(); + } + ); + + $this->assertEqualHTML( $expected, $actual ); } /** * @ticket 58873 */ - public function test_script_client_data_filter_for_external_script_flushes_concat_before_printing() { + public function test_script_client_data_filter_for_external_script_prints_before_deferred_concat_output() { global $wp_scripts, $wp_version; $wp_scripts->do_concat = true; @@ -2810,13 +2816,145 @@ static function ( $client_data ) { } ); - $expected = "\n"; - $expected .= "\n"; + $expected = "\n"; + $expected .= "\n"; $expected .= "\n"; + $actual = get_echo( + static function () { + wp_print_scripts(); + _print_scripts(); + } + ); + + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * @ticket 58873 + */ + public function test_script_client_data_filter_with_localized_data_prints_before_concat_inline_script() { + 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_localize_script( 'one', 'testExample', array( 'foo' => 'bar' ) ); + add_filter( + 'script_client_data_one', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; + } + ); + + $expected = "\n"; + $expected .= "\n\n"; + $expected .= "\n"; + + $actual = get_echo( + static function () { + wp_print_scripts(); + _print_scripts(); + } + ); + + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * @ticket 58873 + */ + public function test_script_client_data_filter_with_inline_script_still_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_add_inline_script( 'one', 'console.log("before one");', 'before' ); + wp_add_inline_script( 'one', 'console.log("after one");' ); + add_filter( + 'script_client_data_one', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; + } + ); + + $expected = "\n"; + $expected .= "\n"; + $expected .= "\n"; + $expected .= "\n"; + $this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) ); } + /** + * @expectedDeprecated WP_Dependencies->add_data() + * + * @ticket 58873 + */ + public function test_script_client_data_filter_does_not_print_for_conditional_script() { + global $wp_scripts; + + $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_script_add_data( 'one', 'conditional', 'gte IE 9' ); + add_filter( + 'script_client_data_one', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; + } + ); + + $actual = get_echo( + static function () { + wp_print_scripts(); + _print_scripts(); + } + ); + + $this->assertSame( '', $actual ); + } + + /** + * @ticket 58873 + */ + public function test_script_client_data_filter_prints_when_script_moves_to_footer() { + wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) ); + wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => true ) ); + add_filter( + 'script_client_data_script-a', + static function ( $client_data ) { + $client_data['clientData'] = 'ok'; + return $client_data; + } + ); + + $header = get_echo( + static function () { + wp_scripts()->do_head_items(); + } + ); + $footer = get_echo( + static function () { + wp_scripts()->do_footer_items(); + } + ); + + $expected_footer = "\n"; + $expected_footer .= "\n"; + $expected_footer .= "\n"; + + $this->assertSame( '', $header ); + $this->assertEqualHTML( $expected_footer, $footer, '', 'Expected client data for a moved script to print in the footer.' ); + } + /** * Data provider. *