From cfd3ca4fdde8fbfc711f0c18e9ef3a6be3e9b8ba Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 22 Jan 2026 16:12:35 +0100 Subject: [PATCH 01/35] Set up for phpbench --- composer.json | 3 +- phpbench-bootstrap.php | 146 +++++++++++++++++++++++++++++++++++++++++ phpbench.json | 4 ++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 phpbench-bootstrap.php create mode 100644 phpbench.json diff --git a/composer.json b/composer.json index 2c5b20f7879a9..4ef5cc6dc0688 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "squizlabs/php_codesniffer": "3.13.5", "wp-coding-standards/wpcs": "~3.3.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", - "yoast/phpunit-polyfills": "^1.1.0" + "yoast/phpunit-polyfills": "^1.1.0", + "phpbench/phpbench": "^1.4" }, "config": { "allow-plugins": { diff --git a/phpbench-bootstrap.php b/phpbench-bootstrap.php new file mode 100644 index 0000000000000..8417cf8d15f65 --- /dev/null +++ b/phpbench-bootstrap.php @@ -0,0 +1,146 @@ + Date: Thu, 22 Jan 2026 20:37:59 +0100 Subject: [PATCH 02/35] phpbench setup --- phpbench-bootstrap.php | 115 ++++++++--------------------------------- phpbench.json | 3 +- 2 files changed, 23 insertions(+), 95 deletions(-) diff --git a/phpbench-bootstrap.php b/phpbench-bootstrap.php index 8417cf8d15f65..a5ac836db1736 100644 --- a/phpbench-bootstrap.php +++ b/phpbench-bootstrap.php @@ -1,105 +1,52 @@ Date: Tue, 31 Mar 2026 17:01:04 +0000 Subject: [PATCH 03/35] Build/Test Tools: Remove erroneous PHP tags for translations from QUnit HTML file. The markup had surely been copied from the PHP source file, as opposed to being copied from the rendered HTML, as it should have been. Developed in https://github.com/WordPress/wordpress-develop/pull/11403 Follow-up to r41773. Props westonruter, jonsurrell, desrosj, SergeyBiryukov. See #64225, #40104. git-svn-id: https://develop.svn.wordpress.org/trunk@62184 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/qunit/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/qunit/index.html b/tests/qunit/index.html index 6b4c4a1dd8811..9fd35f0c1ffc2 100644 --- a/tests/qunit/index.html +++ b/tests/qunit/index.html @@ -245,7 +245,7 @@

  • {{{ data.message || data.code }}} <# if ( data.dismissible ) { #> - + <# } #>
  • @@ -395,14 +395,14 @@

    From 7919efbf22dde7225fc60b7c768cc0754d9d2e05 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 31 Mar 2026 19:25:33 +0000 Subject: [PATCH 04/35] I18N: Provide gettext context to disambiguate translation strings for "Notes". "Notes" translation string is used in both the Notes features and in the Link Manager, and they can have different meaning in some Locales, like in German for example. This changeset helps disambuguating these different contexts. Props westonruter, dmsnell, johnbillion. Fixes #64980. git-svn-id: https://develop.svn.wordpress.org/trunk@62185 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/meta-boxes.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/meta-boxes.php b/src/wp-admin/includes/meta-boxes.php index a1859f45c7422..0884c110b65bd 100644 --- a/src/wp-admin/includes/meta-boxes.php +++ b/src/wp-admin/includes/meta-boxes.php @@ -1453,7 +1453,14 @@ function link_advanced_meta_box( $link ) { - + + + From a2479dae529924b5ea8c401f443b9938c9af5127 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 31 Mar 2026 20:17:34 +0000 Subject: [PATCH 05/35] I18N: Provide gettext context to disambiguate translation strings for "Bulk Edit". The "Bulk Edit" translation string is used for both verbs and nouns, and may have different translations in some Locales. This changeset helps disambuguating these different contexts. Follow-up to [61255]. Props audrasjb, shailu25. Fixes #64994. git-svn-id: https://develop.svn.wordpress.org/trunk@62186 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-posts-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index fc039a7573f19..c7d10fca217ef 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -437,7 +437,7 @@ protected function get_bulk_actions() { if ( $this->is_trash ) { $actions['untrash'] = __( 'Restore' ); } else { - $actions['edit'] = __( 'Bulk edit' ); + $actions['edit'] = _x( 'Bulk edit', 'verb' ); } } From 88734d4c323f459d558be7143a34f1cc08790356 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 31 Mar 2026 20:27:12 +0000 Subject: [PATCH 06/35] Administration: Prevent horizontal scrollbar in contextual help panel. In [62145], an `::after` CSS rule was added that caused an overflow, resulting in an unintended scrollbar always appearing on Windows OS for example. This changeset removes the related CSS rule which is unnecessary to fix the initial issue. Follow-up to [62145]. Props wildworks, SergeyBiryukov, sabernhardt, audrasjb, huzaifaalmesbah, mehrazmorshed, mukesh27. Fixes #64744. git-svn-id: https://develop.svn.wordpress.org/trunk@62187 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 211cf0022c1e0..c691383019f6d 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -2077,17 +2077,6 @@ p.auto-update-status { box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02), 0 1px 0 rgba(0, 0, 0, 0.02); } -.contextual-help-tabs .active::after { - content: ""; - position: absolute; - top: 0; - right: -1px; - width: 2px; - height: 100%; - background: inherit; - z-index: 2; -} - .contextual-help-tabs .active a { border-color: #c3c4c7; color: #2c3338; From abf9109166099011904710d1e8c63f444d0b862a Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 31 Mar 2026 22:03:29 +0000 Subject: [PATCH 07/35] I18N: Add context for Next/Previous strings in the jQuery UI datepicker. Follow-up to [37849]. Props timse201, anupkankale. Fixes #65005. git-svn-id: https://develop.svn.wordpress.org/trunk@62188 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/script-loader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index e164da51bc248..42d42b3f8781d 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2020,8 +2020,8 @@ function wp_localize_jquery_ui_datepicker() { 'currentText' => __( 'Today' ), 'monthNames' => array_values( $wp_locale->month ), 'monthNamesShort' => array_values( $wp_locale->month_abbrev ), - 'nextText' => __( 'Next' ), - 'prevText' => __( 'Previous' ), + 'nextText' => _x( 'Next', 'datepicker: navigate to next month' ), + 'prevText' => _x( 'Previous', 'datepicker: navigate to previous month' ), 'dayNames' => array_values( $wp_locale->weekday ), 'dayNamesShort' => array_values( $wp_locale->weekday_abbrev ), 'dayNamesMin' => array_values( $wp_locale->weekday_initial ), From 1893a30a778a4e190a06cd4a210bcc9194f95aeb Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 1 Apr 2026 02:33:56 +0000 Subject: [PATCH 08/35] Build/Test Tools: Copy vendor scripts earlier in the build. Relocates the `copy-vendor-scripts` to run during the the `build:js` portion of the build script. This ensures the JavaScript files are in place before the `uglify:all` task is run. Follow up to r61438 Props desrosj. Fixes #65006. See #64393. git-svn-id: https://develop.svn.wordpress.org/trunk@62189 602fd350-edb4-49c9-b593-d223f7449a82 --- Gruntfile.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 1c4280aff213b..5f9109fac3cb0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1837,6 +1837,7 @@ module.exports = function(grunt) { 'clean:js', 'build:webpack', 'copy:js', + 'copy-vendor-scripts', 'file_append', 'uglify:all', 'concat:tinymce', @@ -2133,7 +2134,6 @@ module.exports = function(grunt) { 'build:css', 'build:codemirror', 'build:gutenberg', - 'copy-vendor-scripts', 'build:certificates' ] ); } else { @@ -2145,7 +2145,6 @@ module.exports = function(grunt) { 'build:css', 'build:codemirror', 'build:gutenberg', - 'copy-vendor-scripts', 'replace:source-maps', 'verify:build' ] ); From b60f8bae9462280dcb694d375d89fcc87d1db7bc Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 1 Apr 2026 09:18:43 +0000 Subject: [PATCH 09/35] Admin Reskin: Change color picker height to match new design system. Update min-height from 30px to 32px for the color picker button and related elements to match new design system. Props audrasjb, hmbashar, huzaifaalmesbah, joedolson, juanmaguitar, mukesh27, noruzzaman, ozgursar, rahultank, rcorrales, sajib1223, tusharaddweb, vgnavada, wildworks. Fixes #64761. git-svn-id: https://develop.svn.wordpress.org/trunk@62191 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/color-picker.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/css/color-picker.css b/src/wp-admin/css/color-picker.css index 1e7525799e855..8264432dd39cc 100644 --- a/src/wp-admin/css/color-picker.css +++ b/src/wp-admin/css/color-picker.css @@ -10,7 +10,7 @@ /* Needs higher specificity to override `.wp-core-ui .button`. */ .wp-picker-container .wp-color-result.button { - min-height: 30px; + min-height: 32px; margin: 0 6px 6px 0; padding: 0 0 0 30px; font-size: 11px; @@ -22,7 +22,7 @@ border-left: 1px solid #c3c4c7; color: #50575e; display: block; - line-height: 2.54545455; /* 28px */ + line-height: 2.72727273; /* 30px */ padding: 0 6px; text-align: center; } @@ -76,8 +76,8 @@ .wp-customizer .wp-picker-input-wrap .button.wp-picker-clear { margin-left: 6px; padding: 0 8px; - line-height: 2.54545455; /* 28px */ - min-height: 30px; + line-height: 2.72727273; /* 30px */ + min-height: 32px; } .wp-picker-container .iris-square-slider .ui-slider-handle:focus { @@ -97,7 +97,7 @@ margin: 0; padding: 0 5px; vertical-align: top; - min-height: 30px; + min-height: 32px; } .wp-color-picker::-webkit-input-placeholder { From d368a44b94dbf718aa0fab7832dc819228301b11 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 1 Apr 2026 14:56:47 +0000 Subject: [PATCH 10/35] Connectors: Replace `plugin.slug` with `plugin.file` in connector registration. Use the plugin's main file path relative to the plugins directory (e.g. `akismet/akismet.php` or `hello.php`) instead of the WordPress.org slug to identify a connector's associated plugin. This lets `_wp_connectors_get_connector_script_module_data()` check plugin status with `file_exists()` and `is_plugin_active()` directly, removing the `get_plugins()` slug-to-file mapping that was previously needed. Props jorgefilipecosta, mukesh27, gziolo. Fixes #65002. git-svn-id: https://develop.svn.wordpress.org/trunk@62192 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-connector-registry.php | 9 +++-- src/wp-includes/connectors.php | 40 ++++++++----------- .../tests/connectors/wpConnectorRegistry.php | 4 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index 18a5f80c94dbd..d7643360efeeb 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -40,7 +40,7 @@ * env_var_name?: non-empty-string * }, * plugin?: array{ - * slug: non-empty-string + * file: non-empty-string * } * } */ @@ -109,7 +109,8 @@ final class WP_Connector_Registry { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $slug The WordPress.org plugin slug. + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'akismet/akismet.php' or 'hello.php'). * } * } * @return array|null The registered connector data on success, null on failure. @@ -242,8 +243,8 @@ public function register( string $id, array $args ): ?array { } } - if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) { - $connector['plugin'] = $args['plugin']; + if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) && ! empty( $args['plugin']['file'] ) ) { + $connector['plugin'] = array( 'file' => $args['plugin']['file'] ); } $this->registered_connectors[ $id ] = $connector; diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 06683ccaaa25c..68c8b4c1570d0 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -58,7 +58,8 @@ function wp_is_connector_registered( string $id ): bool { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $slug The WordPress.org plugin slug. + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'akismet/akismet.php' or 'hello.php'). * } * } * @phpstan-return ?array{ @@ -74,7 +75,7 @@ function wp_is_connector_registered( string $id ): bool { * env_var_name?: non-empty-string * }, * plugin?: array{ - * slug: non-empty-string + * file: non-empty-string * } * } */ @@ -118,7 +119,8 @@ function wp_get_connector( string $id ): ?array { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $slug The WordPress.org plugin slug. + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'akismet/akismet.php' or 'hello.php'). * } * } * } @@ -135,7 +137,7 @@ function wp_get_connector( string $id ): ?array { * env_var_name?: non-empty-string * }, * plugin?: array{ - * slug: non-empty-string + * file: non-empty-string * } * }> */ @@ -256,7 +258,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re 'description' => __( 'Text generation with Claude.' ), 'type' => 'ai_provider', 'plugin' => array( - 'slug' => 'ai-provider-for-anthropic', + 'file' => 'ai-provider-for-anthropic/plugin.php', ), 'authentication' => array( 'method' => 'api_key', @@ -268,7 +270,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re 'description' => __( 'Text and image generation with Gemini and Imagen.' ), 'type' => 'ai_provider', 'plugin' => array( - 'slug' => 'ai-provider-for-google', + 'file' => 'ai-provider-for-google/plugin.php', ), 'authentication' => array( 'method' => 'api_key', @@ -280,7 +282,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re 'description' => __( 'Text and image generation with GPT and Dall-E.' ), 'type' => 'ai_provider', 'plugin' => array( - 'slug' => 'ai-provider-for-openai', + 'file' => 'ai-provider-for-openai/plugin.php', ), 'authentication' => array( 'method' => 'api_key', @@ -636,15 +638,9 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { function _wp_connectors_get_connector_script_module_data( array $data ): array { $registry = AiClient::defaultRegistry(); - // Build a slug-to-file map for plugin installation status. - if ( ! function_exists( 'get_plugins' ) ) { + if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - $plugin_files_by_slug = array(); - foreach ( array_keys( get_plugins() ) as $plugin_file ) { - $slug = str_contains( $plugin_file, '/' ) ? dirname( $plugin_file ) : str_replace( '.php', '', $plugin_file ); - $plugin_files_by_slug[ $slug ] = $plugin_file; - } $connectors = array(); foreach ( wp_get_connectors() as $connector_id => $connector_data ) { @@ -676,18 +672,14 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { 'authentication' => $auth_out, ); - if ( ! empty( $connector_data['plugin']['slug'] ) ) { - $plugin_slug = $connector_data['plugin']['slug']; - $plugin_file = $plugin_files_by_slug[ $plugin_slug ] ?? null; - - $is_installed = null !== $plugin_file; - $is_activated = $is_installed && is_plugin_active( $plugin_file ); + if ( ! empty( $connector_data['plugin']['file'] ) ) { + $file = $connector_data['plugin']['file']; + $is_installed = file_exists( wp_normalize_path( WP_PLUGIN_DIR . '/' . $file ) ); + $is_activated = $is_installed && is_plugin_active( $file ); $connector_out['plugin'] = array( - 'slug' => $plugin_slug, - 'pluginFile' => $is_installed - ? ( str_ends_with( $plugin_file, '.php' ) ? substr( $plugin_file, 0, -4 ) : $plugin_file ) - : null, + 'file' => $file, + 'isInstalled' => $is_installed, 'isActivated' => $is_activated, ); } diff --git a/tests/phpunit/tests/connectors/wpConnectorRegistry.php b/tests/phpunit/tests/connectors/wpConnectorRegistry.php index cab030d930dcd..d1a46dc0981fe 100644 --- a/tests/phpunit/tests/connectors/wpConnectorRegistry.php +++ b/tests/phpunit/tests/connectors/wpConnectorRegistry.php @@ -294,12 +294,12 @@ public function test_register_omits_logo_url_when_empty() { */ public function test_register_includes_plugin_data() { $args = self::$default_args; - $args['plugin'] = array( 'slug' => 'my-plugin' ); + $args['plugin'] = array( 'file' => 'my-plugin/my-plugin.php' ); $result = $this->registry->register( 'with-plugin', $args ); $this->assertArrayHasKey( 'plugin', $result ); - $this->assertSame( array( 'slug' => 'my-plugin' ), $result['plugin'] ); + $this->assertSame( array( 'file' => 'my-plugin/my-plugin.php' ), $result['plugin'] ); } /** From c5627a124998b20ee492e922edbf4d75d5fd927c Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 1 Apr 2026 15:53:17 +0000 Subject: [PATCH 11/35] Fix: Register Akismet Anti-Spam as a connector. Akismet comes with core but the connectors screen was not showing akismet even if akismet was on the file system. This commit fixes the issue. Props jorgefilipecosta, bluefuton, gziolo. Fixes #65012. git-svn-id: https://develop.svn.wordpress.org/trunk@62193 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/connectors.php | 19 ++++++++++++++++++ .../wpConnectorsGetConnectorSettings.php | 20 ++++++++++++------- .../rest-api/rest-settings-controller.php | 1 + tests/qunit/fixtures/wp-api-generated.js | 7 +++++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 68c8b4c1570d0..a11faeb637623 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -210,6 +210,25 @@ function _wp_connectors_init(): void { _wp_connectors_register_default_ai_providers( $registry ); } + // Non-AI default connectors. + $registry->register( + 'akismet', + array( + 'name' => __( 'Akismet Anti-spam' ), + 'description' => __( 'Protect your site from spam.' ), + 'type' => 'spam_filtering', + 'plugin' => array( + 'file' => 'akismet/akismet.php', + ), + 'authentication' => array( + 'method' => 'api_key', + 'credentials_url' => 'https://akismet.com/get/', + 'setting_name' => 'wordpress_api_key', + 'constant_name' => 'WPCOM_API_KEY', + ), + ) + ); + /** * Fires when the connector registry is ready for plugins to register connectors. * diff --git a/tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php b/tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php index cedac90111101..9d6c4b8486d9c 100644 --- a/tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php +++ b/tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php @@ -37,8 +37,9 @@ public function test_returns_expected_connector_keys(): void { $this->assertArrayHasKey( 'google', $connectors ); $this->assertArrayHasKey( 'openai', $connectors ); $this->assertArrayHasKey( 'anthropic', $connectors ); + $this->assertArrayHasKey( 'akismet', $connectors ); $this->assertArrayHasKey( 'mock-connectors-test', $connectors ); - $this->assertCount( 4, $connectors ); + $this->assertCount( 5, $connectors ); } /** @@ -56,7 +57,7 @@ public function test_each_connector_has_required_fields(): void { $this->assertArrayHasKey( 'description', $connector_data, "Connector '{$connector_id}' is missing 'description'." ); $this->assertIsString( $connector_data['description'], "Connector '{$connector_id}' description should be a string." ); $this->assertArrayHasKey( 'type', $connector_data, "Connector '{$connector_id}' is missing 'type'." ); - $this->assertContains( $connector_data['type'], array( 'ai_provider' ), "Connector '{$connector_id}' has unexpected type '{$connector_data['type']}'." ); + $this->assertContains( $connector_data['type'], array( 'ai_provider', 'spam_filtering' ), "Connector '{$connector_id}' has unexpected type '{$connector_data['type']}'." ); $this->assertArrayHasKey( 'authentication', $connector_data, "Connector '{$connector_id}' is missing 'authentication'." ); $this->assertIsArray( $connector_data['authentication'], "Connector '{$connector_id}' authentication should be an array." ); $this->assertArrayHasKey( 'method', $connector_data['authentication'], "Connector '{$connector_id}' authentication is missing 'method'." ); @@ -79,11 +80,16 @@ public function test_api_key_connectors_have_setting_name_and_credentials_url(): ++$api_key_count; $this->assertArrayHasKey( 'setting_name', $connector_data['authentication'], "Connector '{$connector_id}' authentication is missing 'setting_name'." ); - $this->assertSame( - 'connectors_ai_' . str_replace( '-', '_', $connector_id ) . '_api_key', - $connector_data['authentication']['setting_name'] ?? null, - "Connector '{$connector_id}' setting_name does not match expected format." - ); + + // AI providers use the connectors_ai_{id}_api_key convention. + // Non-AI connectors may use custom setting names. + if ( 'ai_provider' === $connector_data['type'] ) { + $this->assertSame( + 'connectors_ai_' . str_replace( '-', '_', $connector_id ) . '_api_key', + $connector_data['authentication']['setting_name'] ?? null, + "Connector '{$connector_id}' setting_name does not match expected format." + ); + } } $this->assertGreaterThan( 0, $api_key_count, 'At least one connector should use api_key authentication.' ); diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index b83cef41d2cf3..7f2ea9eba71f7 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -119,6 +119,7 @@ public function test_get_items() { 'default_ping_status', 'default_comment_status', 'site_icon', // Registered in wp-includes/blocks/site-logo.php + 'wordpress_api_key', // Registered by Akismet connector. 'wp_collaboration_enabled', ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 003dc397ae305..c3ca057691308 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11011,6 +11011,12 @@ mockedApiResponse.Schema = { "PATCH" ], "args": { + "wordpress_api_key": { + "title": "Akismet Anti-spam API Key", + "description": "API key for the Akismet Anti-spam connector.", + "type": "string", + "required": false + }, "title": { "title": "Title", "description": "Site title.", @@ -14544,6 +14550,7 @@ mockedApiResponse.CommentModel = { }; mockedApiResponse.settings = { + "wordpress_api_key": "", "title": "Test Blog", "description": "", "url": "http://example.org", From b5da8deadc4bd937f358163d1a6a8fe5451a95ca Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 1 Apr 2026 22:02:10 +0000 Subject: [PATCH 12/35] =?UTF-8?q?Admin=20Reskin:=20Correct=20=E2=80=9DCopi?= =?UTF-8?q?ed!=E2=80=9D=20text=20alignment=20on=20Privacy=20Policy=20Guide?= =?UTF-8?q?=20screen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to [61645]. Props mukesh27, wildworks, audrasjb, shailu25, anupkankale, kapilpaul, SergeyBiryukov. Fixes #65009. git-svn-id: https://develop.svn.wordpress.org/trunk@62196 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/edit.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index f2ff6a485767a..b98dd889c59fe 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -994,15 +994,16 @@ form#tags-filter { } .privacy-settings-accordion-actions { - text-align: right; - display: block; + justify-content: right; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 1em; } .privacy-settings-accordion-actions .success { display: none; color: #007017; - padding-right: 1em; - padding-top: 6px; } .privacy-settings-accordion-actions .success.visible { From 2183f2394182a6074ac0c85237344ab706897bb2 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 2 Apr 2026 01:23:45 +0000 Subject: [PATCH 13/35] REST API: Harden Real Time Collaboration endpoint. Adds additional validation and permission checks the the Real Time Collaboration endpoint to ensure only input in the expected format is supported. Props czarate, westonruter, joefusco. Fixes #64890. git-svn-id: https://develop.svn.wordpress.org/trunk@62198 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-http-polling-sync-server.php | 102 +++++- .../tests/rest-api/rest-sync-server.php | 293 +++++++++++++++++- 2 files changed, 378 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index 88554a48c7d54..a90821ab78d3e 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -37,6 +37,30 @@ class WP_HTTP_Polling_Sync_Server { */ const COMPACTION_THRESHOLD = 50; + /** + * Maximum total size (in bytes) of the request body. + * + * @since 7.0.0 + * @var int + */ + const MAX_BODY_SIZE = 16 * MB_IN_BYTES; + + /** + * Maximum number of rooms allowed per request. + * + * @since 7.0.0 + * @var int + */ + const MAX_ROOMS_PER_REQUEST = 50; + + /** + * Maximum length of a single update data string. + * + * @since 7.0.0 + * @var int + */ + const MAX_UPDATE_DATA_SIZE = MB_IN_BYTES; + /** * Sync update type: compaction. * @@ -96,8 +120,9 @@ public function register_routes(): void { $typed_update_args = array( 'properties' => array( 'data' => array( - 'type' => 'string', - 'required' => true, + 'type' => 'string', + 'required' => true, + 'maxLength' => self::MAX_UPDATE_DATA_SIZE, ), 'type' => array( 'type' => 'string', @@ -149,12 +174,14 @@ public function register_routes(): void { 'methods' => array( WP_REST_Server::CREATABLE ), 'callback' => array( $this, 'handle_request' ), 'permission_callback' => array( $this, 'check_permissions' ), + 'validate_callback' => array( $this, 'validate_request' ), 'args' => array( 'rooms' => array( 'items' => array( 'properties' => $room_args, 'type' => 'object', ), + 'maxItems' => self::MAX_ROOMS_PER_REQUEST, 'required' => true, 'type' => 'array', ), @@ -223,6 +250,30 @@ public function check_permissions( WP_REST_Request $request ) { return true; } + /** + * Validates that the request body does not exceed the maximum allowed size. + * + * Runs as the route-level validate_callback, after per-arg schema + * validation has already passed. + * + * @since 7.0.0 + * + * @param WP_REST_Request $request The REST request. + * @return true|WP_Error True if valid, WP_Error if the body is too large. + */ + public function validate_request( WP_REST_Request $request ) { + $body = $request->get_body(); + if ( is_string( $body ) && strlen( $body ) > self::MAX_BODY_SIZE ) { + return new WP_Error( + 'rest_sync_body_too_large', + __( 'Request body is too large.' ), + array( 'status' => 413 ) + ); + } + + return true; + } + /** * Handles request: stores sync updates and awareness data, and returns * updates the client is missing. @@ -278,24 +329,47 @@ public function handle_request( WP_REST_Request $request ) { * * @param string $entity_kind The entity kind, e.g. 'postType', 'taxonomy', 'root'. * @param string $entity_name The entity name, e.g. 'post', 'category', 'site'. - * @param string|null $object_id The object ID / entity key for single entities, null for collections. + * @param string|null $object_id The numeric object ID / entity key for single entities, null for collections. * @return bool True if user has permission, otherwise false. */ private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool { - // Handle single post type entities with a defined object ID. - if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) { - return current_user_can( 'edit_post', (int) $object_id ); + if ( is_string( $object_id ) ) { + if ( ! ctype_digit( $object_id ) ) { + return false; + } + $object_id = (int) $object_id; } - - // Handle single taxonomy term entities with a defined object ID. - if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) { - $taxonomy = get_taxonomy( $entity_name ); - return isset( $taxonomy->cap->assign_terms ) && current_user_can( $taxonomy->cap->assign_terms ); + if ( null !== $object_id && $object_id <= 0 ) { + // Object ID must be numeric if provided. + return false; } - // Handle single comment entities with a defined object ID. - if ( 'root' === $entity_kind && 'comment' === $entity_name && is_numeric( $object_id ) ) { - return current_user_can( 'edit_comment', (int) $object_id ); + // Validate permissions for the provided object ID. + if ( is_int( $object_id ) ) { + // Handle single post type entities with a defined object ID. + if ( 'postType' === $entity_kind ) { + if ( get_post_type( $object_id ) !== $entity_name ) { + // Post is not of the specified post type. + return false; + } + return current_user_can( 'edit_post', $object_id ); + } + + // Handle single taxonomy term entities with a defined object ID. + if ( 'taxonomy' === $entity_kind ) { + $term_exists = term_exists( $object_id, $entity_name ); + if ( ! is_array( $term_exists ) || ! isset( $term_exists['term_id'] ) ) { + // Either term doesn't exist OR term is not in specified taxonomy. + return false; + } + + return current_user_can( 'edit_term', $object_id ); + } + + // Handle single comment entities with a defined object ID. + if ( 'root' === $entity_kind && 'comment' === $entity_name ) { + return current_user_can( 'edit_comment', $object_id ); + } } // All the remaining checks are for collections. If an object ID is provided, diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index 7a04226ced8c9..7ded16bd3b033 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -9,14 +9,20 @@ */ class WP_Test_REST_Sync_Server extends WP_Test_REST_Controller_Testcase { - protected static $editor_id; - protected static $subscriber_id; - protected static $post_id; + protected static int $editor_id; + protected static int $subscriber_id; + protected static int $post_id; + protected static int $category_id; + protected static int $tag_id; + protected static int $comment_id; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); self::$post_id = $factory->post->create( array( 'post_author' => self::$editor_id ) ); + self::$category_id = $factory->category->create(); + self::$tag_id = $factory->tag->create(); + self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) ); // Enable option in setUpBeforeClass to ensure REST routes are registered. update_option( 'wp_collaboration_enabled', 1 ); @@ -27,6 +33,9 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$subscriber_id ); delete_option( 'wp_collaboration_enabled' ); wp_delete_post( self::$post_id, true ); + wp_delete_term( self::$category_id, 'category' ); + wp_delete_term( self::$tag_id, 'post_tag' ); + wp_delete_comment( self::$comment_id, true ); } public function set_up() { @@ -277,6 +286,107 @@ public function test_sync_permission_checked_per_room() { $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); } + /** + * @ticket 64890 + */ + public function test_sync_malformed_object_id_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:1abc' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_zero_object_id_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:0' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_post_type_mismatch_rejected(): void { + wp_set_current_user( self::$editor_id ); + + // The test post is of type 'post', not 'page'. + $response = $this->dispatch_sync( array( $this->build_room( 'postType/page:' . self::$post_id ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_taxonomy_term_allowed(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$category_id ) ) ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_taxonomy_term_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:999999' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_taxonomy_term_wrong_taxonomy_rejected(): void { + wp_set_current_user( self::$editor_id ); + + // The tag term exists in 'post_tag', not 'category'. + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$tag_id ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_comment_allowed(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:' . self::$comment_id ) ) ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_comment_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:999999' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_post_type_collection_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/nonexistent_type' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + /* * Validation tests. */ @@ -293,6 +403,183 @@ public function test_sync_invalid_room_format_rejected() { $this->assertSame( 400, $response->get_status() ); } + /** + * Verifies that schema type validation rejects a non-string value for the + * update 'data' field, confirming that per-arg schema validation still runs + * with a route-level validate_callback registered. + * + * @ticket 64890 + */ + public function test_sync_rejects_non_string_update_data(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => 12345, + 'type' => 'update', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that schema enum validation rejects an invalid update type, + * confirming that per-arg schema validation still runs with a route-level + * validate_callback registered. + * + * @ticket 64890 + */ + public function test_sync_rejects_invalid_update_type_enum(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => 'dGVzdA==', + 'type' => 'invalid_type', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that schema required-field validation rejects a room missing + * the 'client_id' field, confirming that per-arg schema validation still + * runs with a route-level validate_callback registered. + * + * @ticket 64890 + */ + public function test_sync_rejects_missing_required_room_field(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + // 'client_id' deliberately omitted. + 'room' => $this->get_post_room(), + 'updates' => array(), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the maxItems constraint rejects a request with more rooms + * than MAX_ROOMS_PER_REQUEST. + * + * @ticket 64890 + */ + public function test_sync_rejects_rooms_exceeding_max_items(): void { + wp_set_current_user( self::$editor_id ); + + $rooms = array(); + for ( $i = 0; $i < WP_HTTP_Polling_Sync_Server::MAX_ROOMS_PER_REQUEST + 1; $i++ ) { + $rooms[] = $this->build_room( 'root/site', $i + 1 ); + } + + $response = $this->dispatch_sync( $rooms ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the maxLength constraint rejects update data exceeding + * MAX_UPDATE_DATA_SIZE. + * + * @ticket 64890 + */ + public function test_sync_rejects_update_data_exceeding_max_length(): void { + wp_set_current_user( self::$editor_id ); + + $oversized_data = str_repeat( 'a', WP_HTTP_Polling_Sync_Server::MAX_UPDATE_DATA_SIZE + 1 ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => $oversized_data, + 'type' => 'update', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the route-level validate_callback rejects a request body + * exceeding MAX_BODY_SIZE. + * + * @ticket 64890 + */ + public function test_sync_rejects_oversized_request_body(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + + // Set valid parsed params so per-arg schema validation passes first. + $request->set_body_params( + array( + 'rooms' => array( + $this->build_room( $this->get_post_room() ), + ), + ) + ); + + // Set an oversized raw body to trigger the route-level validate_callback. + $request->set_body( str_repeat( 'x', WP_HTTP_Polling_Sync_Server::MAX_BODY_SIZE + 1 ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_sync_body_too_large', $response, 413 ); + } + /* * Response format tests. */ From d0c6277a5fa116af994bf6438a82834c9c2c2199 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 2 Apr 2026 23:09:08 +0000 Subject: [PATCH 14/35] Media: Update upload file overlay colors. Update the colors used for the file upload overlay mask to use the new admin theme colors. Props opurockey, huzaifaalmesbah, wildworks, audrasjb, manhar, joedolson. Fixes #65001. git-svn-id: https://develop.svn.wordpress.org/trunk@62199 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/css/media-views.css | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css index 1b3c6edd7678f..f78a946c260f7 100644 --- a/src/wp-includes/css/media-views.css +++ b/src/wp-includes/css/media-views.css @@ -56,7 +56,7 @@ .media-frame a:focus { border-radius: 2px; box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); - color: #043959; + color: var(--wp-admin-theme-color-darker-20, #183ad6); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -244,13 +244,13 @@ .media-modal-close:hover, .media-modal-close:active { - color: #135e96; + color: var(--wp-admin-theme-color, #3858e9); } .media-modal-close:focus { - color: #135e96; - border-color: #4f94d4; - box-shadow: 0 0 3px rgba(34, 113, 177, 0.8); + color: var(--wp-admin-theme-color, #3858e9); + border-color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 3px rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.8); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -673,7 +673,7 @@ font-size: 14px; line-height: 1.28571428; background: transparent; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); text-align: left; text-decoration: none; cursor: pointer; @@ -684,7 +684,7 @@ } .media-menu .media-menu-item:active { - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); outline: none; } @@ -696,7 +696,7 @@ .media-menu .media-menu-item:focus { box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); - color: #043959; + color: var(--wp-admin-theme-color-darker-20, #183ad6); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -739,7 +739,7 @@ .media-router .media-menu-item:hover, .media-router .media-menu-item:active { - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); } .media-router .active, @@ -749,7 +749,7 @@ .media-router .media-menu-item:focus { box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); - color: #043959; + color: var(--wp-admin-theme-color-darker-20, #183ad6); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; z-index: 1; @@ -1321,8 +1321,8 @@ } .uploader-inline .close:focus { - outline: 1px solid #4f94d4; - box-shadow: 0 0 3px rgba(34, 113, 177, 0.8); + outline: 1px solid var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 3px rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.8); } .attachments-browser.hide-sidebar .attachments, @@ -1409,7 +1409,7 @@ height: 10px; min-width: 20px; width: 0; - background: #2271b1; + background: var(--wp-admin-theme-color, #3858e9); border-radius: 10px; transition: width 300ms; } @@ -1527,7 +1527,7 @@ .uploader-window, .wp-editor-wrap .uploader-editor.droppable { - background: rgba(10, 75, 120, 0.9); + background-color: rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.9); } .uploader-window-content, @@ -1688,13 +1688,13 @@ margin: 1px 8px 1px -8px; line-height: 1.4; border-right: 1px solid #dcdcde; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); text-decoration: none; } .media-selection .button-link:hover, .media-selection .button-link:focus { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20, #183ad6); } .media-selection .button-link:last-child { @@ -1752,7 +1752,7 @@ .wp-core-ui .media-selection .attachment.details:focus { box-shadow: 0 0 0 1px #fff, - 0 0 2px 3px #4f94d4; + 0 0 2px 3px var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -1764,7 +1764,7 @@ .wp-core-ui .media-selection .attachment.details { box-shadow: 0 0 0 1px #fff, - 0 0 0 3px #2271b1; + 0 0 0 3px var(--wp-admin-theme-color, #3858e9); } .media-selection:after { @@ -2044,7 +2044,7 @@ margin: 0; padding: 0; background: transparent; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); font-size: 20px; line-height: 1; cursor: pointer; @@ -2053,9 +2053,9 @@ } .wp-core-ui.media-modal .image-editor .imgedit-help-toggle:focus { - color: #2271b1; - border-color: #2271b1; - box-shadow: 0 0 0 1px #2271b1; + color: var(--wp-admin-theme-color, #3858e9); + border-color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } From d508d24dd2722ad20c837016d66901f2794f0726 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 2 Apr 2026 23:33:50 +0000 Subject: [PATCH 15/35] Admin: Limit scope of admin notice link design. The design changes to admin notices links in the admin refresh were applied broadly to `.notice, .error, and .updated` classes, but these classes are sometimes used outside the context of an admin notice. Change selectors from `.notice a, .error a, .updated a` to `div.notice a, div.error a, div.updated a`. Props opurockey, audrasjb, vgnavada, gaisma22, shailu25, rbcorrales, joedolson. Fixes #64976. git-svn-id: https://develop.svn.wordpress.org/trunk@62200 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index c691383019f6d..28b881d363c7e 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1473,22 +1473,22 @@ div.error p, color: #1e1e1e; } -.notice a, -.error a, -.updated a { +div.notice a, +div.error a, +div.updated a { color: var(--wp-admin-theme-color-darker-10); text-decoration: underline; } -.notice a:hover, -.error a:hover, -.updated a:hover { +div.notice a:hover, +div.error a:hover, +div.updated a:hover { color: var(--wp-admin-theme-color-darker-20); } -.notice a:focus, -.error a:focus, -.updated a:focus { +div.notice a:focus, +div.error a:focus, +div.updated a:focus { box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); outline: 2px solid transparent; border-radius: 2px; From 85108188d02f77712e4f1b88fb08e3e6e87c0216 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 2 Apr 2026 23:48:08 +0000 Subject: [PATCH 16/35] Code Quality: Remove unused variable in `WP_Block_Patterns_Registry`. Follow-up to [56805], [59101]. Props Soean, mukesh27. See #64898. git-svn-id: https://develop.svn.wordpress.org/trunk@62201 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-block-patterns-registry.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-block-patterns-registry.php b/src/wp-includes/class-wp-block-patterns-registry.php index c9bcd63549ab4..782ee9030c19e 100644 --- a/src/wp-includes/class-wp-block-patterns-registry.php +++ b/src/wp-includes/class-wp-block-patterns-registry.php @@ -227,10 +227,9 @@ public function get_registered( $pattern_name ) { * and per style. */ public function get_all_registered( $outside_init_only = false ) { - $patterns = $outside_init_only - ? $this->registered_patterns_outside_init - : $this->registered_patterns; - $hooked_blocks = get_hooked_blocks(); + $patterns = $outside_init_only + ? $this->registered_patterns_outside_init + : $this->registered_patterns; foreach ( $patterns as $index => $pattern ) { $content = $this->get_content( $pattern['name'], $outside_init_only ); From 54593bce56136640b5616dd2ce2ef388bba11975 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 3 Apr 2026 21:58:33 +0000 Subject: [PATCH 17/35] Tests: Move data providers and helpers in `Tests_REST_Server` for consistency. This ensures that data providers or helper functions used by a single test are located next to the test, for consistency with the rest of the test suite. Follow-up to [37905], [37943], [45809], [47239], [47260], [47351], [48947], [49252], [49257], [51960], [53110], [56096], [59032]. See #64225. git-svn-id: https://develop.svn.wordpress.org/trunk@62205 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/rest-api/rest-server.php | 148 +++++++++---------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 440effe4fe6f7..57b7bbb38abcd 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -151,6 +151,21 @@ public function test_envelope_param( $_embed ) { $this->assertSame( $headers, $enveloped['headers'] ); } + /** + * Data provider. + * + * @return array + */ + public function data_envelope_params() { + return array( + array( '1' ), + array( 'true' ), + array( false ), + array( 'alternate' ), + array( array( 'alternate' ) ), + ); + } + public function test_default_param() { register_rest_route( @@ -1721,6 +1736,32 @@ public function test_rest_send_refreshed_nonce_invalid_nonce() { $this->assertArrayNotHasKey( 'X-WP-Nonce', $headers ); } + /** + * Helper to setup a users and auth cookie global for the + * rest_send_refreshed_nonce related tests. + */ + protected function helper_setup_user_for_rest_send_refreshed_nonce_tests() { + $author = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $author ); + + global $wp_rest_auth_cookie; + + $wp_rest_auth_cookie = true; + } + + /** + * Helper to make the request and get the headers for the + * rest_send_refreshed_nonce related tests. + * + * @return array + */ + protected function helper_make_request_and_return_headers_for_rest_send_refreshed_nonce_tests() { + $request = new WP_REST_Request( 'GET', '/', array() ); + $result = rest_get_server()->serve_request( '/' ); + + return rest_get_server()->sent_headers; + } + /** * Refreshed nonce should be present in header when a valid nonce is * passed for logged in/anonymous user and not present when nonce is not @@ -1751,6 +1792,23 @@ public function test_rest_send_refreshed_nonce( $has_logged_in_user, $has_nonce } } + /** + * @return array { + * @type array { + * @type bool $has_logged_in_user Are we registering a user for the test. + * @type bool $has_nonce Is the nonce passed. + * } + * } + */ + public function data_rest_send_refreshed_nonce() { + return array( + array( true, true ), + array( true, false ), + array( false, true ), + array( false, false ), + ); + } + /** * Make sure that a sanitization that transforms the argument type will not * cause the validation to fail. @@ -1790,6 +1848,22 @@ public function test_rest_validate_before_sanitization() { $this->assertSame( 200, $response->get_status() ); } + public function _validate_as_integer_123( $value, $request, $key ) { + if ( ! is_int( $value ) ) { + return new WP_Error( 'some-error', 'This is not valid!' ); + } + + return true; + } + + public function _validate_as_string_foo( $value, $request, $key ) { + if ( ! is_string( $value ) ) { + return new WP_Error( 'some-error', 'This is not valid!' ); + } + + return true; + } + /** * @ticket 43691 */ @@ -2637,78 +2711,4 @@ public function test_prefers_developer_defined_target_hints() { $this->assertArrayHasKey( 'allow', $link['targetHints'] ); $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); } - - public function _validate_as_integer_123( $value, $request, $key ) { - if ( ! is_int( $value ) ) { - return new WP_Error( 'some-error', 'This is not valid!' ); - } - - return true; - } - - public function _validate_as_string_foo( $value, $request, $key ) { - if ( ! is_string( $value ) ) { - return new WP_Error( 'some-error', 'This is not valid!' ); - } - - return true; - } - - /** - * @return array { - * @type array { - * @type bool $has_logged_in_user Are we registering a user for the test. - * @type bool $has_nonce Is the nonce passed. - * } - * } - */ - public function data_rest_send_refreshed_nonce() { - return array( - array( true, true ), - array( true, false ), - array( false, true ), - array( false, false ), - ); - } - - /** - * Helper to setup a users and auth cookie global for the - * rest_send_refreshed_nonce related tests. - */ - protected function helper_setup_user_for_rest_send_refreshed_nonce_tests() { - $author = self::factory()->user->create( array( 'role' => 'author' ) ); - wp_set_current_user( $author ); - - global $wp_rest_auth_cookie; - - $wp_rest_auth_cookie = true; - } - - /** - * Helper to make the request and get the headers for the - * rest_send_refreshed_nonce related tests. - * - * @return array - */ - protected function helper_make_request_and_return_headers_for_rest_send_refreshed_nonce_tests() { - $request = new WP_REST_Request( 'GET', '/', array() ); - $result = rest_get_server()->serve_request( '/' ); - - return rest_get_server()->sent_headers; - } - - /** - * Data provider. - * - * @return array - */ - public function data_envelope_params() { - return array( - array( '1' ), - array( 'true' ), - array( false ), - array( 'alternate' ), - array( array( 'alternate' ) ), - ); - } } From e2d6d2b3174e22a6d37e8e8bf34565fe847211e9 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Sat, 4 Apr 2026 06:37:50 +0000 Subject: [PATCH 18/35] Administration: Improve dashboard widgets border styles. This changeset fixes a CSS glitch on dashboard widgets bottom border when they are collapsed. Follow-up to [61646]. Props pratik-jain, audrasjb, ankitkumarshah. Fixes #65017. See #64549. git-svn-id: https://develop.svn.wordpress.org/trunk@62206 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 28b881d363c7e..b317af45e023e 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -2281,7 +2281,7 @@ html.wp-toolbar { line-height: 1; } -.postbox.closed { +.postbox.closed .postbox-header { border-bottom: 0; } From b10d2f90d7ae85b78ab1e0bbf38ed56209abb58b Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 4 Apr 2026 23:17:45 +0000 Subject: [PATCH 19/35] Tests: Adjust Unicode tests for consistency. Includes: * Adding missing `@covers` tags. * Correcting test class names as per the naming conventions. * Moving `wp_check_invalid_utf8()` tests to their own file, separate from `wp_scrub_utf8()`. Follow-up to [60630], [60793], [61000]. See #64225. git-svn-id: https://develop.svn.wordpress.org/trunk@62207 602fd350-edb4-49c9-b593-d223f7449a82 --- .../tests/unicode/wpCheckInvalidUtf8.php | 110 ++++++++++++++++++ .../tests/unicode/wpHasNoncharacters.php | 4 +- tests/phpunit/tests/unicode/wpIsValidUtf8.php | 5 +- tests/phpunit/tests/unicode/wpScrubUtf8.php | 45 +------ 4 files changed, 122 insertions(+), 42 deletions(-) create mode 100644 tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php diff --git a/tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php b/tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php new file mode 100644 index 0000000000000..f477683eafd06 --- /dev/null +++ b/tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php @@ -0,0 +1,110 @@ +assertSame( + $bytes, + wp_check_invalid_utf8( $bytes ), + 'Should have returned the unchanged string for valid UTF-8 input when not stripping invalid bytes.' + ); + + $this->assertSame( + $bytes, + wp_check_invalid_utf8( $bytes, true ), + 'Should have returned the unchanged string for valid UTF-8 input when stripping invalid bytes.' + ); + } else { + $this->assertSame( + '', + wp_check_invalid_utf8( $bytes ), + 'Should have rejected invalid input, returning an empty string when not stripping invalid bytes.' + ); + + $this->assertSame( + $scrubbed, + wp_check_invalid_utf8( $bytes, true ), + 'Failed to properly scrub the invalid spans of UTF-8 from the input string.' + ); + } + } + + /** + * Data provider. + * + * @throws Exception + * + * @return Generator + */ + public static function data_utf8_test_data() { + $test_file = fopen( __DIR__ . '/../../data/unicode/utf8tests/utf8tests.txt', 'r' ); + $line_number = 0; + $last_description = ''; + + while ( false !== ( $line = fgets( $test_file ) ) ) { + ++$line_number; + + if ( empty( trim( $line ) ) ) { + continue; + } + + if ( str_starts_with( $line, '#' ) ) { + $last_description = trim( substr( $line, 1 ) ); + continue; + } + + $test_parts = explode( ':', $line ); + if ( count( $test_parts ) < 3 ) { + throw new Exception( 'Wrong test data: check utf8tests.txt' ); + } + + list( $reference, $classification, $test_data ) = $test_parts; + + $reference = trim( $reference ); + $classification = trim( $classification ); + $test_data = trim( $test_data ); + + switch ( $classification ) { + case 'valid': + yield "{$reference} {$last_description}" => array( $test_data, null ); + break; + + case 'valid hex': + case 'invalid hex': + if ( 'invalid hex' === $classification && count( $test_parts ) < 5 ) { + throw new Exception( "Test data missing expected “scrubbed” value: check utf8tests.txt:{$line_number}" ); + } + + $bytes = hex2bin( str_replace( ' ', '', $test_data ) ); + $scrubbed = 'invalid hex' === $classification + ? hex2bin( str_replace( ' ', '', trim( $test_parts[4] ) ) ) + : null; + + yield "{$reference} {$last_description}" => array( $bytes, $scrubbed ); + break; + + default: + throw new Exception( "Test input file contains unrecognized input classification '{$classification}' (see utf8tests.txt): {$line}" ); + } + } + } +} diff --git a/tests/phpunit/tests/unicode/wpHasNoncharacters.php b/tests/phpunit/tests/unicode/wpHasNoncharacters.php index d3022dd922df2..880f89c4f8e45 100644 --- a/tests/phpunit/tests/unicode/wpHasNoncharacters.php +++ b/tests/phpunit/tests/unicode/wpHasNoncharacters.php @@ -4,9 +4,11 @@ * * @package WordPress * @group unicode + * + * @covers ::wp_has_noncharacters */ +class Tests_Unicode_WpHasNoncharacters extends WP_UnitTestCase { -class Tests_WpHasNoncharacters extends WP_UnitTestCase { /** * Ensures that a noncharacter inside a string will be properly detected. * diff --git a/tests/phpunit/tests/unicode/wpIsValidUtf8.php b/tests/phpunit/tests/unicode/wpIsValidUtf8.php index 43876a7eee8e6..386ff8cf2d6ee 100644 --- a/tests/phpunit/tests/unicode/wpIsValidUtf8.php +++ b/tests/phpunit/tests/unicode/wpIsValidUtf8.php @@ -1,12 +1,15 @@ assertSame( - $bytes, - wp_check_invalid_utf8( $bytes ), - 'Should have returned the unchanged string for valid UTF-8 input when not stripping invalid bytes.' - ); - - $this->assertSame( - $bytes, - wp_check_invalid_utf8( $bytes, true ), - 'Should have returned the unchanged string for valid UTF-8 input when stripping invalid bytes.' - ); - } else { - $this->assertSame( - '', - wp_check_invalid_utf8( $bytes ), - 'Should have rejected invalid input, returning an empty string when not stripping invalid bytes.' - ); - - $this->assertSame( - $scrubbed, - wp_check_invalid_utf8( $bytes, true ), - 'Failed to properly scrub the invalid spans of UTF-8 from the input string.' - ); - } - } +class Tests_Unicode_WpScrubUtf8 extends WP_UnitTestCase { /** * Verifies that WordPress can properly detect valid UTF-8 while replacing invalid byte sequences. @@ -82,7 +47,7 @@ public function test_properly_scrubs_utf8( string $bytes, ?string $scrubbed = nu * @param string $bytes Bytes as a PHP string. * @param string|null $scrubbed Expected checked value, if string isn’t valid UTF-8. */ - public function test_fallback_properly_checks_utf8( string $bytes, ?string $scrubbed = null ) { + public function test_fallback_properly_scrubs_utf8( string $bytes, ?string $scrubbed = null ) { if ( null === $scrubbed ) { $this->assertSame( $bytes, From 609f25f9c5bed003d4cb9c0645c6e81c0e62e334 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 5 Apr 2026 23:13:50 +0000 Subject: [PATCH 20/35] Tests: Move `wp_dropdown_languages()` tests to their own file. This aims to make the tests more discoverable and easier to expand. Follow-up to [36631], [39169], [43359], [44514]. See #64225. git-svn-id: https://develop.svn.wordpress.org/trunk@62208 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/l10n.php | 169 ------------------ .../tests/l10n/wpDropdownLanguages.php | 167 +++++++++++++++++ 2 files changed, 167 insertions(+), 169 deletions(-) create mode 100644 tests/phpunit/tests/l10n/wpDropdownLanguages.php diff --git a/tests/phpunit/tests/l10n.php b/tests/phpunit/tests/l10n.php index 2f7992c34069f..88d867b38f529 100644 --- a/tests/phpunit/tests/l10n.php +++ b/tests/phpunit/tests/l10n.php @@ -126,175 +126,6 @@ public function test_wp_get_installed_translations_for_core() { $this->assertSame( 'GlotPress/4.0.0-beta.2', $data_en_gb['X-Generator'] ); } - /** - * @ticket 35294 - * - * @covers ::wp_dropdown_languages - */ - public function test_wp_dropdown_languages() { - $args = array( - 'id' => 'foo', - 'name' => 'bar', - 'languages' => array( 'de_DE' ), - 'translations' => $this->wp_dropdown_languages_filter(), - 'selected' => 'de_DE', - 'echo' => false, - ); - $actual = wp_dropdown_languages( $args ); - - $this->assertStringContainsString( 'id="foo"', $actual ); - $this->assertStringContainsString( 'name="bar"', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - } - - /** - * @ticket 38632 - * - * @covers ::wp_dropdown_languages - */ - public function test_wp_dropdown_languages_site_default() { - $args = array( - 'id' => 'foo', - 'name' => 'bar', - 'languages' => array( 'de_DE' ), - 'translations' => $this->wp_dropdown_languages_filter(), - 'selected' => 'de_DE', - 'echo' => false, - 'show_option_site_default' => true, - ); - $actual = wp_dropdown_languages( $args ); - - $this->assertStringContainsString( 'id="foo"', $actual ); - $this->assertStringContainsString( 'name="bar"', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - } - - /** - * @ticket 44494 - * - * @covers ::wp_dropdown_languages - */ - public function test_wp_dropdown_languages_exclude_en_us() { - $args = array( - 'id' => 'foo', - 'name' => 'bar', - 'languages' => array( 'de_DE' ), - 'translations' => $this->wp_dropdown_languages_filter(), - 'selected' => 'de_DE', - 'echo' => false, - 'show_option_en_us' => false, - ); - $actual = wp_dropdown_languages( $args ); - - $this->assertStringNotContainsString( '', $actual ); - } - - /** - * @ticket 38632 - * - * @covers ::wp_dropdown_languages - */ - public function test_wp_dropdown_languages_en_US_selected() { - $args = array( - 'id' => 'foo', - 'name' => 'bar', - 'languages' => array( 'de_DE' ), - 'translations' => $this->wp_dropdown_languages_filter(), - 'selected' => 'en_US', - 'echo' => false, - ); - $actual = wp_dropdown_languages( $args ); - - $this->assertStringContainsString( 'id="foo"', $actual ); - $this->assertStringContainsString( 'name="bar"', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - } - - /** - * Add site default language to ja_JP in dropdown - * - * @covers ::wp_dropdown_languages - */ - public function test_wp_dropdown_languages_site_default_ja_JP() { - $args = array( - 'id' => 'foo', - 'name' => 'bar', - 'languages' => array( 'ja_JP' ), - 'translations' => $this->wp_dropdown_languages_filter(), - 'selected' => 'ja_JP', - 'echo' => false, - 'show_option_site_default' => true, - ); - $actual = wp_dropdown_languages( $args ); - - $this->assertStringContainsString( 'id="foo"', $actual ); - $this->assertStringContainsString( 'name="bar"', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - } - - /** - * Select dropdown language from de_DE to ja_JP - * - * @covers ::wp_dropdown_languages - */ - public function test_wp_dropdown_languages_ja_JP_selected() { - $args = array( - 'id' => 'foo', - 'name' => 'bar', - 'languages' => array( 'de_DE' ), - 'translations' => $this->wp_dropdown_languages_filter(), - 'selected' => 'ja_JP', - 'echo' => false, - ); - $actual = wp_dropdown_languages( $args ); - - $this->assertStringContainsString( 'id="foo"', $actual ); - $this->assertStringContainsString( 'name="bar"', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - $this->assertStringContainsString( '', $actual ); - } - - /** - * We don't want to call the API when testing. - * - * @return array - */ - private function wp_dropdown_languages_filter() { - return array( - 'de_DE' => array( - 'language' => 'de_DE', - 'native_name' => 'Deutsch', - 'iso' => array( 'de' ), - ), - 'it_IT' => array( - 'language' => 'it_IT', - 'native_name' => 'Italiano', - 'iso' => array( 'it', 'ita' ), - ), - 'ja_JP' => array( - 'language' => 'ja_JP', - 'native_name' => '日本語', - 'iso' => array( 'ja' ), - ), - ); - } - /** * @ticket 35284 * diff --git a/tests/phpunit/tests/l10n/wpDropdownLanguages.php b/tests/phpunit/tests/l10n/wpDropdownLanguages.php new file mode 100644 index 0000000000000..3d1b7a08bb02e --- /dev/null +++ b/tests/phpunit/tests/l10n/wpDropdownLanguages.php @@ -0,0 +1,167 @@ + 'foo', + 'name' => 'bar', + 'languages' => array( 'de_DE' ), + 'translations' => $this->wp_dropdown_languages_filter(), + 'selected' => 'de_DE', + 'echo' => false, + ); + $actual = wp_dropdown_languages( $args ); + + $this->assertStringContainsString( 'id="foo"', $actual ); + $this->assertStringContainsString( 'name="bar"', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + } + + /** + * @ticket 38632 + */ + public function test_wp_dropdown_languages_site_default() { + $args = array( + 'id' => 'foo', + 'name' => 'bar', + 'languages' => array( 'de_DE' ), + 'translations' => $this->wp_dropdown_languages_filter(), + 'selected' => 'de_DE', + 'echo' => false, + 'show_option_site_default' => true, + ); + $actual = wp_dropdown_languages( $args ); + + $this->assertStringContainsString( 'id="foo"', $actual ); + $this->assertStringContainsString( 'name="bar"', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + } + + /** + * @ticket 44494 + */ + public function test_wp_dropdown_languages_exclude_en_us() { + $args = array( + 'id' => 'foo', + 'name' => 'bar', + 'languages' => array( 'de_DE' ), + 'translations' => $this->wp_dropdown_languages_filter(), + 'selected' => 'de_DE', + 'echo' => false, + 'show_option_en_us' => false, + ); + $actual = wp_dropdown_languages( $args ); + + $this->assertStringNotContainsString( '', $actual ); + } + + /** + * @ticket 38632 + */ + public function test_wp_dropdown_languages_en_US_selected() { + $args = array( + 'id' => 'foo', + 'name' => 'bar', + 'languages' => array( 'de_DE' ), + 'translations' => $this->wp_dropdown_languages_filter(), + 'selected' => 'en_US', + 'echo' => false, + ); + $actual = wp_dropdown_languages( $args ); + + $this->assertStringContainsString( 'id="foo"', $actual ); + $this->assertStringContainsString( 'name="bar"', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + } + + /** + * Add site default language to ja_JP in dropdown + */ + public function test_wp_dropdown_languages_site_default_ja_JP() { + $args = array( + 'id' => 'foo', + 'name' => 'bar', + 'languages' => array( 'ja_JP' ), + 'translations' => $this->wp_dropdown_languages_filter(), + 'selected' => 'ja_JP', + 'echo' => false, + 'show_option_site_default' => true, + ); + $actual = wp_dropdown_languages( $args ); + + $this->assertStringContainsString( 'id="foo"', $actual ); + $this->assertStringContainsString( 'name="bar"', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + } + + /** + * Select dropdown language from de_DE to ja_JP + */ + public function test_wp_dropdown_languages_ja_JP_selected() { + $args = array( + 'id' => 'foo', + 'name' => 'bar', + 'languages' => array( 'de_DE' ), + 'translations' => $this->wp_dropdown_languages_filter(), + 'selected' => 'ja_JP', + 'echo' => false, + ); + $actual = wp_dropdown_languages( $args ); + + $this->assertStringContainsString( 'id="foo"', $actual ); + $this->assertStringContainsString( 'name="bar"', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + $this->assertStringContainsString( '', $actual ); + } + + /** + * We don't want to call the API when testing. + * + * @return array + */ + private function wp_dropdown_languages_filter() { + return array( + 'de_DE' => array( + 'language' => 'de_DE', + 'native_name' => 'Deutsch', + 'iso' => array( 'de' ), + ), + 'it_IT' => array( + 'language' => 'it_IT', + 'native_name' => 'Italiano', + 'iso' => array( 'it', 'ita' ), + ), + 'ja_JP' => array( + 'language' => 'ja_JP', + 'native_name' => '日本語', + 'iso' => array( 'ja' ), + ), + ); + } +} From 96294ed2d6284a181967064b00793ab12120a6fc Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 23 Jan 2026 14:10:02 +0100 Subject: [PATCH 21/35] benchmarking js escaping --- .../html-api/WpHtmlTagProcessorBench.php | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/benchmarks/html-api/WpHtmlTagProcessorBench.php diff --git a/tests/benchmarks/html-api/WpHtmlTagProcessorBench.php b/tests/benchmarks/html-api/WpHtmlTagProcessorBench.php new file mode 100644 index 0000000000000..7342fe52a0737 --- /dev/null +++ b/tests/benchmarks/html-api/WpHtmlTagProcessorBench.php @@ -0,0 +1,105 @@ +set_modifiable_text( $source_text ), 'Failed to set modifiable text.' ); + } + + /** + * @return iterable + */ + public static function provide_processor(): iterable { + foreach ( self::provide_javascript() as $name => $source_text ) { + $processor = new WP_HTML_Tag_Processor( '' ); + $processor->next_tag(); + yield $name => array( $processor, $source_text ); + } + } + + /** + * Provide simple Unix-style paths. + * @return iterable + */ + public static function provide_javascript(): iterable { + yield 'empty' => ''; + + yield 'short' => 'console.log("Hello, World!");'; + + // yield 'tinymce' => file_get_contents( dirname(__DIR__, 2) . 'src/js/_enqueues/vendor/tinymce/tinymce.js' ); + + yield 'many replacements' => <<<'JS' + /* /src/wp-admin/includes/class-wp-filesystem-ftpsockets\.php /src/wp-includes/class-wp-oembed\.php @@ -329,6 +330,7 @@ /tests/phpunit/* + /tests/benchmarks/benchmarks/* From c4a0adb25da49dd9cb83c626b68e2a7b6c0ed139 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 10 Mar 2026 17:23:09 +0100 Subject: [PATCH 29/35] improve benchmarks and isolate set up --- .../html-api/WpHtmlTagProcessorBench.php | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/tests/benchmarks/benchmarks/html-api/WpHtmlTagProcessorBench.php b/tests/benchmarks/benchmarks/html-api/WpHtmlTagProcessorBench.php index 7c774a5a3623a..e70c4a2c83819 100644 --- a/tests/benchmarks/benchmarks/html-api/WpHtmlTagProcessorBench.php +++ b/tests/benchmarks/benchmarks/html-api/WpHtmlTagProcessorBench.php @@ -9,44 +9,43 @@ use PhpBench\Attributes as Bench; -#[Bench\Warmup( 3 )] -#[Bench\Iterations( 10 )] -#[Bench\Revs( 100 )] class WpHtmlTagProcessorBench { + private $processor = null; + + public function clean_up_processor(): void { + $this->processor = null; + } /** * Benchmark normalizing simple Unix paths. - * @param array{0: WP_HTML_Tag_Processor, 1: string} $params + * @param array{0: string} $params */ - #[Bench\ParamProviders( 'provide_script_tag_processor' )] + #[Bench\Warmup( 2 )] + #[Bench\Iterations( 10 )] + #[Bench\Revs( 50 )] + #[Bench\BeforeMethods( 'set_up_script_tag_processor' )] + #[Bench\AfterMethods( 'clean_up_processor' )] + #[Bench\ParamProviders( 'provide_script_tag_contents' )] public function bench_javascript_custom_escape( array $params ): void { - [$processor, $source_text] = $params; - assert( $processor->set_modifiable_text( $source_text ), 'Failed to set modifiable text.' ); + [ $source_text] = $params; + assert( $this->processor->set_modifiable_text( $source_text ), 'Failed to set modifiable text.' ); } - /** - * @return iterable - */ - public static function provide_script_tag_processor(): iterable { - foreach ( self::provide_javascript() as $name => $source_text ) { - $processor = new WP_HTML_Tag_Processor( '' ); - $processor->next_tag(); - yield $name => array( $processor, $source_text ); - } + public function set_up_script_tag_processor(): void { + $this->processor = new WP_HTML_Tag_Processor( '' ); + $this->processor->next_tag(); } /** - * Provide simple Unix-style paths. - * @return iterable + * @return iterable */ - public static function provide_javascript(): iterable { - yield 'empty' => ''; - - yield 'short' => 'console.log("Hello, World!");'; + public static function provide_script_tag_contents(): iterable { + yield 'empty' => array( '' ); - // yield 'tinymce' => file_get_contents( dirname(__DIR__, 2) . 'src/js/_enqueues/vendor/tinymce/tinymce.js' ); + yield 'short' => array( 'console.log("Hello, World!");' ); - yield 'many replacements' => <<<'JS' + yield 'many replacements' => array( + <<<'JS' /*