From a3770fe40e7892e763178469252faf3379d87b7f Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 12 Dec 2025 21:36:52 +0000 Subject: [PATCH 01/30] Tests: Use `assertSame()` in some newly introduced tests. This ensures that not only the return values match the expected results, but also that their type is the same. Going forward, stricter type checking by using `assertSame()` should generally be preferred to `assertEquals()` where appropriate, to make the tests more reliable. Follow-up to [61032], [61045]. See #64324. git-svn-id: https://develop.svn.wordpress.org/trunk@61373 602fd350-edb4-49c9-b593-d223f7449a82 --- .../wpRestAbilitiesV1CategoriesController.php | 22 +++++++-------- .../wpRestAbilitiesV1ListController.php | 28 +++++++++---------- .../wpRestAbilitiesV1RunController.php | 20 ++++++------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php index 2fa665ed320a9..8a93c7a64047d 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php @@ -171,9 +171,9 @@ public function test_get_item(): void { $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'test-data-retrieval', $data['slug'] ); - $this->assertEquals( 'Data Retrieval', $data['label'] ); - $this->assertEquals( 'Abilities that retrieve and return data from the WordPress site.', $data['description'] ); + $this->assertSame( 'test-data-retrieval', $data['slug'] ); + $this->assertSame( 'Data Retrieval', $data['label'] ); + $this->assertSame( 'Abilities that retrieve and return data from the WordPress site.', $data['description'] ); $this->assertArrayHasKey( 'meta', $data ); } @@ -189,10 +189,10 @@ public function test_get_item_with_meta(): void { $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'test-communication', $data['slug'] ); + $this->assertSame( 'test-communication', $data['slug'] ); $this->assertArrayHasKey( 'meta', $data ); $this->assertIsArray( $data['meta'] ); - $this->assertEquals( 'high', $data['meta']['priority'] ); + $this->assertSame( 'high', $data['meta']['priority'] ); } /** @@ -212,8 +212,8 @@ public function test_get_item_with_selected_fields(): void { $data = $response->get_data(); $this->assertCount( 2, $data, 'Response should only contain the requested fields.' ); - $this->assertEquals( 'test-data-retrieval', $data['slug'] ); - $this->assertEquals( 'Data Retrieval', $data['label'] ); + $this->assertSame( 'test-data-retrieval', $data['slug'] ); + $this->assertSame( 'Data Retrieval', $data['label'] ); } /** @@ -230,7 +230,7 @@ public function test_get_item_not_found(): void { $this->assertEquals( 404, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_category_not_found', $data['code'] ); + $this->assertSame( 'rest_ability_category_not_found', $data['code'] ); } /** @@ -424,8 +424,8 @@ public function test_get_schema(): void { $this->assertArrayHasKey( 'schema', $data ); $schema = $data['schema']; - $this->assertEquals( 'ability-category', $schema['title'] ); - $this->assertEquals( 'object', $schema['type'] ); + $this->assertSame( 'ability-category', $schema['title'] ); + $this->assertSame( 'object', $schema['type'] ); $this->assertArrayHasKey( 'properties', $schema ); $properties = $schema['properties']; @@ -438,7 +438,7 @@ public function test_get_schema(): void { $this->assertArrayHasKey( 'meta', $properties ); $slug_property = $properties['slug']; - $this->assertEquals( 'string', $slug_property['type'] ); + $this->assertSame( 'string', $slug_property['type'] ); $this->assertTrue( $slug_property['readonly'] ); } diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php index 19e3522f01a9f..e64965242dc98 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php @@ -307,10 +307,10 @@ public function test_get_item(): void { $data = $response->get_data(); $this->assertCount( 7, $data, 'Response should contain all fields.' ); - $this->assertEquals( 'test/calculator', $data['name'] ); - $this->assertEquals( 'Calculator', $data['label'] ); - $this->assertEquals( 'Performs basic calculations', $data['description'] ); - $this->assertEquals( 'math', $data['category'] ); + $this->assertSame( 'test/calculator', $data['name'] ); + $this->assertSame( 'Calculator', $data['label'] ); + $this->assertSame( 'Performs basic calculations', $data['description'] ); + $this->assertSame( 'math', $data['category'] ); $this->assertArrayHasKey( 'input_schema', $data ); $this->assertArrayHasKey( 'output_schema', $data ); $this->assertArrayHasKey( 'meta', $data ); @@ -334,8 +334,8 @@ public function test_get_item_with_selected_fields(): void { $data = $response->get_data(); $this->assertCount( 2, $data, 'Response should only contain the requested fields.' ); - $this->assertEquals( 'test/calculator', $data['name'] ); - $this->assertEquals( 'Calculator', $data['label'] ); + $this->assertSame( 'test/calculator', $data['name'] ); + $this->assertSame( 'Calculator', $data['label'] ); } /** @@ -355,9 +355,9 @@ public function test_get_item_with_embed_context(): void { $data = $response->get_data(); $this->assertCount( 3, $data, 'Response should only contain the fields for embed context.' ); - $this->assertEquals( 'test/calculator', $data['name'] ); - $this->assertEquals( 'Calculator', $data['label'] ); - $this->assertEquals( 'math', $data['category'] ); + $this->assertSame( 'test/calculator', $data['name'] ); + $this->assertSame( 'Calculator', $data['label'] ); + $this->assertSame( 'math', $data['category'] ); } /** @@ -374,7 +374,7 @@ public function test_get_item_not_found(): void { $this->assertEquals( 404, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_not_found', $data['code'] ); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); } /** @@ -389,7 +389,7 @@ public function test_get_item_not_show_in_rest(): void { $this->assertEquals( 404, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_not_found', $data['code'] ); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); } /** @@ -581,8 +581,8 @@ public function test_get_schema(): void { $this->assertArrayHasKey( 'schema', $data ); $schema = $data['schema']; - $this->assertEquals( 'ability', $schema['title'] ); - $this->assertEquals( 'object', $schema['type'] ); + $this->assertSame( 'ability', $schema['title'] ); + $this->assertSame( 'object', $schema['type'] ); $this->assertArrayHasKey( 'properties', $schema ); $properties = $schema['properties']; @@ -745,7 +745,7 @@ public function test_filter_by_category(): void { // Should only have math category abilities foreach ( $data as $ability ) { - $this->assertEquals( 'math', $ability['category'], 'All abilities should be in math category' ); + $this->assertSame( 'math', $ability['category'], 'All abilities should be in math category' ); } // Should at least contain the calculator diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php index f4340f85fad81..bccc30c2f2e94 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php @@ -472,7 +472,7 @@ public function test_execute_destructive_ability_delete(): void { $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 'User successfully deleted!', $response->get_data() ); + $this->assertSame( 'User successfully deleted!', $response->get_data() ); } /** @@ -630,7 +630,7 @@ public function test_contextual_permission_check(): void { $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 'Success: test data', $response->get_data() ); + $this->assertSame( 'Success: test data', $response->get_data() ); } /** @@ -646,8 +646,8 @@ public function test_do_not_show_in_rest(): void { $this->assertEquals( 404, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_not_found', $data['code'] ); - $this->assertEquals( 'Ability not found.', $data['message'] ); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); + $this->assertSame( 'Ability not found.', $data['message'] ); } /** @@ -679,8 +679,8 @@ public function test_wp_error_return_handling(): void { $this->assertEquals( 500, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'test_error', $data['code'] ); - $this->assertEquals( 'This is a test error', $data['message'] ); + $this->assertSame( 'test_error', $data['code'] ); + $this->assertSame( 'This is a test error', $data['message'] ); } /** @@ -698,7 +698,7 @@ public function test_execute_non_existent_ability(): void { $this->assertEquals( 404, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_not_found', $data['code'] ); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); } /** @@ -714,8 +714,8 @@ public function test_run_endpoint_schema(): void { $this->assertArrayHasKey( 'schema', $data ); $schema = $data['schema']; - $this->assertEquals( 'ability-execution', $schema['title'] ); - $this->assertEquals( 'object', $schema['type'] ); + $this->assertSame( 'ability-execution', $schema['title'] ); + $this->assertSame( 'object', $schema['type'] ); $this->assertArrayHasKey( 'properties', $schema ); $this->assertArrayHasKey( 'result', $schema['properties'] ); } @@ -761,7 +761,7 @@ public function test_get_request_with_nested_input_array(): void { $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'nested', $data['level1']['level2']['value'] ); + $this->assertSame( 'nested', $data['level1']['level2']['value'] ); $this->assertEquals( array( 1, 2, 3 ), $data['array'] ); } From a8630ebdd88213733e6e438933e4b18614a58ad3 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 13 Dec 2025 20:10:50 +0000 Subject: [PATCH 02/30] Filesystem API: Pass correct `$file` value to `pre_unzip_file` and `unzip_file` filters. This commit ensures that the original `$file` argument passed to the function is not unintentionally overwritten by the use of the same variable name in two `foreach` loops. Follow-up to [56689]. Props sanchothefat, westonruter, mukesh27, SergeyBiryukov. Fixes #64398. git-svn-id: https://develop.svn.wordpress.org/trunk@61374 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/file.php | 22 +++++++++---------- .../tests/filesystem/unzipFilePclzip.php | 10 +++++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 0658662126a59..948cb8e88e86d 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -1896,14 +1896,14 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) { $uncompressed_size = 0; // Determine any children directories needed (From within the archive). - foreach ( $archive_files as $file ) { - if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory. + foreach ( $archive_files as $archive_file ) { + if ( str_starts_with( $archive_file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory. continue; } - $uncompressed_size += $file['size']; + $uncompressed_size += $archive_file['size']; - $needed_dirs[] = $to . untrailingslashit( $file['folder'] ? $file['filename'] : dirname( $file['filename'] ) ); + $needed_dirs[] = $to . untrailingslashit( $archive_file['folder'] ? $archive_file['filename'] : dirname( $archive_file['filename'] ) ); } // Enough space to unzip the file and copy its contents, with a 10% buffer. @@ -1967,22 +1967,22 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) { } // Extract the files from the zip. - foreach ( $archive_files as $file ) { - if ( $file['folder'] ) { + foreach ( $archive_files as $archive_file ) { + if ( $archive_file['folder'] ) { continue; } - - if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. + + if ( str_starts_with( $archive_file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. continue; } // Don't extract invalid files: - if ( 0 !== validate_file( $file['filename'] ) ) { + if ( 0 !== validate_file( $archive_file['filename'] ) ) { continue; } - if ( ! $wp_filesystem->put_contents( $to . $file['filename'], $file['content'], FS_CHMOD_FILE ) ) { - return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $file['filename'] ); + if ( ! $wp_filesystem->put_contents( $to . $archive_file['filename'], $archive_file['content'], FS_CHMOD_FILE ) ) { + return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $archive_file['filename'] ); } } diff --git a/tests/phpunit/tests/filesystem/unzipFilePclzip.php b/tests/phpunit/tests/filesystem/unzipFilePclzip.php index a53ce50a0df75..10e4b1082a794 100644 --- a/tests/phpunit/tests/filesystem/unzipFilePclzip.php +++ b/tests/phpunit/tests/filesystem/unzipFilePclzip.php @@ -37,7 +37,7 @@ public static function set_up_before_class() { */ public function test_should_apply_pre_unzip_file_filters() { $filter = new MockAction(); - add_filter( 'pre_unzip_file', array( $filter, 'filter' ) ); + add_filter( 'pre_unzip_file', array( $filter, 'filter' ), 10, 2 ); // Prepare test environment. $unzip_destination = self::$test_data_dir . 'archive/'; @@ -53,7 +53,8 @@ public function test_should_apply_pre_unzip_file_filters() { $this->rmdir( $unzip_destination ); $this->delete_folders( $unzip_destination ); - $this->assertSame( 1, $filter->get_call_count() ); + $this->assertSame( 1, $filter->get_call_count(), 'The filter should be called once.' ); + $this->assertSame( self::$test_data_dir . 'archive.zip', $filter->get_args()[0][1], 'The $file parameter should be correct.' ); } /** @@ -63,7 +64,7 @@ public function test_should_apply_pre_unzip_file_filters() { */ public function test_should_apply_unzip_file_filters() { $filter = new MockAction(); - add_filter( 'unzip_file', array( $filter, 'filter' ) ); + add_filter( 'unzip_file', array( $filter, 'filter' ), 10, 2 ); // Prepare test environment. $unzip_destination = self::$test_data_dir . 'archive/'; @@ -79,6 +80,7 @@ public function test_should_apply_unzip_file_filters() { $this->rmdir( $unzip_destination ); $this->delete_folders( $unzip_destination ); - $this->assertSame( 1, $filter->get_call_count() ); + $this->assertSame( 1, $filter->get_call_count(), 'The filter should be called once.' ); + $this->assertSame( self::$test_data_dir . 'archive.zip', $filter->get_args()[0][1], 'The $file parameter should be correct.' ); } } From 048ea7405f23df412bd49367f8697be21a666657 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 14 Dec 2025 06:57:50 +0000 Subject: [PATCH 03/30] Coding Standards: Remove whitespace at end of line. Fixes PHPCS warning: `Squiz.WhiteSpace.SuperfluousWhitespace.EndLine`. Follow-up to [61374]. See #64398. git-svn-id: https://develop.svn.wordpress.org/trunk@61375 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/file.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 948cb8e88e86d..3627a16d3ecda 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -1971,7 +1971,7 @@ function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) { if ( $archive_file['folder'] ) { continue; } - + if ( str_starts_with( $archive_file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files. continue; } From 355672f293542a27998c095104eaeca983bd913e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 14 Dec 2025 07:11:49 +0000 Subject: [PATCH 04/30] Taxonomy: Avoid type error in `wp_delete_object_term_relationships()` when invalid taxonomy supplied. Developed in https://github.com/WordPress/wordpress-develop/pull/10621 Props owolter, westonruter. Fixes #64406. git-svn-id: https://develop.svn.wordpress.org/trunk@61376 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/taxonomy.php | 4 ++++ .../term/wpDeleteObjectTermRelationships.php | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 69f3fe7484c24..c314d474e6734 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1999,6 +1999,10 @@ function wp_delete_object_term_relationships( $object_id, $taxonomies ) { foreach ( (array) $taxonomies as $taxonomy ) { $term_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids' ) ); + if ( ! is_array( $term_ids ) ) { + // Skip return value in the case of an error or the 'wp_get_object_terms' filter returning an invalid value. + continue; + } $term_ids = array_map( 'intval', $term_ids ); wp_remove_object_terms( $object_id, $term_ids, $taxonomy ); } diff --git a/tests/phpunit/tests/term/wpDeleteObjectTermRelationships.php b/tests/phpunit/tests/term/wpDeleteObjectTermRelationships.php index e4ee55f46d104..bd97b4de3b0e7 100644 --- a/tests/phpunit/tests/term/wpDeleteObjectTermRelationships.php +++ b/tests/phpunit/tests/term/wpDeleteObjectTermRelationships.php @@ -53,4 +53,24 @@ public function test_array_of_taxonomies() { $this->assertSameSets( array( $t2 ), $terms ); } + + /** + * @ticket 64406 + */ + public function test_delete_when_error() { + $taxonomy_name = 'wptests_tax'; + register_taxonomy( $taxonomy_name, 'post' ); + $term_id = self::factory()->term->create( array( 'taxonomy' => $taxonomy_name ) ); + $object_id = 567; + wp_set_object_terms( $object_id, array( $term_id ), $taxonomy_name ); + + // Confirm the setup. + $terms = wp_get_object_terms( $object_id, array( $taxonomy_name ), array( 'fields' => 'ids' ) ); + $this->assertSame( array( $term_id ), $terms, 'Expected same object terms.' ); + + // Try wp_delete_object_term_relationships() when the taxonomy is invalid (no change expected). + wp_delete_object_term_relationships( $object_id, 'wptests_taxation' ); + $terms = wp_get_object_terms( $object_id, array( $taxonomy_name ), array( 'fields' => 'ids' ) ); + $this->assertSame( array( $term_id ), $terms, 'Expected the object terms to be unchanged.' ); + } } From ea60bd525f79bc5ca2086fa762639b1397051692 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 14 Dec 2025 08:10:25 +0000 Subject: [PATCH 05/30] REST API: Use valid host in unit tests for URL Details endpoint. This ensures that the `url` parameter is not marked as invalid due to `wp_http_validate_url()` failing because of a `gethostbyname()` failure. Follow-up to [51973]. Props westonruter, swissspidy. See #54358. Fixes #64412. git-svn-id: https://develop.svn.wordpress.org/trunk@61377 602fd350-edb4-49c9-b593-d223f7449a82 --- .../tests/rest-api/wpRestUrlDetailsController.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php b/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php index 57ecccd7bf42d..7187d1696c05a 100644 --- a/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php +++ b/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php @@ -43,11 +43,15 @@ class Tests_REST_WpRestUrlDetailsController extends WP_Test_REST_Controller_Test /** * URL placeholder. * + * Even though the request is being intercepted with a mocked response, it is not fully bypassing the network. The + * REST API endpoint is validating the `url` parameter with `wp_http_validate_url()` which includes a call to + * `gethostbyname()`. So the domain used in the placeholder URL must be valid to ensure it passes a validity check. + * * @since 5.9.0 * * @var string */ - const URL_PLACEHOLDER = 'https://placeholder-site.com'; + const URL_PLACEHOLDER = 'https://example.com'; /** * Array of request args. @@ -129,9 +133,9 @@ public function test_get_items() { $this->assertSame( array( 'title' => 'Example Website — - with encoded content.', - 'icon' => 'https://placeholder-site.com/favicon.ico?querystringaddedfortesting', + 'icon' => 'https://example.com/favicon.ico?querystringaddedfortesting', 'description' => 'Example description text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.', - 'image' => 'https://placeholder-site.com/images/home/screen-themes.png?3', + 'image' => 'https://example.com/images/home/screen-themes.png?3', ), $data ); @@ -444,7 +448,7 @@ static function ( $response, $url ) { $this->assertSame( 418, $data['status'], 'Response "status" is not 418' ); - $expected = 'Response for URL https://placeholder-site.com altered via rest_prepare_url_details filter'; + $expected = 'Response for URL https://example.com altered via rest_prepare_url_details filter'; $this->assertSame( $expected, $data['response'], 'Response "response" is not "' . $expected . '"' ); } From d5cbd3d7152ae4a8230123dbb1da2cfd45348e5b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 14 Dec 2025 22:28:52 +0000 Subject: [PATCH 06/30] Heartbeat: Handle race condition in `wp-auth-check` where `heartbeat-tick` may fire before `DOMContentLoaded`. Developed in https://github.com/WordPress/wordpress-develop/pull/10624 Follow-up to [23805], [50547]. Props westonruter, ArtZ91, siliconforks. See #23295. Fixes #64403. git-svn-id: https://develop.svn.wordpress.org/trunk@61379 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/lib/auth-check.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/js/_enqueues/lib/auth-check.js b/src/js/_enqueues/lib/auth-check.js index 44ff15a153410..ff64573639a25 100644 --- a/src/js/_enqueues/lib/auth-check.js +++ b/src/js/_enqueues/lib/auth-check.js @@ -159,12 +159,23 @@ setShowTimeout(); }); }).on( 'heartbeat-tick.wp-auth-check', function( e, data ) { - if ( 'wp-auth-check' in data ) { + if ( ! ( 'wp-auth-check' in data ) ) { + return; + } + + var showOrHide = function () { if ( ! data['wp-auth-check'] && wrap.hasClass( 'hidden' ) && ! tempHidden ) { show(); } else if ( data['wp-auth-check'] && ! wrap.hasClass( 'hidden' ) ) { hide(); } + }; + + // This is necessary due to a race condition where the heartbeat-tick event may fire before DOMContentLoaded. + if ( wrap ) { + showOrHide(); + } else { + $( showOrHide ); } }); From eda8d9d27b129ac48143af7d9c2df595dc172e44 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 14 Dec 2025 23:55:01 +0000 Subject: [PATCH 07/30] Docs: Update `wp_get_media_creation_timestamp()` DocBlock for consistency. Follow-up to [19687], [41746]. See #64224. git-svn-id: https://develop.svn.wordpress.org/trunk@61380 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/media.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 8436bde73b7fa..210539681ad56 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -3769,8 +3769,8 @@ function wp_read_audio_metadata( $file ) { * @link https://github.com/JamesHeinrich/getID3/blob/master/structure.txt * * @param array $metadata The metadata returned by getID3::analyze(). - * @return int|false A UNIX timestamp for the media's creation date if available - * or a boolean FALSE if a timestamp could not be determined. + * @return int|false A Unix timestamp for the media's creation date if available + * or a boolean false if the timestamp could not be determined. */ function wp_get_media_creation_timestamp( $metadata ) { $creation_date = false; From 0d12267678988ba90196b41fa00f8b5997f51088 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 20:31:28 +0200 Subject: [PATCH 08/30] Escape script modifiable text --- .../html-api/class-wp-html-tag-processor.php | 138 +++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 31c4bc8a10654..f3b294a90aad8 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3833,7 +3833,26 @@ public function set_modifiable_text( string $plaintext_content ): bool { false !== stripos( $plaintext_content, 'is_javascript_script_tag() ) { + $plaintext_content = preg_replace_callback( + '~<(/?)(s)(cript)~i', + static function ( $matches ) { + $escaped_s_char = 's' === $matches[2] + ? '\\u0073' + : '\\u0053'; + return "<{$matches[1]}{$escaped_s_char}{$matches[3]}"; + }, + $plaintext_content + ); + } else { + return false; + } } $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( @@ -3891,6 +3910,123 @@ static function ( $tag_match ) { return false; } + /** + * Indicates if the currently matched tag is a JavaScript script tag. + * + * @see https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element + * + * @since {WP_VERSION} + * + * @return boolean True if the script tag will be evaluated as JavaScript. + */ + public function is_javascript_script_tag(): bool { + if ( 'SCRIPT' !== $this->get_tag() || $this->get_namespace() !== 'html' ) { + return false; + } + + /* + * > If any of the following are true: + * > - el has a type attribute whose value is the empty string; + * > - el has no type attribute but it has a language attribute and that attribute's + * > value is the empty string; or + * > - el has neither a type attribute nor a language attribute, + * > then let the script block's type string for this script element be "text/javascript". + */ + $type_attr = $this->get_attribute( 'type' ); + $language_attr = $this->get_attribute( 'language' ); + + if ( true === $type_attr || '' === $type_attr ) { + return true; + } + if ( + null === $type_attr && ( + true === $language_attr || + '' === $language_attr || + null === $language_attr + ) + ) { + return true; + } + + /* + * > Otherwise, if el has a type attribute, then let the script block's type string be + * > the value of that attribute with leading and trailing ASCII whitespace stripped. + * > Otherwise, el has a non-empty language attribute; let the script block's type string + * > be the concatenation of "text/" and the value of el's language attribute. + */ + $type_string = $type_attr ? trim( $type_attr, " \t\f\r\n" ) : "text/{$language_attr}"; + + /* + * > If the script block's type string is a JavaScript MIME type essence match, then + * > set el's type to "classic". + * + * > A string is a JavaScript MIME type essence match if it is an ASCII case-insensitive + * > match for one of the JavaScript MIME type essence strings. + + * > A JavaScript MIME type is any MIME type whose essence is one of the following: + * > + * > - application/ecmascript + * > - application/javascript + * > - application/x-ecmascript + * > - application/x-javascript + * > - text/ecmascript + * > - text/javascript + * > - text/javascript1.0 + * > - text/javascript1.1 + * > - text/javascript1.2 + * > - text/javascript1.3 + * > - text/javascript1.4 + * > - text/javascript1.5 + * > - text/jscript + * > - text/livescript + * > - text/x-ecmascript + * > - text/x-javascript + * + * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type + * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type-essence-match + */ + switch ( strtolower( $type_string ) ) { + case 'application/ecmascript': + case 'application/javascript': + case 'application/x-ecmascript': + case 'application/x-javascript': + case 'text/ecmascript': + case 'text/javascript': + case 'text/javascript1.0': + case 'text/javascript1.1': + case 'text/javascript1.2': + case 'text/javascript1.3': + case 'text/javascript1.4': + case 'text/javascript1.5': + case 'text/jscript': + case 'text/livescript': + case 'text/x-ecmascript': + case 'text/x-javascript': + return true; + + /* + * > Otherwise, if the script block's type string is an ASCII case-insensitive match for + * > the string "module", then set el's type to "module". + * + * A module is evaluated as JavaScript + */ + case 'module': + return true; + } + + /* + * > - Otherwise, if the script block's type string is an ASCII case-insensitive match for + * > the string "importmap", then set el's type to "importmap". + * + * An importmap is JSON and not evaluated as JavaScript. This case is not handled here. + */ + + /* + * > Otherwise, return. (No script is executed, and el's type is left as null.) + */ + return false; + } + /** * Updates or creates a new attribute on the currently matched tag with the passed value. * From 3dfb7ff83eb1be7da43515a55bc818b80a2056a4 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 20:53:44 +0200 Subject: [PATCH 09/30] Stop escaping slashes, json_encode dangerously This helps demonstrate the effectiveness. --- src/wp-includes/block-editor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index 6f5720ec21c9d..d2beae38ece76 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -774,7 +774,7 @@ function block_editor_rest_api_preload( array $preload_paths, $block_editor_cont 'wp-api-fetch', sprintf( 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', - wp_json_encode( $preload_data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) + wp_json_encode( $preload_data, JSON_UNESCAPED_SLASHES ) ), 'after' ); From a95a0f699667c2562d8fd67eed91518052df8e0d Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 20:55:07 +0200 Subject: [PATCH 10/30] Use the tag processor to correctly extract script tag contents --- src/wp-includes/functions.wp-scripts.php | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index f86b456d5f69a..ea4986758683f 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -130,18 +130,24 @@ function wp_print_scripts( $handles = false ) { function wp_add_inline_script( $handle, $data, $position = 'after' ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); - if ( false !== stripos( $data, '' ) ) { - _doing_it_wrong( - __FUNCTION__, - sprintf( - /* translators: 1: #is', '$1', $data ) ); + if ( false !== stripos( $data, '', wp_sanitize_script_attributes( $attributes ) ); + $processor = new WP_HTML_Tag_Processor( $script_tag ); + if ( $processor->next_tag( 'SCRIPT' ) && $processor->set_modifiable_text( $data ) ) { + return $processor->get_updated_html() . "\n"; + } + return sprintf( "%s\n", wp_sanitize_script_attributes( $attributes ), $data ); } From d39ba6173d3aad75fe25818d6601b26c241e8036 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 12 Dec 2025 18:59:15 +0100 Subject: [PATCH 12/30] Improve closing script tag test --- tests/phpunit/tests/blocks/editor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 4241161388eb8..2a6b02f5a81db 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -760,7 +760,7 @@ public function test_ensure_preload_data_script_tag_closes() { array( 'methods' => 'GET', 'callback' => function () { - return 'Unclosed comment and a script open tag ' => array( '', 'Comments end in -->' ), - 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), - 'SCRIPT with ' => array( '', 'Just a ' ), - 'SCRIPT with ' => array( '', 'beforeafter' ), - 'SCRIPT with "', '' => array( '', 'Comments end in -->' ), + 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), + 'XML type SCRIPT' => array( '', 'Just a ' ), + 'Non-JavaSript SCRIPT' => array( '', 'beforeafter' ), + + // We can handle these now! + //'SCRIPT with ' => array( '', 'Just a ' ), + //'SCRIPT with ' => array( '', 'beforeafter' ), + //'SCRIPT with "', '' => array( '', 'Comments end in -->' ), - 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), - 'XML type SCRIPT' => array( '', 'Just a ' ), - 'Non-JavaSript SCRIPT' => array( '', 'beforeafter' ), - - // We can handle these now! - //'SCRIPT with ' => array( '', 'Just a ' ), - //'SCRIPT with ' => array( '', 'beforeafter' ), - //'SCRIPT with "', '' => array( '', 'Comments end in -->' ), + 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), + 'Non-JS SCRIPT with ', '