From 752d544e9ab7bb5bd9948203dc65a2f97bc43015 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 26 Jun 2026 22:49:12 +0000 Subject: [PATCH 01/51] Docs: Correct `$format` default in `get_next_post_link()` and `next_post_link()`. Follow-up to [37254]. Props ishihara-takashi, sabernhardt, khokansardar, mindctrl, SergeyBiryukov. Fixes #65541. git-svn-id: https://develop.svn.wordpress.org/trunk@62565 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/link-template.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index cfff8b6525e10..223d6b5548fc6 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -2291,7 +2291,7 @@ function previous_post_link( $format = '« %link', $link = '%title', $in_sa * * @since 3.7.0 * - * @param string $format Optional. Link anchor format. Default '« %link'. + * @param string $format Optional. Link anchor format. Default '%link »'. * @param string $link Optional. Link permalink format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. @@ -2311,7 +2311,7 @@ function get_next_post_link( $format = '%link »', $link = '%title', $in_sa * * @see get_next_post_link() * - * @param string $format Optional. Link anchor format. Default '« %link'. + * @param string $format Optional. Link anchor format. Default '%link »'. * @param string $link Optional. Link permalink format. Default '%title'. * @param bool $in_same_term Optional. Whether link should be in the same taxonomy term. * Default false. From 2a5a37e54b1474f24e3f49a9345b22c49ff6e2e4 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 27 Jun 2026 22:17:32 +0000 Subject: [PATCH 02/51] Build/Test Tools: Correct `git pull` command for syncing with upstream. Follow-up to [61202]. Props mkrndmane, mukesh27, khokansardar, dhruvang21, SergeyBiryukov. Fixes #65540. git-svn-id: https://develop.svn.wordpress.org/trunk@62566 602fd350-edb4-49c9-b593-d223f7449a82 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5201a5180c1da..820b54759c907 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ You can get started using the local development environment with these steps: 1. Then clone the forked repository to your computer using `git clone https://github.com//wordpress-develop.git`. 1. Navigate into the directory for the cloned repository using `cd wordpress-develop`. 1. Add the origin repo as an `upstream` remote via `git remote add upstream https://github.com/WordPress/wordpress-develop.git`. -1. Then you can keep your branches up to date via `git pull --ff upstream/trunk`, for example. +1. Then you can keep your branches up to date via `git pull --ff upstream trunk`, for example. Alternatively, if you have the [GitHub CLI](https://cli.github.com/) installed, you can simply run `gh repo fork WordPress/wordpress-develop --clone --remote` ([docs](https://cli.github.com/manual/gh_repo_fork)). This command will: 1. Fork the repository to your account (use the `--org` flag to clone into an organization). From e0e6680d69097e330921898e1747030e7d204f48 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Sun, 28 Jun 2026 20:38:14 +0000 Subject: [PATCH 03/51] Administration: Fix selected/active buttons in High Contrast Mode. Follow up to [62467]. Replaces original fix, which turned out to be insufficient. Replaces pseudo-elements with more standard outlines, shifted in scale for visibility. Props sabernhardt, wildworks, apermo, joedolson. Fixes #65153. git-svn-id: https://develop.svn.wordpress.org/trunk@62567 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/css/buttons.css | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/css/buttons.css b/src/wp-includes/css/buttons.css index 09457ce6a4dc5..967970a9ba461 100644 --- a/src/wp-includes/css/buttons.css +++ b/src/wp-includes/css/buttons.css @@ -194,7 +194,8 @@ TABLE OF CONTENTS: color: var(--wp-admin-theme-color-darker-20, #183ad6); border-color: var(--wp-admin-theme-color, #3858e9); box-shadow: inset 0 2px 6px -2px var(--wp-admin-theme-color-darker-20); - position: relative; + outline: 3px solid transparent; + outline-offset: -3px; } .wp-core-ui .button.active:focus { @@ -202,19 +203,7 @@ TABLE OF CONTENTS: color: var(--wp-admin-theme-color-darker-20, #183ad6); border-color: var(--wp-admin-theme-color-darker-20, #183ad6); box-shadow: inset 0 2px 6px -2px var(--wp-admin-theme-color-darker-20), 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); -} - -/* Only visible in Windows High Contrast mode */ -.wp-core-ui .button.active:before { - content: ""; - display: block; - position: absolute; - width: 100%; - height: 0; - border-top: 3px solid transparent; - bottom: 0; - left: 0; - box-sizing: border-box; + outline-width: 4px; } .wp-core-ui .button[disabled], From 8b36984e643a15ca94a9bffff3f9e92d29ac12e9 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Sun, 28 Jun 2026 21:41:22 +0000 Subject: [PATCH 04/51] Administration: Fix cursor on first submenu list item in admin menu. The first submenu item in the collapsed view of the admin menu accepts a click event to navigate, but does not have `cursor: pointer` to indicate that it's interactive. These were removed in [51684], but this specific case (JS activate, menu collapsed, first list item) should remain. Props mazharulrobeen, sumitsingh, sabernhardt, swapnil1010, joedolson. Fixes #65250. git-svn-id: https://develop.svn.wordpress.org/trunk@62568 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/admin-menu.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-admin/css/admin-menu.css b/src/wp-admin/css/admin-menu.css index c4b32ac4b9e87..2b665f583484f 100644 --- a/src/wp-admin/css/admin-menu.css +++ b/src/wp-admin/css/admin-menu.css @@ -417,6 +417,10 @@ ul#adminmenu > li.current > a.current:after { border-color: transparent; } +.js #adminmenu .wp-submenu .wp-submenu-head { + cursor: pointer; +} + #adminmenu li.current, .folded #adminmenu li.wp-menu-open { border: 0 none; From cdf2433e06650ac5d007ccbffbe03c089bd7573a Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 28 Jun 2026 23:21:09 +0000 Subject: [PATCH 05/51] Build/Test Tools: Update GitHub CLI fork command in `README.md`. This resolves an error when running the documented command as of GitHub CLI 2.88.0: {{{ the `--remote` flag is unsupported when a repository argument is provided. }}} Reference: [https://github.com/cli/cli/pull/12375 GitHub CLI: fix: error when --remote flag used with repo argument]. Follow-up to [61202]. Props mkrndmane, khokansardar. Fixes #65542. git-svn-id: https://develop.svn.wordpress.org/trunk@62569 602fd350-edb4-49c9-b593-d223f7449a82 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 820b54759c907..bb6d06c034651 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ You can get started using the local development environment with these steps: 1. Add the origin repo as an `upstream` remote via `git remote add upstream https://github.com/WordPress/wordpress-develop.git`. 1. Then you can keep your branches up to date via `git pull --ff upstream trunk`, for example. -Alternatively, if you have the [GitHub CLI](https://cli.github.com/) installed, you can simply run `gh repo fork WordPress/wordpress-develop --clone --remote` ([docs](https://cli.github.com/manual/gh_repo_fork)). This command will: +Alternatively, if you have the [GitHub CLI](https://cli.github.com/) installed, you can simply run `gh repo fork WordPress/wordpress-develop --clone` ([docs](https://cli.github.com/manual/gh_repo_fork)). This command will: 1. Fork the repository to your account (use the `--org` flag to clone into an organization). 1. Clone the repository to your machine. 1. Add `WordPress/wordpress-develop` as `upstream` and set it to the default `remote` repository From f5523819aa419d97a9bade437a7bbe7d685e9505 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Mon, 29 Jun 2026 08:34:46 +0000 Subject: [PATCH 06/51] Build Tools: Replace deprecated browserslist --update-db command. Replaces the deprecated `--update-db` command in the `browserslist:update` Grunt task with `update-browserslist-db@latest`. Props ekla, sergeybiryukov, masteradhoc. Fixes #64900. git-svn-id: https://develop.svn.wordpress.org/trunk@62570 602fd350-edb4-49c9-b593-d223f7449a82 --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index dae8c3e972e4c..ab56358b8f60d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2370,7 +2370,7 @@ module.exports = function(grunt) { grunt.registerTask( 'browserslist:update', 'Update the local database of browser supports', function() { grunt.log.writeln( `Updating browsers list` ); - spawn( 'npx', [ 'browserslist@latest', '--update-db' ], { + spawn( 'npx', [ 'update-browserslist-db@latest' ], { cwd: __dirname, stdio: 'inherit', } ); From 0853070fca4911a2ac979befa3d94f0cd1921d9a Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 29 Jun 2026 12:21:10 +0000 Subject: [PATCH 07/51] Editor: Add a `date` field to templates and template parts. Templates and template parts previously exposed only a `modified` date. This adds a `date` property to the `WP_Block_Template` class, populated from the post's publish date (`post_date`), and exposes it as a read-only `date` field through the templates REST API controller. Having the publish date available alongside `modified` simplifies revisions handling for these post types, removing workarounds that previously relied on the `modified` date alone. Props ntsekouras, audrasjb, wildworks, mamaduka. Fixes #65049. git-svn-id: https://develop.svn.wordpress.org/trunk@62571 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-template-utils.php | 3 +++ src/wp-includes/class-wp-block-template.php | 8 ++++++++ .../class-wp-rest-templates-controller.php | 12 ++++++++++++ .../wpRestTemplateAutosavesController.php | 5 +++-- .../wpRestTemplateRevisionsController.php | 5 +++-- .../rest-api/wpRestTemplatesController.php | 17 ++++++++++++++++- 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 7aa8cbf8c241e..bb04f7767c171 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -595,6 +595,7 @@ function _remove_theme_attribute_from_template_part_block( &$block ) { * * @since 5.9.0 * @since 6.3.0 Added `modified` property to template objects. + * @since 7.1.0 Added `date` property to template objects. * @access private * * @param array $template_file Theme file. @@ -617,6 +618,7 @@ function _build_block_template_result_from_file( $template_file, $template_type $template->has_theme_file = true; $template->is_custom = true; $template->modified = null; + $template->date = null; if ( 'wp_template' === $template_type ) { $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template_file['slug'] ); @@ -867,6 +869,7 @@ function _build_block_template_object_from_post_object( $post, $terms = array(), $template->is_custom = empty( $meta['is_wp_suggestion'] ); $template->author = $post->post_author; $template->modified = $post->post_modified; + $template->date = $post->post_date; if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { $template->post_types = $template_file['postTypes']; diff --git a/src/wp-includes/class-wp-block-template.php b/src/wp-includes/class-wp-block-template.php index 822302d4c4d85..532180d05f4c3 100644 --- a/src/wp-includes/class-wp-block-template.php +++ b/src/wp-includes/class-wp-block-template.php @@ -162,4 +162,12 @@ class WP_Block_Template { * @var string|null */ public $modified; + + /** + * Date. + * + * @since 7.1.0 + * @var string|null + */ + public $date; } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index e7d6b97934a84..b821ca09453e3 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -667,6 +667,7 @@ protected function prepare_item_for_database( $request ) { * @since 5.8.0 * @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support. * @since 6.3.0 Added `modified` property to the response. + * @since 7.1.0 Added `date` property to the response. * * @param WP_Block_Template $item Template instance. * @param WP_REST_Request $request Request object. @@ -778,6 +779,10 @@ public function prepare_item_for_response( $item, $request ) { $data['modified'] = mysql_to_rfc3339( $template->modified ); } + if ( rest_is_field_included( 'date', $fields ) ) { + $data['date'] = mysql_to_rfc3339( $template->date ); + } + if ( rest_is_field_included( 'author_text', $fields ) ) { $data['author_text'] = self::get_wp_templates_author_text_field( $template ); } @@ -1172,6 +1177,13 @@ public function get_item_schema() { 'user', ), ), + 'date' => array( + 'description' => __( "The date the template was published, in the site's timezone." ), + 'type' => array( 'string', 'null' ), + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), ), ); diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php index d3cbf91260488..9ea5c5abc5f60 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -682,6 +682,7 @@ public function test_get_item_schema_with_data_provider( $rest_base, $template_i $this->assertArrayHasKey( 'has_theme_file', $properties, 'has_theme_file key should exist in properties.' ); $this->assertArrayHasKey( 'author', $properties, 'author key should exist in properties.' ); $this->assertArrayHasKey( 'modified', $properties, 'modified key should exist in properties.' ); + $this->assertArrayHasKey( 'date', $properties, 'date key should exist in properties.' ); $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); $this->assertArrayHasKey( 'author_text', $properties, 'author_text key should exist in properties.' ); $this->assertArrayHasKey( 'original_source', $properties, 'original_source key should exist in properties.' ); @@ -700,13 +701,13 @@ public function data_get_item_schema_with_data_provider() { 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, - 19, + 20, array( 'is_custom', 'plugin' ), ), 'template parts' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, - 18, + 19, array( 'area' ), ), ); diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php index e8a18b275e7cd..b5c4c79b00c70 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php @@ -928,6 +928,7 @@ public function test_get_item_schema_with_data_provider( $rest_base, $template_i $this->assertArrayHasKey( 'has_theme_file', $properties, 'has_theme_file key should exist in properties.' ); $this->assertArrayHasKey( 'author', $properties, 'author key should exist in properties.' ); $this->assertArrayHasKey( 'modified', $properties, 'modified key should exist in properties.' ); + $this->assertArrayHasKey( 'date', $properties, 'date key should exist in properties.' ); $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); $this->assertArrayHasKey( 'author_text', $properties, 'author_text key should exist in properties.' ); $this->assertArrayHasKey( 'original_source', $properties, 'original_source key should exist in properties.' ); @@ -947,13 +948,13 @@ public function data_get_item_schema_with_data_provider() { 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, - 19, + 20, array( 'is_custom', 'plugin' ), ), 'template parts' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, - 18, + 19, array( 'area' ), ), ); diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index 0bbd6b151c6c0..42eed8dfa9c35 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -168,6 +168,7 @@ public function test_get_items() { 'is_custom' => true, 'author' => 0, 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'date' => mysql_to_rfc3339( self::$template_post->post_date ), 'author_text' => 'Test Blog', 'original_source' => 'site', ), @@ -247,6 +248,7 @@ public function test_get_items_editor() { 'is_custom' => true, 'author' => 0, 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'date' => mysql_to_rfc3339( self::$template_post->post_date ), 'author_text' => 'Test Blog', 'original_source' => 'site', ), @@ -304,6 +306,7 @@ public function test_get_item() { 'is_custom' => true, 'author' => 0, 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'date' => mysql_to_rfc3339( self::$template_post->post_date ), 'author_text' => 'Test Blog', 'original_source' => 'site', ), @@ -355,6 +358,7 @@ public function test_get_item_editor() { 'is_custom' => true, 'author' => 0, 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'date' => mysql_to_rfc3339( self::$template_post->post_date ), 'author_text' => 'Test Blog', 'original_source' => 'site', ), @@ -404,6 +408,7 @@ public function test_get_item_works_with_a_single_slash( $endpoint_url ) { 'is_custom' => true, 'author' => 0, 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'date' => mysql_to_rfc3339( self::$template_post->post_date ), 'author_text' => 'Test Blog', 'original_source' => 'site', ), @@ -469,6 +474,7 @@ public function test_get_item_with_valid_theme_dirname( $theme_dir, $template, a 'modified' => mysql_to_rfc3339( $post->post_modified ), 'author_text' => $author_name, 'original_source' => 'user', + 'date' => mysql_to_rfc3339( $post->post_date ), ), $data ); @@ -673,6 +679,7 @@ public function test_create_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $modified = get_post( $data['wp_id'] )->post_modified; + $date = get_post( $data['wp_id'] )->post_date; unset( $data['_links'] ); unset( $data['wp_id'] ); @@ -699,6 +706,7 @@ public function test_create_item() { 'is_custom' => true, 'author' => self::$admin_id, 'modified' => mysql_to_rfc3339( $modified ), + 'date' => mysql_to_rfc3339( $date ), 'author_text' => $author_name, 'original_source' => 'user', ), @@ -725,6 +733,7 @@ public function test_create_item_with_numeric_slug() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $modified = get_post( $data['wp_id'] )->post_modified; + $date = get_post( $data['wp_id'] )->post_date; unset( $data['_links'] ); unset( $data['wp_id'] ); @@ -751,6 +760,7 @@ public function test_create_item_with_numeric_slug() { 'is_custom' => false, 'author' => self::$admin_id, 'modified' => mysql_to_rfc3339( $modified ), + 'date' => mysql_to_rfc3339( $date ), 'author_text' => $author_name, 'original_source' => 'user', ), @@ -781,6 +791,7 @@ public function test_create_item_raw() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $modified = get_post( $data['wp_id'] )->post_modified; + $date = get_post( $data['wp_id'] )->post_date; unset( $data['_links'] ); unset( $data['wp_id'] ); @@ -807,6 +818,7 @@ public function test_create_item_raw() { 'is_custom' => true, 'author' => self::$admin_id, 'modified' => mysql_to_rfc3339( $modified ), + 'date' => mysql_to_rfc3339( $date ), 'author_text' => $author_name, 'original_source' => 'user', ), @@ -967,7 +979,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 18, $properties ); + $this->assertCount( 19, $properties ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'slug', $properties ); @@ -984,6 +996,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'is_custom', $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'modified', $properties ); + $this->assertArrayHasKey( 'date', $properties ); $this->assertArrayHasKey( 'author_text', $properties ); $this->assertArrayHasKey( 'original_source', $properties ); $this->assertArrayHasKey( 'plugin', $properties ); @@ -1020,7 +1033,9 @@ public function test_create_item_with_is_wp_suggestion( array $body_params, arra $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $modified = get_post( $data['wp_id'] )->post_modified; + $date = get_post( $data['wp_id'] )->post_date; $expected['modified'] = mysql_to_rfc3339( $modified ); + $expected['date'] = mysql_to_rfc3339( $date ); $expected['author_text'] = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); $expected['original_source'] = 'user'; From cb14d226fd341f24e351976e93aab7c6561752b0 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 29 Jun 2026 14:12:06 +0000 Subject: [PATCH 08/51] XML-RPC: Correct argument mismatch in `::_multisite_getUsersBlogs()`. This ensures that `::wp_getUsersBlogs()` receives the valid authentication arguments when called from `::blogger_getUsersBlogs()` via `::_multisite_getUsersBlogs()`. Follow-up to [54468]. Props sainathpoojary, SergeyBiryukov. Fixes #65536. git-svn-id: https://develop.svn.wordpress.org/trunk@62572 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-xmlrpc-server.php | 2 +- .../tests/xmlrpc/blogger/getUsersBlogs.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/phpunit/tests/xmlrpc/blogger/getUsersBlogs.php diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 8cbf6d977f5a2..1fff1bba65adf 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -4892,7 +4892,7 @@ protected function _multisite_getUsersBlogs( $args ) { $domain = $current_blog->domain; $path = $current_blog->path . 'xmlrpc.php'; - $blogs = $this->wp_getUsersBlogs( $args ); + $blogs = $this->wp_getUsersBlogs( array( $args[1], $args[2] ) ); if ( $blogs instanceof IXR_Error ) { return $blogs; } diff --git a/tests/phpunit/tests/xmlrpc/blogger/getUsersBlogs.php b/tests/phpunit/tests/xmlrpc/blogger/getUsersBlogs.php new file mode 100644 index 0000000000000..5ca31c9da2495 --- /dev/null +++ b/tests/phpunit/tests/xmlrpc/blogger/getUsersBlogs.php @@ -0,0 +1,28 @@ +make_user_by_role( 'subscriber' ); + + $result = $this->myxmlrpcserver->blogger_getUsersBlogs( array( 1, 'subscriber', 'subscriber' ) ); + + $this->assertNotIXRError( $result, 'The result should not be an instance of IXR_Error.' ); + $this->assertIsArray( $result, 'The result should be an array.' ); + $this->assertNotEmpty( $result, 'The result should not be empty.' ); + + $blog = $result[0]; + $this->assertArrayHasKey( 'url', $blog, 'The result should include the url field.' ); + $this->assertArrayHasKey( 'blogid', $blog, 'The result should include the blogid field.' ); + $this->assertArrayHasKey( 'blogName', $blog, 'The result should include the blogName field.' ); + } +} From db48dcfc2636fe84c913cadfc5ba1ed94dea21b7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 14:33:42 +0000 Subject: [PATCH 09/51] HTML API: Replace locale-dependent ctype check in HTML decoder. `ctype_alnum()` behaves differently depending on the host system and locale. Replace it with a direct ASCII byte comparison that behaves consistently across environments. Developed in https://github.com/WordPress/wordpress-develop/pull/12286. Props jonsurrell, dmsnell. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62573 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-decoder.php | 72 +++++--- .../phpunit/tests/html-api/wpHtmlDecoder.php | 158 ++++++++++++++++++ 2 files changed, 207 insertions(+), 23 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-decoder.php b/src/wp-includes/html-api/class-wp-html-decoder.php index b6c240bdcff5f..e4634f8fa23ed 100644 --- a/src/wp-includes/html-api/class-wp-html-decoder.php +++ b/src/wp-includes/html-api/class-wp-html-decoder.php @@ -367,34 +367,60 @@ public static function read_character_reference( $context, $text, $at = 0, &$mat $after_name = $name_at + $name_length; - // If the match ended with a semicolon then it should always be decoded. - if ( ';' === $text[ $name_at + $name_length - 1 ] ) { - $match_byte_length = $after_name - $at; - return $replacement; - } - - /* - * At this point though there's a match for an entry in the named - * character reference table but the match doesn't end in `;`. - * It may be allowed if it's followed by something unambiguous. + /** + * For historical reasons, a matched named character reference is left as literal + * text (its decoded replacement is not used) when all of the following hold: + * + * 1. It was matched in attribute context. + * 2. The match does not end in U+003B SEMICOLON (;) — i.e. it is one of the + * legacy forms recognized without a trailing semicolon. + * 3. The next input character is U+003D EQUALS SIGN (=) or an ASCII alphanumeric. + * + * Some illustrative examples follow. Note that both `not` and `not;` appear in the + * named character references list. References start with `&` and typically end with + * `;`, but the legacy forms are recognized without one. + * + * - In _data context_, "¬me" is decoded to "¬me": condition 1 fails (not an + * attribute), so the reference is decoded. + * - In _attribute context_, "¬me" is decoded to "¬me": the longest match is + * "not;", which ends in a semicolon, so condition 2 fails. + * - In _attribute context_, "¬己" is decoded to "¬己": the following character + * "己" is a letter but not an ASCII alphanumeric (nor "="), so condition 3 fails. + * - In _attribute context_, "¬" is decoded to "¬": there is no next input + * character, so condition 3 fails. + * - In _attribute context_, "¬=me" is left as the literal text "¬=me": all + * three conditions hold. + * - In _attribute context_, "¬me" is left as the literal text "¬me": all + * three conditions hold. + * + * Without these special rules, ordinary URL query strings could have surprising + * replacements applied. Consider: + * + * + * + * The literal attribute value `/?random°ree>=0<=360¬=90` is preserved + * by the special handling. Otherwise, the value would decode to + * `/?random°ree>=0<=360¬=90`, which is unlikely to be the author's intent. + * + * (Authors should not rely on this. Escaping the example as + * `/?random&degree&gt=0&lt=360&not=90` produces the intended + * value regardless of the following character.) + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state + * @see https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references */ - $ambiguous_follower = ( - $after_name < $length && - $name_at < $length && - ( - ctype_alnum( $text[ $after_name ] ) || - '=' === $text[ $after_name ] - ) - ); - - // It's non-ambiguous, safe to leave it in. - if ( ! $ambiguous_follower ) { + if ( 'attribute' !== $context || ';' === $text[ $after_name - 1 ] || $after_name >= $length ) { $match_byte_length = $after_name - $at; return $replacement; } - // It's ambiguous, which isn't allowed inside attributes. - if ( 'attribute' === $context ) { + $follower_byte = ord( $text[ $after_name ] ); + if ( + 0x3D === $follower_byte || // EQUALS SIGN + ( $follower_byte >= 0x30 && $follower_byte <= 0x39 ) || // ASCII digits 0-9 + ( $follower_byte >= 0x41 && $follower_byte <= 0x5A ) || // ASCII upper alpha A-Z + ( $follower_byte >= 0x61 && $follower_byte <= 0x7A ) // ASCII lower alpha a-z + ) { return null; } diff --git a/tests/phpunit/tests/html-api/wpHtmlDecoder.php b/tests/phpunit/tests/html-api/wpHtmlDecoder.php index 97954f4eb3e30..158115cdfbf06 100644 --- a/tests/phpunit/tests/html-api/wpHtmlDecoder.php +++ b/tests/phpunit/tests/html-api/wpHtmlDecoder.php @@ -12,6 +12,55 @@ * @coversDefaultClass WP_HTML_Decoder */ class Tests_HtmlApi_WpHtmlDecoder extends WP_UnitTestCase { + /** + * Original LC_CTYPE locale. + * + * @var string|bool + */ + private static $original_lc_ctype = false; + + /** + * Locale where ctype_alnum() classifies high-bit bytes as alphanumeric. + * + * @var string|null + */ + private static ?string $problematic_lc_ctype = null; + + public static function set_up_before_class() { + parent::set_up_before_class(); + + self::$original_lc_ctype = setlocale( LC_CTYPE, 0 ); + + // Find a locale where ctype_alnum() classifies high-bit bytes as alphanumeric. + $locale_candidates = array( + 'C.UTF-8', + 'C.utf8', + 'en_US.UTF-8', + 'en_US.utf8', + 'en_GB.UTF-8', + 'en_GB.utf8', + ); + foreach ( $locale_candidates as $locale ) { + $candidate_locale = setlocale( LC_CTYPE, $locale ); + + if ( false !== $candidate_locale && ctype_alnum( "\xC2" ) ) { + self::$problematic_lc_ctype = $candidate_locale; + break; + } + } + + if ( self::$original_lc_ctype ) { + setlocale( LC_CTYPE, self::$original_lc_ctype ); + } + } + + public function tear_down() { + if ( self::$original_lc_ctype ) { + setlocale( LC_CTYPE, self::$original_lc_ctype ); + } + parent::tear_down(); + } + /** * Ensures proper decoding of edge cases. * @@ -61,6 +110,115 @@ static function ( int $errno, string $errstr ) use ( &$errors ) { $this->assertSame( "&\x00b", $decoded, 'Should have decoded the text without changing it.' ); } + /** + * Ensures semicolonless legacy references decode before non-ASCII UTF-8 bytes in attributes. + * + * @dataProvider data_semicolonless_attribute_behaviors + * + * @ticket 65372 + */ + public function test_semicolonless_legacy_reference_before_multibyte_attribute_follower( string $encoded_attribute_value, string $expected, string $expected_decode, int $expected_byte_length ): void { + if ( null !== self::$problematic_lc_ctype ) { + setlocale( LC_CTYPE, self::$problematic_lc_ctype ); + } + + $this->assertSame( + $expected, + WP_HTML_Decoder::decode_attribute( $encoded_attribute_value ), + 'Failed to decode the full attribute value as expected.' + ); + + $match_byte_length = null; + $this->assertSame( + $expected_decode, + WP_HTML_Decoder::read_character_reference( 'attribute', $encoded_attribute_value, 0, $match_byte_length ), + 'Failed to decode the character reference as expected.' + ); + $this->assertSame( $expected_byte_length, $match_byte_length, 'Failed to produce expected byte length.' ); + } + + /** + * Data provider. + * + * Attribute values encoded with character references including followers that are + * treated as alphanumerics by `ctype_alnum()` on some systems, but should never + * be recognized as ASCII Alphanumerics according to the HTML standards. + * + * @see https://html.spec.whatwg.org/#named-character-reference-state + * + * @return array Test cases. + */ + public static function data_semicolonless_attribute_behaviors(): array { + return array( + array( '©¯\_(ツ)_/¯', '©¯\_(ツ)_/¯', '©', 5 ), + array( '¬ಠ_ಠ', '¬ಠ_ಠ', '¬', 4 ), + array( ' £20', "\u{00A0}£20", "\u{00A0}", 5 ), + array( ' 🎉', "\u{00A0}🎉", "\u{00A0}", 5 ), + array( '®™', '®™', '®', 4 ), + ); + } + + /** + * Ensures ambiguous ampersand is recognized with trailing ASCII alphanumerics. + * + * @dataProvider data_semicolonless_attribute_character_reference_no_decode_followers + * + * @ticket 65372 + * + * @param string $raw_attribute Raw attribute value with an ambiguous legacy reference follower. + */ + public function test_ascii_alphanumeric_attribute_follower_is_ambiguous( string $raw_attribute ): void { + $this->assertSame( + $raw_attribute, + WP_HTML_Decoder::decode_attribute( $raw_attribute ), + 'Should not have decoded an ambiguous semicolonless legacy reference.' + ); + + $match_byte_length = 'sentinel'; + $this->assertNull( + WP_HTML_Decoder::read_character_reference( 'attribute', $raw_attribute, 0, $match_byte_length ), + 'Should not have matched an ambiguous semicolonless legacy reference.' + ); + $this->assertSame( 'sentinel', $match_byte_length ); + } + + /** + * Data provider. + * + * HTML character references with followers that trigger the literal flush behavior + * when parsing attribute values. HTML defines this as `=` or an ASCII alphanumeric character. + * + * > An ASCII alphanumeric is an ASCII digit or ASCII alpha. + * > An ASCII alpha is an ASCII upper alpha or ASCII lower alpha. + * + * @see https://html.spec.whatwg.org/#named-character-reference-state + * + * @return Generator Test cases. + */ + public static function data_semicolonless_attribute_character_reference_no_decode_followers(): Generator { + yield "Equals sign follower '='" => array( 'Á=' ); + // > An ASCII digit is a code point in the range U+0030 (0) to U+0039 (9), inclusive. + for ( $i = 0x30; $i <= 0x39; $i++ ) { + $char = chr( $i ); + yield "ASCII digit follower '{$char}'" => array( "Á{$char}" ); + } + // > An ASCII upper alpha is a code point in the range U+0041 (A) to U+005A (Z), inclusive. + for ( $i = 0x41; $i <= 0x5A; $i++ ) { + $char = chr( $i ); + yield "ASCII upper alpha follower '{$char}'" => array( "Á{$char}" ); + } + // > An ASCII lower alpha is a code point in the range U+0061 (a) to U+007A (z), inclusive. + for ( $i = 0x61; $i <= 0x7A; $i++ ) { + $char = chr( $i ); + yield "ASCII lower alpha follower '{$char}'" => array( "Á{$char}" ); + } + } + /** * Ensures proper detection of attribute prefixes ignoring ASCII case. * From 23a6bbf248a826ae213a012e91441c4e29d5f34c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 15:06:56 +0000 Subject: [PATCH 10/51] HTML API: Prevent HTML newline normalization on foreign elements. HTML and foreign element normalization differ in some cases. Ensure the HTML-specific newline injection is not applied to foreign elements like `svg:textarea`. Developed in https://github.com/WordPress/wordpress-develop/pull/12322. Follow-up to [61747]. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62574 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-processor.php | 6 +-- .../html-api/wpHtmlProcessor-serialize.php | 49 ++++++++++++++----- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 10f3ee3e2dd0f..5f15da5383f34 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1468,8 +1468,8 @@ public function serialize_token(): string { /* * The HTML parser strips a leading newline immediately after the start - * tag of TEXTAREA, PRE, and LISTING elements. When serializing, prepend - * a leading newline to ensure the semantic HTML content is preserved. + * tag of TEXTAREA, PRE, and LISTING elements in HTML content. When serializing, + * prepend a leading newline to ensure the semantic HTML content is preserved. * * For example, `
\n\nX
` must not become `
\nX
` because its content * has changed. However, `
X
` and `
\nX
` are _equivalent_. @@ -1488,7 +1488,7 @@ public function serialize_token(): string { * * @see https://html.spec.whatwg.org/multipage/parsing.html */ - if ( 'TEXTAREA' === $tag_name || 'PRE' === $tag_name || 'LISTING' === $tag_name ) { + if ( $in_html && ( 'TEXTAREA' === $tag_name || 'PRE' === $tag_name || 'LISTING' === $tag_name ) ) { $html .= "\n"; } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index d9d7d7c13394a..e332ec12a0a91 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -463,21 +463,30 @@ public static function data_provider_serialize_doctype() { } /** - * Ensures that leading newlines in PRE, LISTING, and TEXTAREA elements are preserved upon normalization, - * and that normalization is idempotent in these cases. + * Ensures that leading newlines in PRE, LISTING, and TEXTAREA elements are normalized + * according to their parsing namespace, and that normalization is idempotent in these cases. * * @ticket 64607 * * @dataProvider data_provider_normalize_special_leading_newline_cases * * @param string $input HTML input containing leading newlines in PRE, LISTING, or TEXTAREA elements. - * @param string $expected Expected output after normalization, which should preserve leading newlines. + * @param string $expected Expected exact output after normalization. */ public function test_normalize_special_leading_newline_handling( string $input, string $expected ) { $normalized = WP_HTML_Processor::normalize( $input ); - $this->assertEqualHTML( $expected, $normalized ); + + /* + * Byte equality pins normalize()'s serialized form; HTML equality verifies + * semantic equivalence. This distinction matters because HTML parsing ignores + * one leading LF after PRE, LISTING, and TEXTAREA start tags. + */ + $this->assertSame( $expected, $normalized ); + $this->assertEqualHTML( $input, $normalized ); + $normalized_twice = WP_HTML_Processor::normalize( $normalized ); - $this->assertEqualHTML( $expected, $normalized_twice ); + $this->assertSame( $expected, $normalized_twice ); + $this->assertEqualHTML( $normalized, $normalized_twice ); } /** @@ -653,13 +662,13 @@ public static function data_provider_normalized_fuzzer_cases_that_should_be_idem /** * Data provider. * - * @return array[] + * @return array */ - public static function data_provider_normalize_special_leading_newline_cases() { + public static function data_provider_normalize_special_leading_newline_cases(): array { return array( 'Leading newline in PRE' => array( "
\nline 1\nline 2
", - "
line 1\nline 2
", + "
\nline 1\nline 2
", ), 'Double leading newline in PRE' => array( "
\n\nline 2\nline 3
", @@ -667,7 +676,7 @@ public static function data_provider_normalize_special_leading_newline_cases() { ), 'Multiple text nodes inside PRE' => array( "
\nline 1 still line 1
", - '
line 1 still line 1
', + "
\nline 1 still line 1
", ), 'Multiple text nodes inside PRE with leading newlines' => array( "
\n\nline 2 still line 2
", @@ -675,7 +684,7 @@ public static function data_provider_normalize_special_leading_newline_cases() { ), 'Leading newline in LISTING' => array( "\nline 1\nline 2", - "line 1\nline 2", + "\nline 1\nline 2", ), 'Double leading newline in LISTING' => array( "\n\nline 2\nline 3", @@ -683,7 +692,7 @@ public static function data_provider_normalize_special_leading_newline_cases() { ), 'Multiple text nodes inside LISTING' => array( "\nline 1 still line 1", - 'line 1 still line 1', + "\nline 1 still line 1", ), 'Multiple text nodes inside LISTING with leading newlines' => array( "\n\nline 2 still line 2", @@ -691,12 +700,28 @@ public static function data_provider_normalize_special_leading_newline_cases() { ), 'Leading newline in TEXTAREA' => array( "", - "", + "", ), 'Double leading newline in TEXTAREA' => array( "", "", ), + 'Foreign MathML TEXTAREA does not ignore leading newlines' => array( + '', + '', + ), + 'Foreign MathML TEXTAREA preserves leading newline' => array( + "", + "", + ), + 'Foreign SVG TEXTAREA does not ignore leading newlines' => array( + '', + '', + ), + 'Foreign SVG TEXTAREA preserves leading newline' => array( + "", + "", + ), ); } } From f149add5b5196da9c23c93bb9fdea6c787b55c21 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 29 Jun 2026 17:54:32 +0000 Subject: [PATCH 11/51] HTML API: Respect namespace in open element lookup. Prevent foreign elements from incorrectly satisfying checks for open HTML elements. Developed in https://github.com/WordPress/wordpress-develop/pull/12353. Props jonsurrell, dmsnell. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62575 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/class-wp-html-open-elements.php | 6 +++--- .../tests/html-api/wpHtmlProcessor.php | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-open-elements.php b/src/wp-includes/html-api/class-wp-html-open-elements.php index aeee107250895..5c99db6d5eb4e 100644 --- a/src/wp-includes/html-api/class-wp-html-open-elements.php +++ b/src/wp-includes/html-api/class-wp-html-open-elements.php @@ -128,16 +128,16 @@ public function at( int $nth ): ?WP_HTML_Token { } /** - * Reports if a node of a given name is in the stack of open elements. + * Reports if an HTML element of a given name is on the stack of open elements. * * @since 6.7.0 * - * @param string $node_name Name of node for which to check. + * @param string $node_name Name of HTML element for which to check. * @return bool Whether a node of the given name is in the stack of open elements. */ public function contains( string $node_name ): bool { foreach ( $this->walk_up() as $item ) { - if ( $node_name === $item->node_name ) { + if ( 'html' === $item->namespace && $node_name === $item->node_name ) { return true; } } diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php index bb18629563493..1e1ca7f6f8c39 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -642,6 +642,26 @@ public function test_template_tag_closes_html_template_element() { $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $processor->get_breadcrumbs() ); } + /** + * Ensures foreign TEMPLATE elements do not satisfy HTML template handling. + * + * @ticket 65372 + */ + public function test_unmatched_template_closer_after_mathml_template_is_ignored() { + $processor = WP_HTML_Processor::create_fragment( '