From a3520d2720f6d434b78346dd53cb38ec88c74bcb Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 25 Aug 2024 23:47:01 +0000 Subject: [PATCH] Script Loader: Refactor Etag generation for concatenated assets. Move Etag HTTP header generation in `load-scripts.php` and `load-styles.php` to `WP_Dependencies`. Introduces the method `WP_Dependencies::get_etag()` and associated unit tests. Follow up to [57943]. Props vrajadas, martinkrcho, mukesh27. Fixes #61485. git-svn-id: https://develop.svn.wordpress.org/trunk@58935 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/load-scripts.php | 19 +---- src/wp-admin/load-styles.php | 19 +---- src/wp-includes/class-wp-dependencies.php | 38 ++++++++++ tests/phpunit/tests/dependencies.php | 90 +++++++++++++++++++++++ 4 files changed, 130 insertions(+), 36 deletions(-) diff --git a/src/wp-admin/load-scripts.php b/src/wp-admin/load-scripts.php index b802394ece309..e4b6a1ee5b918 100644 --- a/src/wp-admin/load-scripts.php +++ b/src/wp-admin/load-scripts.php @@ -52,24 +52,7 @@ wp_default_packages_vendor( $wp_scripts ); wp_default_packages_scripts( $wp_scripts ); -$etag = "WP:{$wp_version};"; - -foreach ( $load as $handle ) { - if ( ! array_key_exists( $handle, $wp_scripts->registered ) ) { - continue; - } - - $ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_version; - $etag .= "{$handle}:{$ver};"; -} - -/* - * This is not intended to be cryptographically secure, just a fast way to get - * a fixed length string based on the script versions. As this file does not - * load the full WordPress environment, it is not possible to use the salted - * wp_hash() function. - */ -$etag = 'W/"' . md5( $etag ) . '"'; +$etag = $wp_scripts->get_etag( $load ); if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) && stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) === $etag ) { header( "$protocol 304 Not Modified" ); diff --git a/src/wp-admin/load-styles.php b/src/wp-admin/load-styles.php index 9e985569aab7d..083c3832eb8cb 100644 --- a/src/wp-admin/load-styles.php +++ b/src/wp-admin/load-styles.php @@ -55,24 +55,7 @@ $wp_styles = new WP_Styles(); wp_default_styles( $wp_styles ); -$etag = "WP:{$wp_version};"; - -foreach ( $load as $handle ) { - if ( ! array_key_exists( $handle, $wp_styles->registered ) ) { - continue; - } - - $ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_version; - $etag .= "{$handle}:{$ver};"; -} - -/* - * This is not intended to be cryptographically secure, just a fast way to get - * a fixed length string based on the script versions. As this file does not - * load the full WordPress environment, it is not possible to use the salted - * wp_hash() function. - */ -$etag = 'W/"' . md5( $etag ) . '"'; +$etag = $wp_styles->get_etag( $wp_version, $load ); if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) && stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) === $etag ) { header( "$protocol 304 Not Modified" ); diff --git a/src/wp-includes/class-wp-dependencies.php b/src/wp-includes/class-wp-dependencies.php index e2bbf4ff98bb7..ef9dfa7d5fd49 100644 --- a/src/wp-includes/class-wp-dependencies.php +++ b/src/wp-includes/class-wp-dependencies.php @@ -490,4 +490,42 @@ public function set_group( $handle, $recursion, $group ) { return true; } + + /** + * Get etag header for cache validation. + * + * @since 6.7.0 + * + * @global string $wp_version The WordPress version string. + * + * @param string[] $load Array of script or style handles to load. + * @return string Etag header. + */ + public function get_etag( $load ) { + /* + * Note: wp_get_wp_version() is not used here, as this file can be included + * via wp-admin/load-scripts.php or wp-admin/load-styles.php, in which case + * wp-includes/functions.php is not loaded. + */ + global $wp_version; + + $etag = "WP:{$wp_version};"; + + foreach ( $load as $handle ) { + if ( ! array_key_exists( $handle, $this->registered ) ) { + continue; + } + + $ver = $this->registered[ $handle ]->ver ?? $wp_version; + $etag .= "{$handle}:{$ver};"; + } + + /* + * This is not intended to be cryptographically secure, just a fast way to get + * a fixed length string based on the script versions. As this file does not + * load the full WordPress environment, it is not possible to use the salted + * wp_hash() function. + */ + return 'W/"' . md5( $etag ) . '"'; + } } diff --git a/tests/phpunit/tests/dependencies.php b/tests/phpunit/tests/dependencies.php index 39a2860fc365e..dfcd253239583 100644 --- a/tests/phpunit/tests/dependencies.php +++ b/tests/phpunit/tests/dependencies.php @@ -148,4 +148,94 @@ public function test_enqueue_before_register() { $this->assertContains( 'one', $dep->queue ); } + + /** + * Data provider for test_get_etag. + * + * @return array[] + */ + public function data_provider_get_etag() { + return array( + 'should accept one dependency' => array( + 'load' => array( + 'abcd' => '1.0.2', + ), + 'hash_source_string' => 'WP:6.7;abcd:1.0.2;', + 'expected' => 'W/"8145d7e3c41d5a9cc2bccba4afa861fc"', + ), + 'should accept empty array of dependencies' => array( + 'load' => array(), + 'hash_source_string' => 'WP:6.7;', + 'expected' => 'W/"7ee896c19250a3d174f11469a4ad0b1e"', + ), + ); + } + + /** + * Tests get_etag method for WP_Scripts. + * + * @ticket 58433 + * @ticket 61485 + * + * @covers WP_Dependencies::get_etag + * + * @dataProvider data_provider_get_etag + * + * @param array $load List of scripts to load. + * @param string $hash_source_string Hash source string. + * @param string $expected Expected etag. + */ + public function test_get_etag_scripts( $load, $hash_source_string, $expected ) { + global $wp_version; + // Modify global to avoid tests needing to change with each new version of WordPress. + $original_wp_version = $wp_version; + $wp_version = '6.7'; + $instance = wp_scripts(); + + foreach ( $load as $handle => $ver ) { + // The src should not be empty. + wp_enqueue_script( $handle, 'https://example.org', array(), $ver ); + } + + $result = $instance->get_etag( array_keys( $load ) ); + + // Restore global prior to making assertions. + $wp_version = $original_wp_version; + + $this->assertSame( $expected, $result, "Expected MD hash: $expected for $hash_source_string, but got: $result." ); + } + + /** + * Tests get_etag method for WP_Styles. + * + * @ticket 58433 + * @ticket 61485 + * + * @covers WP_Dependencies::get_etag + * + * @dataProvider data_provider_get_etag + * + * @param array $load List of styles to load. + * @param string $hash_source_string Hash source string. + * @param string $expected Expected etag. + */ + public function test_get_etag_styles( $load, $hash_source_string, $expected ) { + global $wp_version; + // Modify global to avoid tests needing to change with each new version of WordPress. + $original_wp_version = $wp_version; + $wp_version = '6.7'; + $instance = wp_scripts(); + + foreach ( $load as $handle => $ver ) { + // The src should not be empty. + wp_enqueue_style( $handle, 'https://example.cdn', array(), $ver ); + } + + $result = $instance->get_etag( array_keys( $load ) ); + + // Restore global prior to making assertions. + $wp_version = $original_wp_version; + + $this->assertSame( $expected, $result, "Expected MD hash: $expected for $hash_source_string, but got: $result." ); + } }