From 6974b994de5e9c707607dcb4233bd785865b4c6a Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 30 Apr 2024 16:43:09 +0000 Subject: [PATCH 01/21] Coding Standards: Remove extra conditional in `get_plugins()`. Follow-up to [1894], [5152], [55990]. Props abhijitrakas, mukesh27. Fixes #44853. git-svn-id: https://develop.svn.wordpress.org/trunk@58067 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/plugin.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index bcae2733670d2..f882b9b61ffc1 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -319,10 +319,8 @@ function get_plugins( $plugin_folder = '' ) { closedir( $plugins_subdir ); } - } else { - if ( str_ends_with( $file, '.php' ) ) { - $plugin_files[] = $file; - } + } elseif ( str_ends_with( $file, '.php' ) ) { + $plugin_files[] = $file; } } From 816ff687eb6fce33577472285d7f36015271216a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 May 2024 15:40:02 +0000 Subject: [PATCH 02/21] Script Loader: Ensure `wp_localize_script()` works when called early. Before, `wp_localize_script()` did not work when the `$wp_scripts` global was not already set (for example because of a script registration happening elsewhere) and even emitted a warning in that case. Due to side effects such as block registration early in the load process, this usually never happened. However, the absence of these side effects in 6.5 caused the `wp_localize_script()` to no longer work in places such as the `login_enqueue_scripts`. By calling `wp_scripts()` in `wp_localize_script()`, the `$wp_scripts` global is automatically set if needed, restoring previous behavior. Adds both a PHP unit test and an e2e test to verify this use case. Hat tip: jorbin. Happy birthday, Aaron! Props salcode, aslamdoctor, jorbin, swissspidy. Fixes #60862. git-svn-id: https://develop.svn.wordpress.org/trunk@58068 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.wp-scripts.php | 8 +-- tests/e2e/specs/login-localize-script.test.js | 53 +++++++++++++++++++ .../tests/dependencies/wpLocalizeScript.php | 41 ++++++++++++++ 3 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 tests/e2e/specs/login-localize-script.test.js create mode 100644 tests/phpunit/tests/dependencies/wpLocalizeScript.php diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 1207502c44839..de134961f8f3a 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -211,7 +211,6 @@ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args * * @see WP_Scripts::localize() * @link https://core.trac.wordpress.org/ticket/11520 - * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. * * @since 2.2.0 * @@ -224,12 +223,7 @@ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args * @return bool True if the script was successfully localized, false otherwise. */ function wp_localize_script( $handle, $object_name, $l10n ) { - global $wp_scripts; - - if ( ! ( $wp_scripts instanceof WP_Scripts ) ) { - _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); - return false; - } + $wp_scripts = wp_scripts(); return $wp_scripts->localize( $handle, $object_name, $l10n ); } diff --git a/tests/e2e/specs/login-localize-script.test.js b/tests/e2e/specs/login-localize-script.test.js new file mode 100644 index 0000000000000..ecaa48998329d --- /dev/null +++ b/tests/e2e/specs/login-localize-script.test.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * WordPress dependencies + */ +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; + +test.describe( 'Localize Script on wp-login.php', () => { + const muPlugins = join( + process.cwd(), + process.env.LOCAL_DIR ?? 'src', + 'wp-content/mu-plugins' + ); + const muPluginFile = join( muPlugins, 'login-test.php' ); + + test.beforeAll( async () => { + const muPluginCode = ` 42, + ] + ); + } + );`; + + if ( ! existsSync( muPlugins ) ) { + mkdirSync( muPlugins, { recursive: true } ); + } + writeFileSync( muPluginFile, muPluginCode ); + } ); + + test.afterAll( async () => { + unlinkSync( muPluginFile ); + } ); + + test( 'should localize script', async ( { page } ) => { + await page.goto( '/wp-login.php' ); + await page.waitForSelector( '#login' ); + const testData = await page.evaluate( () => window.testData ); + expect( + testData.answerToTheUltimateQuestionOfLifeTheUniverseAndEverything + ).toBe( '42' ); + } ); +} ); diff --git a/tests/phpunit/tests/dependencies/wpLocalizeScript.php b/tests/phpunit/tests/dependencies/wpLocalizeScript.php new file mode 100644 index 0000000000000..ed79d3e7bd5db --- /dev/null +++ b/tests/phpunit/tests/dependencies/wpLocalizeScript.php @@ -0,0 +1,41 @@ +old_wp_scripts = $GLOBALS['wp_scripts'] ?? null; + $GLOBALS['wp_scripts'] = null; + } + + public function tear_down() { + $GLOBALS['wp_scripts'] = $this->old_wp_scripts; + parent::tear_down(); + } + + /** + * Verifies that wp_localize_script() works if global has not been initialized yet. + * + * @ticket 60862 + * @covers ::wp_localize_script + */ + public function test_wp_localize_script_works_before_enqueue_script() { + $this->assertTrue( + wp_localize_script( + 'wp-util', + 'salcodeExample', + array( + 'answerToTheUltimateQuestionOfLifeTheUniverseAndEverything' => 42, + ) + ) + ); + } +} From 2f2dbbf24671f1aa12d013f7929491059055d6bf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 May 2024 17:59:05 +0000 Subject: [PATCH 03/21] General: Remove any usage of `wp_reset_vars()`. The way `wp_reset_vars()` sets global variables based on `$_POST` and `$_GET` values makes code hard to understand and maintain. It also makes it easy to forget to sanitize input. This change removes the few places where `wp_reset_vars()` is used in the admin to explicitly use `$_REQUEST` and sanitize any input. Props swissspidy, audrasjb, davideferre, killua99, weijland, voldemortensen. Fixes #38073. git-svn-id: https://develop.svn.wordpress.org/trunk@58069 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/admin-post.php | 2 +- src/wp-admin/comment.php | 3 ++- src/wp-admin/customize.php | 4 +++- src/wp-admin/edit-tag-form.php | 6 +----- src/wp-admin/includes/class-wp-links-list-table.php | 5 ++++- .../includes/class-wp-ms-themes-list-table.php | 4 +++- .../includes/class-wp-plugin-install-list-table.php | 2 +- src/wp-admin/includes/class-wp-plugins-list-table.php | 3 ++- .../includes/class-wp-theme-install-list-table.php | 3 ++- src/wp-admin/includes/misc.php | 1 - src/wp-admin/link-add.php | 4 +++- src/wp-admin/link.php | 4 +++- src/wp-admin/media.php | 2 +- src/wp-admin/options-head.php | 2 +- src/wp-admin/options.php | 3 ++- src/wp-admin/post.php | 2 +- src/wp-admin/revision.php | 10 ++++++---- src/wp-admin/site-health.php | 2 +- src/wp-admin/theme-editor.php | 5 ++++- src/wp-admin/theme-install.php | 2 +- src/wp-admin/themes.php | 4 +++- src/wp-admin/user-edit.php | 5 +++-- 22 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/wp-admin/admin-post.php b/src/wp-admin/admin-post.php index e71f5cd1e73d8..be32e0710ac92 100644 --- a/src/wp-admin/admin-post.php +++ b/src/wp-admin/admin-post.php @@ -29,7 +29,7 @@ /** This action is documented in wp-admin/admin.php */ do_action( 'admin_init' ); -$action = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : ''; +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; // Reject invalid parameters. if ( ! is_scalar( $action ) ) { diff --git a/src/wp-admin/comment.php b/src/wp-admin/comment.php index 349a32a43ed09..e1058695a3c5d 100644 --- a/src/wp-admin/comment.php +++ b/src/wp-admin/comment.php @@ -16,7 +16,8 @@ * @global string $action */ global $action; -wp_reset_vars( array( 'action' ) ); + +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; if ( isset( $_POST['deletecomment'] ) ) { $action = 'deletecomment'; diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 2f0bc87b86243..2a53480feec9c 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -84,8 +84,10 @@ } } +$url = ! empty( $_REQUEST['url'] ) ? sanitize_text_field( $_REQUEST['url'] ) : ''; +$return = ! empty( $_REQUEST['return'] ) ? sanitize_text_field( $_REQUEST['return'] ) : ''; +$autofocus = ! empty( $_REQUEST['autofocus'] ) ? sanitize_text_field( $_REQUEST['autofocus'] ) : ''; -wp_reset_vars( array( 'url', 'return', 'autofocus' ) ); if ( ! empty( $url ) ) { $wp_customize->set_preview_url( wp_unslash( $url ) ); } diff --git a/src/wp-admin/edit-tag-form.php b/src/wp-admin/edit-tag-form.php index 8126f84556c0c..ba2e187de43a7 100644 --- a/src/wp-admin/edit-tag-form.php +++ b/src/wp-admin/edit-tag-form.php @@ -44,11 +44,7 @@ do_action_deprecated( 'edit_tag_form_pre', array( $tag ), '3.0.0', '{$taxonomy}_pre_edit_form' ); } -/** - * Use with caution, see https://developer.wordpress.org/reference/functions/wp_reset_vars/ - */ -wp_reset_vars( array( 'wp_http_referer' ) ); - +$wp_http_referer = ! empty( $_REQUEST['wp_http_referer'] ) ? sanitize_text_field( $_REQUEST['wp_http_referer'] ) : ''; $wp_http_referer = remove_query_arg( array( 'action', 'message', 'tag_ID' ), $wp_http_referer ); // Also used by Edit Tags. diff --git a/src/wp-admin/includes/class-wp-links-list-table.php b/src/wp-admin/includes/class-wp-links-list-table.php index 5159c1c273e07..66c4990d0f7b7 100644 --- a/src/wp-admin/includes/class-wp-links-list-table.php +++ b/src/wp-admin/includes/class-wp-links-list-table.php @@ -50,7 +50,10 @@ public function ajax_user_can() { public function prepare_items() { global $cat_id, $s, $orderby, $order; - wp_reset_vars( array( 'action', 'cat_id', 'link_id', 'orderby', 'order', 's' ) ); + $cat_id = ! empty( $_REQUEST['cat_id'] ) ? absint( $_REQUEST['cat_id'] ) : 0; + $orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : ''; + $order = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : ''; + $s = ! empty( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : ''; $args = array( 'hide_invisible' => 0, diff --git a/src/wp-admin/includes/class-wp-ms-themes-list-table.php b/src/wp-admin/includes/class-wp-ms-themes-list-table.php index cc0206eec0522..96a1d99576cfe 100644 --- a/src/wp-admin/includes/class-wp-ms-themes-list-table.php +++ b/src/wp-admin/includes/class-wp-ms-themes-list-table.php @@ -99,7 +99,9 @@ public function ajax_user_can() { public function prepare_items() { global $status, $totals, $page, $orderby, $order, $s; - wp_reset_vars( array( 'orderby', 'order', 's' ) ); + $orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : ''; + $order = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : ''; + $s = ! empty( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : ''; $themes = array( /** diff --git a/src/wp-admin/includes/class-wp-plugin-install-list-table.php b/src/wp-admin/includes/class-wp-plugin-install-list-table.php index f3452a7d94248..21922b7943a89 100644 --- a/src/wp-admin/includes/class-wp-plugin-install-list-table.php +++ b/src/wp-admin/includes/class-wp-plugin-install-list-table.php @@ -92,7 +92,7 @@ public function prepare_items() { global $tabs, $tab, $paged, $type, $term; - wp_reset_vars( array( 'tab' ) ); + $tab = ! empty( $_REQUEST['tab'] ) ? sanitize_text_field( $_REQUEST['tab'] ) : ''; $paged = $this->get_pagenum(); diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index 4cc0132b6fa7d..db3148193b040 100644 --- a/src/wp-admin/includes/class-wp-plugins-list-table.php +++ b/src/wp-admin/includes/class-wp-plugins-list-table.php @@ -90,7 +90,8 @@ public function ajax_user_can() { public function prepare_items() { global $status, $plugins, $totals, $page, $orderby, $order, $s; - wp_reset_vars( array( 'orderby', 'order' ) ); + $orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : ''; + $order = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : ''; /** * Filters the full array of plugins to list in the Plugins list table. diff --git a/src/wp-admin/includes/class-wp-theme-install-list-table.php b/src/wp-admin/includes/class-wp-theme-install-list-table.php index 945fb6e9efe81..e273d4bc81bbd 100644 --- a/src/wp-admin/includes/class-wp-theme-install-list-table.php +++ b/src/wp-admin/includes/class-wp-theme-install-list-table.php @@ -36,7 +36,8 @@ public function prepare_items() { require ABSPATH . 'wp-admin/includes/theme-install.php'; global $tabs, $tab, $paged, $type, $theme_field_defaults; - wp_reset_vars( array( 'tab' ) ); + + $tab = ! empty( $_REQUEST['tab'] ) ? sanitize_text_field( $_REQUEST['tab'] ) : ''; $search_terms = array(); $search_string = ''; diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index 7794183d6ed58..ffe3801ce8345 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -575,7 +575,6 @@ function update_home_siteurl( $old_value, $value ) { } } - /** * Resets global variables based on $_GET and $_POST. * diff --git a/src/wp-admin/link-add.php b/src/wp-admin/link-add.php index d8c98bb5c7263..57450f0a529a9 100644 --- a/src/wp-admin/link-add.php +++ b/src/wp-admin/link-add.php @@ -17,7 +17,9 @@ $title = __( 'Add New Link' ); $parent_file = 'link-manager.php'; -wp_reset_vars( array( 'action', 'cat_id', 'link_id' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; +$cat_id = ! empty( $_REQUEST['cat_id'] ) ? absint( $_REQUEST['cat_id'] ) : 0; +$link_id = ! empty( $_REQUEST['link_id'] ) ? absint( $_REQUEST['link_id'] ) : 0; wp_enqueue_script( 'link' ); wp_enqueue_script( 'xfn' ); diff --git a/src/wp-admin/link.php b/src/wp-admin/link.php index f07cc5897d3a3..36a023a9c877e 100644 --- a/src/wp-admin/link.php +++ b/src/wp-admin/link.php @@ -12,7 +12,9 @@ /** Load WordPress Administration Bootstrap */ require_once __DIR__ . '/admin.php'; -wp_reset_vars( array( 'action', 'cat_id', 'link_id' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; +$cat_id = ! empty( $_REQUEST['cat_id'] ) ? absint( $_REQUEST['cat_id'] ) : 0; +$link_id = ! empty( $_REQUEST['link_id'] ) ? absint( $_REQUEST['link_id'] ) : 0; if ( ! current_user_can( 'manage_links' ) ) { wp_link_manager_disabled_message(); diff --git a/src/wp-admin/media.php b/src/wp-admin/media.php index 5b7ac353c694b..ab8952f8c1036 100644 --- a/src/wp-admin/media.php +++ b/src/wp-admin/media.php @@ -15,7 +15,7 @@ $parent_file = 'upload.php'; $submenu_file = 'upload.php'; -wp_reset_vars( array( 'action' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; switch ( $action ) { case 'editattachment': diff --git a/src/wp-admin/options-head.php b/src/wp-admin/options-head.php index d96978b438d2b..9dba3703c5ad7 100644 --- a/src/wp-admin/options-head.php +++ b/src/wp-admin/options-head.php @@ -8,7 +8,7 @@ * @subpackage Administration */ -wp_reset_vars( array( 'action' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; if ( isset( $_GET['updated'] ) && isset( $_GET['page'] ) ) { // For back-compat with plugins that don't use the Settings API and just set updated=1 in the redirect. diff --git a/src/wp-admin/options.php b/src/wp-admin/options.php index 33779a7492515..eb3a4d0abb48c 100644 --- a/src/wp-admin/options.php +++ b/src/wp-admin/options.php @@ -23,7 +23,8 @@ $this_file = 'options.php'; $parent_file = 'options-general.php'; -wp_reset_vars( array( 'action', 'option_page' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; +$option_page = ! empty( $_REQUEST['option_page'] ) ? sanitize_text_field( $_REQUEST['option_page'] ) : ''; $capability = 'manage_options'; diff --git a/src/wp-admin/post.php b/src/wp-admin/post.php index 17875cb3e541c..1230dda23da77 100644 --- a/src/wp-admin/post.php +++ b/src/wp-admin/post.php @@ -14,7 +14,7 @@ $parent_file = 'edit.php'; $submenu_file = 'edit.php'; -wp_reset_vars( array( 'action' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; if ( isset( $_GET['post'] ) && isset( $_POST['post_ID'] ) && (int) $_GET['post'] !== (int) $_POST['post_ID'] ) { wp_die( __( 'A post ID mismatch has been detected.' ), __( 'Sorry, you are not allowed to edit this item.' ), 400 ); diff --git a/src/wp-admin/revision.php b/src/wp-admin/revision.php index 72b8e74ae2a97..12ec2b6b1f0f3 100644 --- a/src/wp-admin/revision.php +++ b/src/wp-admin/revision.php @@ -21,14 +21,16 @@ * @global int $from The revision to compare from. * @global int $to Optional, required if revision missing. The revision to compare to. */ -wp_reset_vars( array( 'revision', 'action', 'from', 'to' ) ); -$revision_id = absint( $revision ); +$revision_id = ! empty( $_REQUEST['revision'] ) ? absint( $_REQUEST['revision'] ) : 0; +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; +$from = ! empty( $_REQUEST['from'] ) && is_numeric( $_REQUEST['from'] ) ? absint( $_REQUEST['from'] ) : null; +$to = ! empty( $_REQUEST['to'] ) && is_numeric( $_REQUEST['to'] ) ? absint( $_REQUEST['to'] ) : null; -$from = is_numeric( $from ) ? absint( $from ) : null; if ( ! $revision_id ) { - $revision_id = absint( $to ); + $revision_id = $to; } + $redirect = 'edit.php'; switch ( $action ) { diff --git a/src/wp-admin/site-health.php b/src/wp-admin/site-health.php index ededbf001b2a7..0fd7fef07a00a 100644 --- a/src/wp-admin/site-health.php +++ b/src/wp-admin/site-health.php @@ -9,7 +9,7 @@ /** WordPress Administration Bootstrap */ require_once __DIR__ . '/admin.php'; -wp_reset_vars( array( 'action' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; $tabs = array( /* translators: Tab heading for Site Health Status page. */ diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index dfbe69abb6ec4..bf869f4d8a886 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -56,7 +56,10 @@ '

' . __( 'Support forums' ) . '

' ); -wp_reset_vars( array( 'action', 'error', 'file', 'theme' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; +$theme = ! empty( $_REQUEST['theme'] ) ? sanitize_text_field( $_REQUEST['theme'] ) : ''; +$file = ! empty( $_REQUEST['file'] ) ? sanitize_text_field( $_REQUEST['file'] ) : ''; +$error = ! empty( $_REQUEST['error'] ); if ( $theme ) { $stylesheet = $theme; diff --git a/src/wp-admin/theme-install.php b/src/wp-admin/theme-install.php index 25212974d8e43..293d9a0ccae0b 100644 --- a/src/wp-admin/theme-install.php +++ b/src/wp-admin/theme-install.php @@ -10,7 +10,7 @@ require_once __DIR__ . '/admin.php'; require ABSPATH . 'wp-admin/includes/theme-install.php'; -wp_reset_vars( array( 'tab' ) ); +$tab = ! empty( $_REQUEST['tab'] ) ? sanitize_text_field( $_REQUEST['tab'] ) : ''; if ( ! current_user_can( 'install_themes' ) ) { wp_die( __( 'Sorry, you are not allowed to install themes on this site.' ) ); diff --git a/src/wp-admin/themes.php b/src/wp-admin/themes.php index 31a2e26d64794..4db4c5066cad0 100644 --- a/src/wp-admin/themes.php +++ b/src/wp-admin/themes.php @@ -215,7 +215,9 @@ } else { $themes = wp_prepare_themes_for_js( array( wp_get_theme() ) ); } -wp_reset_vars( array( 'theme', 'search' ) ); + +$theme = ! empty( $_REQUEST['theme'] ) ? sanitize_text_field( $_REQUEST['theme'] ) : ''; +$search = ! empty( $_REQUEST['search'] ) ? sanitize_text_field( $_REQUEST['search'] ) : ''; wp_localize_script( 'theme', diff --git a/src/wp-admin/user-edit.php b/src/wp-admin/user-edit.php index bbb321a272938..bbad60959cb64 100644 --- a/src/wp-admin/user-edit.php +++ b/src/wp-admin/user-edit.php @@ -12,9 +12,10 @@ /** WordPress Translation Installation API */ require_once ABSPATH . 'wp-admin/includes/translation-install.php'; -wp_reset_vars( array( 'action', 'user_id', 'wp_http_referer' ) ); +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; +$user_id = ! empty( $_REQUEST['user_id'] ) ? absint( $_REQUEST['user_id'] ) : 0; +$wp_http_referer = ! empty( $_REQUEST['wp_http_referer'] ) ? sanitize_text_field( $_REQUEST['wp_http_referer'] ) : ''; -$user_id = (int) $user_id; $current_user = wp_get_current_user(); if ( ! defined( 'IS_PROFILE_PAGE' ) ) { From ebc66bb9a13180d85f5a8b374d85ce14e0b7b2db Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 1 May 2024 18:54:36 +0000 Subject: [PATCH 04/21] Tests: Use `assertSame()` in `wp_validate_redirect()` tests. This ensures that not only the return values match the expected results, but also that their type is the same. Going forward, stricter type checking by using `assertSame()` should generally be preferred to `assertEquals()` where appropriate, to make the tests more reliable. Follow-up to [36444]. Props costdev. See #60706. git-svn-id: https://develop.svn.wordpress.org/trunk@58070 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/formatting/redirect.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/formatting/redirect.php b/tests/phpunit/tests/formatting/redirect.php index 4fdcf0f62175b..c4b1d682094f4 100644 --- a/tests/phpunit/tests/formatting/redirect.php +++ b/tests/phpunit/tests/formatting/redirect.php @@ -76,6 +76,9 @@ public function test_wp_sanitize_redirect_should_encode_spaces() { * @dataProvider data_wp_validate_redirect_valid_url * * @covers ::wp_validate_redirect + * + * @param string $url Redirect requested. + * @param string $expected Expected destination. */ public function test_wp_validate_redirect_valid_url( $url, $expected ) { $this->assertSame( $expected, wp_validate_redirect( $url ) ); @@ -101,15 +104,18 @@ public function data_wp_validate_redirect_valid_url() { * @dataProvider data_wp_validate_redirect_invalid_url * * @covers ::wp_validate_redirect + * + * @param string $url Redirect requested. + * @param string|false $expected Optional. Expected destination. Default false. */ - public function test_wp_validate_redirect_invalid_url( $url ) { - $this->assertEquals( false, wp_validate_redirect( $url, false ) ); + public function test_wp_validate_redirect_invalid_url( $url, $expected = false ) { + $this->assertSame( $expected, wp_validate_redirect( $url, false ) ); } public function data_wp_validate_redirect_invalid_url() { return array( // parse_url() fails. - array( '' ), + array( '', '' ), array( 'http://:' ), // Non-safelisted domain. @@ -179,6 +185,10 @@ public function data_wp_validate_redirect_invalid_url() { * @dataProvider data_wp_validate_redirect_relative_url * * @covers ::wp_validate_redirect + * + * @param string $current_uri Current URI (i.e. path and query string only). + * @param string $url Redirect requested. + * @param string $expected Expected destination. */ public function test_wp_validate_redirect_relative_url( $current_uri, $url, $expected ) { // Backup the global. From aca5616afd0e3c3aebfc8624d5e4d1890e99face Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 1 May 2024 23:42:24 +0000 Subject: [PATCH 05/21] Docs: Correct some docblock indentation. See #60699 git-svn-id: https://develop.svn.wordpress.org/trunk@58071 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-site-health.php | 10 +++++----- src/wp-includes/block-template-utils.php | 20 +++++++++---------- src/wp-includes/user.php | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index c0516096ed1fc..15996fd4209af 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3325,12 +3325,12 @@ private function check_for_page_caching() { * @since 6.1.0 * * @return WP_Error|array { - * Page cache detail or else a WP_Error if unable to determine. + * Page cache detail or else a WP_Error if unable to determine. * - * @type string $status Page cache status. Good, Recommended or Critical. - * @type bool $advanced_cache_present Whether page cache plugin is available or not. - * @type string[] $headers Client caching response headers detected. - * @type float $response_time Response time of site. + * @type string $status Page cache status. Good, Recommended or Critical. + * @type bool $advanced_cache_present Whether page cache plugin is available or not. + * @type string[] $headers Client caching response headers detected. + * @type float $response_time Response time of site. * } */ private function get_page_cache_detail() { diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index fa9e22dce000b..a7586061adcdd 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -249,16 +249,16 @@ function _get_block_templates_paths( $base_directory ) { * @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'. * @param string $slug Template slug. * @return array|null { - * Array with template metadata if $template_type is one of 'wp_template' or 'wp_template_part', - * null otherwise. - * - * @type string $slug Template slug. - * @type string $path Template file path. - * @type string $theme Theme slug. - * @type string $type Template type. - * @type string $area Template area. Only for 'wp_template_part'. - * @type string $title Optional. Template title. - * @type string[] $postTypes Optional. List of post types that the template supports. Only for 'wp_template'. + * Array with template metadata if $template_type is one of 'wp_template' or 'wp_template_part', + * null otherwise. + * + * @type string $slug Template slug. + * @type string $path Template file path. + * @type string $theme Theme slug. + * @type string $type Template type. + * @type string $area Template area. Only for 'wp_template_part'. + * @type string $title Optional. Template title. + * @type string[] $postTypes Optional. List of post types that the template supports. Only for 'wp_template'. * } */ function _get_block_template_file( $template_type, $slug ) { diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 301e8f0fcb9be..1f100db4f421d 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -3255,9 +3255,9 @@ function retrieve_password( $user_login = null ) { * @type string $message The body of the email. * @type string $headers The headers of the email. * } - * @type string $key The activation key. - * @type string $user_login The username for the user. - * @type WP_User $user_data WP_User object. + * @param string $key The activation key. + * @param string $user_login The username for the user. + * @param WP_User $user_data WP_User object. */ $notification_email = apply_filters( 'retrieve_password_notification_email', $defaults, $key, $user_login, $user_data ); From a8d5879867e03d62e3a91569aa18c84142e47c62 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Wed, 1 May 2024 23:43:11 +0000 Subject: [PATCH 06/21] HTML API: Fix context reset in html5lib test suite. The html5lib-tests suite parses tests from a number of files with a specific data format. It uses a dataProvider in a loop that yields test information. This relies on some variables being reset on each iteration. The context element has not properly reset on each iteration. The test specification describes the context element as follows: https://github.com/html5lib/html5lib-tests/blob/a9f44960a9fedf265093d22b2aa3c7ca123727b9/tree-construction/README.md > Then there *may* be a line that says "#document-fragment", which must be > followed by a newline (LF), followed by a string of characters that indicates > the context element, followed by a newline (LF). If the string of characters > starts with "svg ", the context element is in the SVG namespace and the > substring after "svg " is the local name. If the string of characters starts > with "math ", the context element is in the MathML namespace and the > substring after "math " is the local name. Otherwise, the context element is > in the HTML namespace and the string is the local name. If this line is > present the "#data" must be parsed using the HTML fragment parsing algorithm > with the context element as context. Without the proper reset of this value, a single context element would change subsequent tests, breaking the test suite. This patch adds the reset to ensure that the test suite works properly. Developed in https://github.com/WordPress/wordpress-develop/pull/6464 Discussed in https://core.trac.wordpress.org/ticket/61102 Fixes #61102. Props costdev, dmsnell, jonsurrell. git-svn-id: https://develop.svn.wordpress.org/trunk@58072 602fd350-edb4-49c9-b593-d223f7449a82 --- .../html-api/wpHtmlProcessorHtml5lib.php | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php index b20ba5d0ea345..c40481ac18e45 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorHtml5lib.php @@ -60,6 +60,7 @@ class Tests_HtmlApi_Html5lib extends WP_UnitTestCase { 'tests23/line0101' => 'Unimplemented: Reconstruction of active formatting elements.', 'tests25/line0169' => 'Bug.', 'tests26/line0263' => 'Bug: An active formatting element should be created for a trailing text node.', + 'tests7/line0354' => 'Bug.', 'tests8/line0001' => 'Bug.', 'tests8/line0020' => 'Bug.', 'tests8/line0037' => 'Bug.', @@ -155,12 +156,14 @@ private static function should_skip_test( $test_name, $expected_tree ): bool { /** * Generates the tree-like structure represented in the Html5lib tests. * - * @param string $fragment_context Context element in which to parse HTML, such as BODY or SVG. - * @param string $html Given test HTML. + * @param string|null $fragment_context Context element in which to parse HTML, such as BODY or SVG. + * @param string $html Given test HTML. * @return string|null Tree structure of parsed HTML, if supported, else null. */ - private static function build_tree_representation( $fragment_context, $html ) { - $processor = WP_HTML_Processor::create_fragment( $html, "<{$fragment_context}>" ); + private static function build_tree_representation( ?string $fragment_context, string $html ) { + $processor = $fragment_context + ? WP_HTML_Processor::create_fragment( $html, "<{$fragment_context}>" ) + : WP_HTML_Processor::create_fragment( $html ); if ( null === $processor ) { return null; } @@ -273,7 +276,7 @@ public static function parse_html5_dat_testfile( $filename ) { $line_number = 0; $test_html = ''; $test_dom = ''; - $test_context_element = 'body'; + $test_context_element = null; $test_line_number = 0; while ( false !== ( $line = fgets( $handle ) ) ) { @@ -294,9 +297,10 @@ public static function parse_html5_dat_testfile( $filename ) { } // Finish previous test. - $test_line_number = $line_number; - $test_html = ''; - $test_dom = ''; + $test_line_number = $line_number; + $test_html = ''; + $test_dom = ''; + $test_context_element = null; } $state = trim( substr( $line, 1 ) ); From ec1110c719252c69559c7d3b5709a6dc914fd27b Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 1 May 2024 23:59:03 +0000 Subject: [PATCH 07/21] Docs: Various docblock corrections. See #60699 git-svn-id: https://develop.svn.wordpress.org/trunk@58073 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/plugin-install.php | 2 +- src/wp-includes/block-bindings.php | 2 +- src/wp-includes/class-wp-block-bindings-registry.php | 2 +- src/wp-includes/class-wp-block-type.php | 6 +++--- src/wp-includes/class-wp-plugin-dependencies.php | 2 -- src/wp-includes/fonts.php | 6 ++++-- src/wp-includes/fonts/class-wp-font-utils.php | 4 ++-- tests/phpunit/tests/user/capabilities.php | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index a3afbcb8e9791..b8b72a4548a26 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -917,7 +917,7 @@ function install_plugin_information() { * } * @param bool $compatible_php The result of a PHP compatibility check. * @param bool $compatible_wp The result of a WP compatibility check. - * @return string $button The markup for the dependency row button. + * @return string The markup for the dependency row button. */ function wp_get_plugin_action_button( $name, $data, $compatible_php, $compatible_wp ) { $button = ''; diff --git a/src/wp-includes/block-bindings.php b/src/wp-includes/block-bindings.php index cee00e246d8c1..d7b2692081090 100644 --- a/src/wp-includes/block-bindings.php +++ b/src/wp-includes/block-bindings.php @@ -76,7 +76,7 @@ * The array of arguments that are used to register a source. * * @type string $label The label of the source. - * @type callback $get_value_callback A callback executed when the source is processed during block rendering. + * @type callable $get_value_callback A callback executed when the source is processed during block rendering. * The callback should have the following signature: * * `function ($source_args, $block_instance,$attribute_name): mixed` diff --git a/src/wp-includes/class-wp-block-bindings-registry.php b/src/wp-includes/class-wp-block-bindings-registry.php index e556982903e49..0d76b1a4c27bb 100644 --- a/src/wp-includes/class-wp-block-bindings-registry.php +++ b/src/wp-includes/class-wp-block-bindings-registry.php @@ -79,7 +79,7 @@ final class WP_Block_Bindings_Registry { * The array of arguments that are used to register a source. * * @type string $label The label of the source. - * @type callback $get_value_callback A callback executed when the source is processed during block rendering. + * @type callable $get_value_callback A callback executed when the source is processed during block rendering. * The callback should have the following signature: * * `function ($source_args, $block_instance,$attribute_name): mixed` diff --git a/src/wp-includes/class-wp-block-type.php b/src/wp-includes/class-wp-block-type.php index 8f88b1a6836a9..6916f6a462981 100644 --- a/src/wp-includes/class-wp-block-type.php +++ b/src/wp-includes/class-wp-block-type.php @@ -302,7 +302,7 @@ class WP_Block_Type { * Deprecated the `editor_script`, `script`, `view_script`, `editor_style`, and `style` properties. * @since 6.3.0 Added the `selectors` property. * @since 6.4.0 Added the `block_hooks` property. - * @since 6.5.0 Added the `view_style_handles` property. + * @since 6.5.0 Added the `allowed_blocks`, `variation_callback`, and `view_style_handles` properties. * * @see register_block_type() * @@ -621,7 +621,7 @@ public function get_variations() { * * @since 6.5.0 * - * @return array[] + * @return string[] */ public function get_uses_context() { /** @@ -629,7 +629,7 @@ public function get_uses_context() { * * @since 6.5.0 * - * @param array $uses_context Array of registered uses context for a block type. + * @param string[] $uses_context Array of registered uses context for a block type. * @param WP_Block_Type $block_type The full block type object. */ return apply_filters( 'get_block_type_uses_context', $this->uses_context, $this ); diff --git a/src/wp-includes/class-wp-plugin-dependencies.php b/src/wp-includes/class-wp-plugin-dependencies.php index 3f64e59c9ab47..d28b401c4d30e 100644 --- a/src/wp-includes/class-wp-plugin-dependencies.php +++ b/src/wp-includes/class-wp-plugin-dependencies.php @@ -553,8 +553,6 @@ protected static function get_plugins() { * Reads and stores dependency slugs from a plugin's 'Requires Plugins' header. * * @since 6.5.0 - * - * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. */ protected static function read_dependencies_from_plugin_headers() { self::$dependencies = array(); diff --git a/src/wp-includes/fonts.php b/src/wp-includes/fonts.php index 4806662160b81..dedf571489dab 100644 --- a/src/wp-includes/fonts.php +++ b/src/wp-includes/fonts.php @@ -15,11 +15,13 @@ * @param array[][] $fonts { * Optional. The font-families and their font faces. Default empty array. * - * @type array { + * @type array ...$0 { * An indexed or associative (keyed by font-family) array of font variations for this font-family. * Each font face has the following structure. * - * @type array { + * @type array ...$0 { + * The font face properties. + * * @type string $font-family The font-family property. * @type string|string[] $src The URL(s) to each resource containing the font data. * @type string $font-style Optional. The font-style property. Default 'normal'. diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index 6e914407487c6..2dc685ed0d5cd 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -221,8 +221,8 @@ public static function sanitize_from_schema( $tree, $schema ) { * * @since 6.5.0 * - * @param mixed $value The value to sanitize. - * @param mixed $sanitizer The sanitizer function to apply. + * @param mixed $value The value to sanitize. + * @param callable $sanitizer The sanitizer function to apply. * @return mixed The sanitized value. */ private static function apply_sanitizer( $value, $sanitizer ) { diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index 05721ce3f1852..473b0417ba198 100644 --- a/tests/phpunit/tests/user/capabilities.php +++ b/tests/phpunit/tests/user/capabilities.php @@ -454,7 +454,7 @@ public function test_all_caps_of_users_are_being_tested() { /** * Test the tests. The administrator role has all primitive capabilities, therefore the - * primitive capabilitity tests can be tested by checking that the list of tested + * primitive capability tests can be tested by checking that the list of tested * capabilities matches those of the administrator role. * * @group capTestTests From cc3055e5c4630a5f224592f0342e8ac7728729a4 Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Thu, 2 May 2024 06:59:30 +0000 Subject: [PATCH 08/21] Editor: Merge element style and classname generation to single filter. Fixes element classnames not being output when block attributes are filtered with `render_block_data`. Props aaronrobertshaw, isabel_brison, jorbin. Fixes #60681. git-svn-id: https://develop.svn.wordpress.org/trunk@58074 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-supports/elements.php | 145 ++++++++++++------ .../wpRenderElementsSupport.php | 11 +- .../wpRenderElementsSupportStyles.php | 2 +- 3 files changed, 108 insertions(+), 50 deletions(-) diff --git a/src/wp-includes/block-supports/elements.php b/src/wp-includes/block-supports/elements.php index e7fa76a9ae792..afa31eb618cf8 100644 --- a/src/wp-includes/block-supports/elements.php +++ b/src/wp-includes/block-supports/elements.php @@ -22,6 +22,8 @@ function wp_get_elements_class_name( $block ) { /** * Updates the block content with elements class names. * + * @deprecated 6.6.0 Generation of element class name is handled via `render_block_data` filter. + * * @since 5.8.0 * @since 6.4.0 Added support for button and heading element styling. * @access private @@ -31,18 +33,28 @@ function wp_get_elements_class_name( $block ) { * @return string Filtered block content. */ function wp_render_elements_support( $block_content, $block ) { - if ( ! $block_content || ! isset( $block['attrs']['style']['elements'] ) ) { - return $block_content; - } + _deprecated_function( __FUNCTION__, '6.6.0', 'wp_render_elements_class_name' ); + return $block_content; +} - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - if ( ! $block_type ) { - return $block_content; +/** + * Determines whether an elements class name should be added to the block. + * + * @since 6.6.0 + * @access private + * + * @param array $block Block object. + * @param array $options Per element type options e.g. whether to skip serialization. + * @return boolean Whether the block needs an elements class name. + */ +function wp_should_add_elements_class_name( $block, $options ) { + if ( ! isset( $block['attrs']['style']['elements'] ) ) { + return false; } $element_color_properties = array( 'button' => array( - 'skip' => wp_should_skip_block_supports_serialization( $block_type, 'color', 'button' ), + 'skip' => isset( $options['button']['skip'] ) ? $options['button']['skip'] : false, 'paths' => array( array( 'button', 'color', 'text' ), array( 'button', 'color', 'background' ), @@ -50,14 +62,14 @@ function wp_render_elements_support( $block_content, $block ) { ), ), 'link' => array( - 'skip' => wp_should_skip_block_supports_serialization( $block_type, 'color', 'link' ), + 'skip' => isset( $options['link']['skip'] ) ? $options['link']['skip'] : false, 'paths' => array( array( 'link', 'color', 'text' ), array( 'link', ':hover', 'color', 'text' ), ), ), 'heading' => array( - 'skip' => wp_should_skip_block_supports_serialization( $block_type, 'color', 'heading' ), + 'skip' => isset( $options['heading']['skip'] ) ? $options['heading']['skip'] : false, 'paths' => array( array( 'heading', 'color', 'text' ), array( 'heading', 'color', 'background' ), @@ -84,14 +96,6 @@ function wp_render_elements_support( $block_content, $block ) { ), ); - $skip_all_element_color_serialization = $element_color_properties['button']['skip'] && - $element_color_properties['link']['skip'] && - $element_color_properties['heading']['skip']; - - if ( $skip_all_element_color_serialization ) { - return $block_content; - } - $elements_style_attributes = $block['attrs']['style']['elements']; foreach ( $element_color_properties as $element_config ) { @@ -101,31 +105,16 @@ function wp_render_elements_support( $block_content, $block ) { foreach ( $element_config['paths'] as $path ) { if ( null !== _wp_array_get( $elements_style_attributes, $path, null ) ) { - /* - * It only takes a single custom attribute to require that the custom - * class name be added to the block, so once one is found there's no - * need to continue looking for others. - * - * As is done with the layout hook, this code assumes that the block - * contains a single wrapper and that it's the first element in the - * rendered output. That first element, if it exists, gets the class. - */ - $tags = new WP_HTML_Tag_Processor( $block_content ); - if ( $tags->next_tag() ) { - $tags->add_class( wp_get_elements_class_name( $block ) ); - } - - return $tags->get_updated_html(); + return true; } } } - // If no custom attributes were found then there's nothing to modify. - return $block_content; + return false; } /** - * Renders the elements stylesheet. + * Render the elements stylesheet and adds elements class name to block as required. * * In the case of nested blocks we want the parent element styles to be rendered before their descendants. * This solves the issue of an element (e.g.: link color) being styled in both the parent and a descendant: @@ -133,18 +122,36 @@ function wp_render_elements_support( $block_content, $block ) { * * @since 6.0.0 * @since 6.1.0 Implemented the style engine to generate CSS and classnames. + * @since 6.6.0 Element block support class and styles are generated via the `render_block_data` filter instead of `pre_render_block`. * @access private * - * @param string|null $pre_render The pre-rendered content. Default null. - * @param array $block The block being rendered. - * @return null + * @param array $parsed_block The parsed block. + * @return array The same parsed block with elements classname added if appropriate. */ -function wp_render_elements_support_styles( $pre_render, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $element_block_styles = isset( $block['attrs']['style']['elements'] ) ? $block['attrs']['style']['elements'] : null; +function wp_render_elements_support_styles( $parsed_block ) { + /* + * The generation of element styles and classname were moved to the + * `render_block_data` filter in 6.6.0 to avoid filtered attributes + * breaking the application of the elements CSS class. + * + * @see https://github.com/WordPress/gutenberg/pull/59535. + * + * The change in filter means, the argument types for this function + * have changed and require deprecating. + */ + if ( is_string( $parsed_block ) ) { + _deprecated_argument( + __FUNCTION__, + '6.6.0', + __( 'Use as a `pre_render_block` filter is deprecated. Use with `render_block_data` instead.' ) + ); + } + + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] ); + $element_block_styles = isset( $parsed_block['attrs']['style']['elements'] ) ? $parsed_block['attrs']['style']['elements'] : null; if ( ! $element_block_styles ) { - return null; + return $parsed_block; } $skip_link_color_serialization = wp_should_skip_block_supports_serialization( $block_type, 'color', 'link' ); @@ -155,11 +162,25 @@ function wp_render_elements_support_styles( $pre_render, $block ) { $skip_button_color_serialization; if ( $skips_all_element_color_serialization ) { - return null; + return $parsed_block; } - $class_name = wp_get_elements_class_name( $block ); + $options = array( + 'button' => array( 'skip' => $skip_button_color_serialization ), + 'link' => array( 'skip' => $skip_link_color_serialization ), + 'heading' => array( 'skip' => $skip_heading_color_serialization ), + ); + + if ( ! wp_should_add_elements_class_name( $parsed_block, $options ) ) { + return $parsed_block; + } + + $class_name = wp_get_elements_class_name( $parsed_block ); + $updated_class_name = isset( $parsed_block['attrs']['className'] ) ? $parsed_block['attrs']['className'] . " $class_name" : $class_name; + + _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); + // Generate element styles based on selector and store in style engine for enqueuing. $element_types = array( 'button' => array( 'selector' => ".$class_name .wp-element-button, .$class_name .wp-block-button__link", @@ -225,8 +246,38 @@ function wp_render_elements_support_styles( $pre_render, $block ) { } } - return null; + return $parsed_block; +} + +/** + * Ensure the elements block support class name generated, and added to + * block attributes, in the `render_block_data` filter gets applied to the + * block's markup. + * + * @see wp_render_elements_support_styles + * @since 6.6.0 + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * + * @return string Filtered block content. + */ +function wp_render_elements_class_name( $block_content, $block ) { + $class_string = $block['attrs']['className'] ?? ''; + preg_match( '/\bwp-elements-\S+\b/', $class_string, $matches ); + + if ( empty( $matches ) ) { + return $block_content; + } + + $tags = new WP_HTML_Tag_Processor( $block_content ); + + if ( $tags->next_tag() ) { + $tags->add_class( $matches[0] ); + } + + return $tags->get_updated_html(); } -add_filter( 'render_block', 'wp_render_elements_support', 10, 2 ); -add_filter( 'pre_render_block', 'wp_render_elements_support_styles', 10, 2 ); +add_filter( 'render_block', 'wp_render_elements_class_name', 10, 2 ); +add_filter( 'render_block_data', 'wp_render_elements_support_styles', 10, 1 ); diff --git a/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php b/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php index 791cec8a72704..503e320290c55 100644 --- a/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php +++ b/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php @@ -44,7 +44,7 @@ public function test_leaves_block_content_alone_when_block_type_not_registered() ); $block_markup = '

Hello WordPress!

'; - $actual = wp_render_elements_support( $block_markup, $block ); + $actual = wp_render_elements_class_name( $block_markup, $block ); $this->assertSame( $block_markup, $actual, 'Expected to leave block content unmodified, but found changes.' ); } @@ -90,7 +90,14 @@ public function test_elements_block_support_class( $color_settings, $elements_st ), ); - $actual = wp_render_elements_support( $block_markup, $block ); + /* + * To ensure a consistent elements class name it is generated within a + * `render_block_data` filter and stored in the `className` attribute. + * As a result, the block data needs to be passed through the same + * function for this test. + */ + $filtered_block = wp_render_elements_support_styles( $block ); + $actual = wp_render_elements_class_name( $block_markup, $filtered_block ); $this->assertMatchesRegularExpression( $expected_markup, diff --git a/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php index 3be0fa1e10a90..2ab7819c635e1 100644 --- a/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php @@ -59,7 +59,7 @@ public function test_elements_block_support_styles( $color_settings, $elements_s ), ); - wp_render_elements_support_styles( null, $block ); + wp_render_elements_support_styles( $block ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); $this->assertMatchesRegularExpression( From f1724c73a4c117fcafb899d78bb51fd6d026b373 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 2 May 2024 13:17:45 +0000 Subject: [PATCH 09/21] Docs: Various docblock improvements. See #60699 git-svn-id: https://develop.svn.wordpress.org/trunk@58075 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/misc.php | 16 ++++++++-------- src/wp-includes/block-bindings.php | 2 +- .../class-wp-block-bindings-registry.php | 6 +++--- .../class-wp-block-bindings-source.php | 3 +-- src/wp-includes/class-wp-script-modules.php | 11 +++++------ src/wp-includes/cron.php | 13 +++++++------ src/wp-includes/fonts/class-wp-font-face.php | 2 +- src/wp-includes/fonts/class-wp-font-utils.php | 2 +- src/wp-includes/option.php | 4 ++-- 9 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index ffe3801ce8345..06fc29444551f 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -556,7 +556,7 @@ function wp_print_plugin_file_tree( $tree, $label = '', $level = 2, $size = 1, $ } /** - * Flushes rewrite rules if siteurl, home or page_on_front changed. + * Flushes rewrite rules if `siteurl`, `home` or `page_on_front` changed. * * @since 2.1.0 * @@ -576,11 +576,11 @@ function update_home_siteurl( $old_value, $value ) { } /** - * Resets global variables based on $_GET and $_POST. + * Resets global variables based on `$_GET` and `$_POST`. * * This function resets global variables based on the names passed - * in the $vars array to the value of $_POST[$var] or $_GET[$var] or '' - * if neither is defined. + * in the `$vars` array to the value of `$_POST[$var]` or `$_GET[$var]` or an + * empty string if neither is defined. * * @since 2.0.0 * @@ -754,7 +754,7 @@ function set_screen_options() { /** * Filters a screen option value before it is set. * - * The filter can also be used to modify non-standard [items]_per_page + * The filter can also be used to modify non-standard `[items]_per_page` * settings. See the parent function for a full list of standard options. * * Returning false from the filter will skip saving the current option. @@ -1304,7 +1304,7 @@ function wp_refresh_metabox_loader_nonces( $response, $data ) { } /** - * Adds the latest Heartbeat and REST-API nonce to the Heartbeat response. + * Adds the latest Heartbeat and REST API nonce to the Heartbeat response. * * @since 5.0.0 * @@ -1398,11 +1398,11 @@ function wp_admin_canonical_url() { $filtered_url = remove_query_arg( $removable_query_args, $current_url ); /** - * Filters the admin canonical url value. + * Filters the admin canonical URL value. * * @since 6.5.0 * - * @param string $filtered_url The admin canonical url value. + * @param string $filtered_url The admin canonical URL value. */ $filtered_url = apply_filters( 'wp_admin_canonical_url', $filtered_url ); ?> diff --git a/src/wp-includes/block-bindings.php b/src/wp-includes/block-bindings.php index d7b2692081090..300b4ddde3b1d 100644 --- a/src/wp-includes/block-bindings.php +++ b/src/wp-includes/block-bindings.php @@ -87,7 +87,7 @@ * - @param string $attribute_name The name of an attribute . * The callback has a mixed return type; it may return a string to override * the block's original value, null, false to remove an attribute, etc. - * @type array $uses_context (optional) Array of values to add to block `uses_context` needed by the source. + * @type string[] $uses_context (optional) Array of values to add to block `uses_context` needed by the source. * } * @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure. */ diff --git a/src/wp-includes/class-wp-block-bindings-registry.php b/src/wp-includes/class-wp-block-bindings-registry.php index 0d76b1a4c27bb..278ec47de2881 100644 --- a/src/wp-includes/class-wp-block-bindings-registry.php +++ b/src/wp-includes/class-wp-block-bindings-registry.php @@ -36,7 +36,7 @@ final class WP_Block_Bindings_Registry { * Supported source properties that can be passed to the registered source. * * @since 6.5.0 - * @var array + * @var string[] */ private $allowed_source_properties = array( 'label', @@ -48,7 +48,7 @@ final class WP_Block_Bindings_Registry { * Supported blocks that can use the block bindings API. * * @since 6.5.0 - * @var array + * @var string[] */ private $supported_blocks = array( 'core/paragraph', @@ -90,7 +90,7 @@ final class WP_Block_Bindings_Registry { * - @param string $attribute_name The name of the target attribute. * The callback has a mixed return type; it may return a string to override * the block's original value, null, false to remove an attribute, etc. - * @type array $uses_context (optional) Array of values to add to block `uses_context` needed by the source. + * @type string[] $uses_context (optional) Array of values to add to block `uses_context` needed by the source. * } * @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure. */ diff --git a/src/wp-includes/class-wp-block-bindings-source.php b/src/wp-includes/class-wp-block-bindings-source.php index 036e17c3dd578..c1b479f20e928 100644 --- a/src/wp-includes/class-wp-block-bindings-source.php +++ b/src/wp-includes/class-wp-block-bindings-source.php @@ -36,7 +36,6 @@ final class WP_Block_Bindings_Source { */ public $label; - /** * The function used to get the value from the source. * @@ -49,7 +48,7 @@ final class WP_Block_Bindings_Source { * The context added to the blocks needed by the source. * * @since 6.5.0 - * @var array|null + * @var string[]|null */ public $uses_context = null; diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 89d12a6b3ff31..d391878d7c4a0 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -18,7 +18,7 @@ class WP_Script_Modules { * Holds the registered script modules, keyed by script module identifier. * * @since 6.5.0 - * @var array + * @var array[] */ private $registered = array(); @@ -274,7 +274,7 @@ private function get_import_map(): array { * * @since 6.5.0 * - * @return array Script modules marked for enqueue, keyed by script module identifier. + * @return array[] Script modules marked for enqueue, keyed by script module identifier. */ private function get_marked_for_enqueue(): array { $enqueued = array(); @@ -296,11 +296,10 @@ private function get_marked_for_enqueue(): array { * * @since 6.5.0 * - * @param string[] $ids The identifiers of the script modules for which to gather dependencies. - * @param array $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both. + * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both. * Default is both. - * @return array List of dependencies, keyed by script module identifier. + * @return array[] List of dependencies, keyed by script module identifier. */ private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) { return array_reduce( @@ -353,7 +352,7 @@ private function get_src( string $id ): string { * * @since 6.5.0 * - * @param string $src Module source url. + * @param string $src Module source URL. * @param string $id Module identifier. */ $src = apply_filters( 'script_module_loader_src', $src, $id ); diff --git a/src/wp-includes/cron.php b/src/wp-includes/cron.php index aadc22b7eb17d..9371515aba588 100644 --- a/src/wp-includes/cron.php +++ b/src/wp-includes/cron.php @@ -659,8 +659,9 @@ function wp_unschedule_hook( $hook, $wp_error = false ) { * process, causing the function to return the filtered value instead. * * For plugins replacing wp-cron, return the number of events successfully - * unscheduled (zero if no events were registered with the hook) or false - * if unscheduling one or more events fails. + * unscheduled (zero if no events were registered with the hook). If unscheduling + * one or more events fails then return either a WP_Error object or false depending + * on the value of the `$wp_error` parameter. * * @since 5.1.0 * @since 5.7.0 The `$wp_error` parameter was added, and a `WP_Error` object can now be returned. @@ -1047,14 +1048,14 @@ function _wp_cron() { * one is 'interval' and the other is 'display'. * * The 'interval' is a number in seconds of when the cron job should run. - * So for 'hourly' the time is `HOUR_IN_SECONDS` (60 * 60 or 3600). For 'monthly', - * the value would be `MONTH_IN_SECONDS` (30 * 24 * 60 * 60 or 2592000). + * So for 'hourly' the time is `HOUR_IN_SECONDS` (`60 * 60` or `3600`). For 'monthly', + * the value would be `MONTH_IN_SECONDS` (`30 * 24 * 60 * 60` or `2592000`). * * The 'display' is the description. For the 'monthly' key, the 'display' * would be `__( 'Once Monthly' )`. * - * For your plugin, you will be passed an array. You can easily add your - * schedule by doing the following. + * For your plugin, you will be passed an array. You can add your + * schedule by doing the following: * * // Filter parameter variable name is 'array'. * $array['monthly'] = array( diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 974e13ed1db8c..50d0268a70e8c 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -256,7 +256,7 @@ private function generate_style_element_attributes() { * * @since 6.4.0 * - * @param array $font_faces The font-faces to generate @font-face CSS styles. + * @param array[] $font_faces The font-faces to generate @font-face CSS styles. * @return string The `@font-face` CSS styles. */ private function get_css( $font_faces ) { diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index 2dc685ed0d5cd..0ec36abc3f64b 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -244,7 +244,7 @@ private static function apply_sanitizer( $value, $sanitizer ) { * * @access private * - * @return array A collection of mime types keyed by file extension. + * @return string[] A collection of mime types keyed by file extension. */ public static function get_allowed_font_mime_types() { $php_7_ttf_mime_type = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf'; diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 4c0071e23d670..0c5247f1a54f1 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -3034,7 +3034,7 @@ function filter_default_option( $default_value, $option, $passed_default ) { * * @since 6.6.0 * - * @return array The values that trigger autoloading. + * @return string[] The values that trigger autoloading. */ function wp_autoload_values_to_autoload() { $autoload_values = array( 'yes', 'on', 'auto-on', 'auto' ); @@ -3046,7 +3046,7 @@ function wp_autoload_values_to_autoload() { * * @since 6.6.0 * - * @param array $autoload_values Autoload values used to autoload option. + * @param string[] $autoload_values Autoload values used to autoload option. * Default list contains 'yes', 'on', 'auto-on', and 'auto'. */ $filtered_values = apply_filters( 'wp_autoload_values_to_autoload', $autoload_values ); From 53b75a10fce4e0bae751e5a17b4c72835b2dde83 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 May 2024 13:57:49 +0000 Subject: [PATCH 10/21] Build/Test Tools: Overhaul performance tests to improve stability and cover more scenarios. Simplifies the tests setup by leveraging a test matrix, improving maintenance and making it much easier to test more scenarios. With this change, tests are now also run with an external object cache (Memcached). Additional information such as memory usage and the number of database queries is now collected as well. Improves test setup and cleanup by disabling external HTTP requests and cron for the tests, as well as deleting expired transients and flushing the cache in-between. This should aid the test stability. When testing the previous commit / target branch, this now leverages the already built artifact from the build process workflow. Raw test results are now also uploaded as artifacts to aid debugging. Props swissspidy, adamsilverstein, joemcgill, mukesh27, desrosj, youknowriad, flixos90. Fixes #59900 git-svn-id: https://develop.svn.wordpress.org/trunk@58076 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/performance.yml | 170 +++++++----- tests/performance/compare-results.js | 242 ++++++++---------- tests/performance/config/global-setup.js | 4 +- .../config/performance-reporter.js | 74 +++++- tests/performance/log-results.js | 123 +++++---- tests/performance/playwright.config.js | 3 +- tests/performance/results.js | 42 --- tests/performance/specs/admin-l10n.test.js | 52 ---- tests/performance/specs/admin.test.js | 72 ++++-- .../specs/home-block-theme-l10n.test.js | 63 ----- .../specs/home-block-theme.test.js | 57 ----- .../specs/home-classic-theme-l10n.test.js | 62 ----- .../specs/home-classic-theme.test.js | 56 ---- tests/performance/specs/home.test.js | 79 ++++++ tests/performance/utils.js | 177 ++++++++++++- .../wp-content/mu-plugins/server-timing.php | 53 +++- 16 files changed, 683 insertions(+), 646 deletions(-) delete mode 100644 tests/performance/results.js delete mode 100644 tests/performance/specs/admin-l10n.test.js delete mode 100644 tests/performance/specs/home-block-theme-l10n.test.js delete mode 100644 tests/performance/specs/home-block-theme.test.js delete mode 100644 tests/performance/specs/home-classic-theme-l10n.test.js delete mode 100644 tests/performance/specs/home-classic-theme.test.js create mode 100644 tests/performance/specs/home.test.js diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 62663c730bf03..d79ae5f8012e7 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -66,38 +66,45 @@ jobs: # - Install WordPress. # - Install WordPress Importer plugin. # - Import mock data. + # - Deactivate WordPress Importer plugin. # - Update permalink structure. + # - Install additional languages. + # - Disable external HTTP requests. + # - Disable cron. + # - List defined constants. # - Install MU plugin. # - Run performance tests (current commit). - # - Print performance tests results. - # - Check out target commit (target branch or previous commit). - # - Switch Node.js versions if necessary. - # - Install npm dependencies. - # - Build WordPress. + # - Download previous build artifact (target branch or previous commit). + # - Download artifact. + # - Unzip the build. # - Run any database upgrades. + # - Flush cache. + # - Delete expired transients. # - Run performance tests (previous/target commit). - # - Print target performance tests results. - # - Reset to original commit. - # - Switch Node.js versions if necessary. - # - Install npm dependencies. # - Set the environment to the baseline version. # - Run any database upgrades. + # - Flush cache. + # - Delete expired transients. # - Run baseline performance tests. - # - Print baseline performance tests results. - # - Compare results with base. + # - Archive artifacts. + # - Compare results. # - Add workflow summary. # - Set the base sha. # - Set commit details. # - Publish performance results. # - Ensure version-controlled files are not modified or deleted. - # - Dispatch workflow run. performance: - name: Run performance tests + name: Run performance tests ${{ matrix.memcached && '(with memcached)' || '' }} runs-on: ubuntu-latest permissions: contents: read if: ${{ ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) && ! contains( github.event.before, '00000000' ) }} - + strategy: + fail-fast: false + matrix: + memcached: [ true, false ] + env: + LOCAL_PHP_MEMCACHED: ${{ matrix.memcached }} steps: - name: Configure environment variables run: | @@ -127,14 +134,17 @@ jobs: run: npm ci - name: Install Playwright browsers - run: npx playwright install --with-deps + run: npx playwright install --with-deps chromium - name: Build WordPress run: npm run build - name: Start Docker environment - run: | - npm run env:start + run: npm run env:start + + - name: Install object cache drop-in + if: ${{ matrix.memcached }} + run: cp src/wp-content/object-cache.php build/wp-content/object-cache.php - name: Log running Docker containers run: docker ps -a @@ -160,9 +170,11 @@ jobs: npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path=/var/www/${{ env.LOCAL_DIR }} rm themeunittestdata.wordpress.xml + - name: Deactivate WordPress Importer plugin + run: npm run env:cli -- plugin deactivate wordpress-importer --path=/var/www/${{ env.LOCAL_DIR }} + - name: Update permalink structure - run: | - npm run env:cli -- rewrite structure '/%year%/%monthnum%/%postname%/' --path=/var/www/${{ env.LOCAL_DIR }} + run: npm run env:cli -- rewrite structure '/%year%/%monthnum%/%postname%/' --path=/var/www/${{ env.LOCAL_DIR }} - name: Install additional languages run: | @@ -170,6 +182,17 @@ jobs: npm run env:cli -- language plugin install de_DE --all --path=/var/www/${{ env.LOCAL_DIR }} npm run env:cli -- language theme install de_DE --all --path=/var/www/${{ env.LOCAL_DIR }} + # Prevent background update checks from impacting test stability. + - name: Disable external HTTP requests + run: npm run env:cli -- config set WP_HTTP_BLOCK_EXTERNAL true --raw --type=constant --path=/var/www/${{ env.LOCAL_DIR }} + + # Prevent background tasks from impacting test stability. + - name: Disable cron + run: npm run env:cli -- config set DISABLE_WP_CRON true --raw --type=constant --path=/var/www/${{ env.LOCAL_DIR }} + + - name: List defined constants + run: npm run env:cli -- config list --path=/var/www/${{ env.LOCAL_DIR }} + - name: Install MU plugin run: | mkdir ./${{ env.LOCAL_DIR }}/wp-content/mu-plugins @@ -178,74 +201,93 @@ jobs: - name: Run performance tests (current commit) run: npm run test:performance - - name: Print performance tests results - run: node ./tests/performance/results.js + - name: Download previous build artifact (target branch or previous commit) + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: get-previous-build + with: + script: | + const artifacts = await github.rest.actions.listArtifactsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wordpress-build-' + process.env.TARGET_SHA, + }); + + const matchArtifact = artifacts.data.artifacts[0]; - - name: Check out target commit (target branch or previous commit) - run: | - if [[ -z "$TARGET_REF" ]]; then - git fetch -n origin $TARGET_SHA - else - git fetch -n origin $TARGET_REF - fi - git reset --hard $TARGET_SHA + if ( ! matchArtifact ) { + core.setFailed( 'No artifact found!' ); + return false; + } - - name: Set up Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version-file: '.nvmrc' - cache: npm + const download = await github.rest.actions.downloadArtifact( { + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + } ); - - name: Install npm dependencies - run: npm ci + const fs = require( 'fs' ); + fs.writeFileSync( '${{ github.workspace }}/before.zip', Buffer.from( download.data ) ) - - name: Build WordPress - run: npm run build + return true; + + - name: Unzip the build + if: ${{ steps.get-previous-build.outputs.result }} + run: | + unzip ${{ github.workspace }}/before.zip + unzip -o ${{ github.workspace }}/wordpress.zip - name: Run any database upgrades + if: ${{ steps.get-previous-build.outputs.result }} run: npm run env:cli -- core update-db --path=/var/www/${{ env.LOCAL_DIR }} - - name: Run target performance tests (base/previous commit) - env: - TEST_RESULTS_PREFIX: before - run: npm run test:performance + - name: Flush cache + if: ${{ steps.get-previous-build.outputs.result }} + run: npm run env:cli -- cache flush --path=/var/www/${{ env.LOCAL_DIR }} + + - name: Delete expired transients + if: ${{ steps.get-previous-build.outputs.result }} + run: npm run env:cli -- transient delete --expired --path=/var/www/${{ env.LOCAL_DIR }} - - name: Print target performance tests results + - name: Run target performance tests (previous/target commit) + if: ${{ steps.get-previous-build.outputs.result }} env: TEST_RESULTS_PREFIX: before - run: node ./tests/performance/results.js - - - name: Reset to original commit - run: git reset --hard $GITHUB_SHA - - - name: Set up Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Install npm dependencies - run: npm ci + run: npm run test:performance - name: Set the environment to the baseline version + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} run: | npm run env:cli -- core update --version=${{ env.BASE_TAG }} --force --path=/var/www/${{ env.LOCAL_DIR }} npm run env:cli -- core version --path=/var/www/${{ env.LOCAL_DIR }} - name: Run any database upgrades + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} run: npm run env:cli -- core update-db --path=/var/www/${{ env.LOCAL_DIR }} + - name: Flush cache + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + run: npm run env:cli -- cache flush --path=/var/www/${{ env.LOCAL_DIR }} + + - name: Delete expired transients + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + run: npm run env:cli -- transient delete --expired --path=/var/www/${{ env.LOCAL_DIR }} + - name: Run baseline performance tests + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} env: TEST_RESULTS_PREFIX: base run: npm run test:performance - - name: Print baseline performance tests results - env: - TEST_RESULTS_PREFIX: base - run: node ./tests/performance/results.js + - name: Archive artifacts + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + if: always() + with: + name: performance-artifacts${{ matrix.memcached && '-memcached' || '' }}-${{ github.run_id }} + path: artifacts + if-no-files-found: ignore - - name: Compare results with base + - name: Compare results run: node ./tests/performance/compare-results.js ${{ runner.temp }}/summary.md - name: Add workflow summary @@ -253,7 +295,7 @@ jobs: - name: Set the base sha # Only needed when publishing results. - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! matrix.memcached }} uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 id: base-sha with: @@ -264,7 +306,7 @@ jobs: - name: Set commit details # Only needed when publishing results. - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! matrix.memcached }} uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 id: commit-timestamp with: @@ -275,7 +317,7 @@ jobs: - name: Publish performance results # Only publish results on pushes to trunk. - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! matrix.memcached }} env: BASE_SHA: ${{ steps.base-sha.outputs.result }} COMMITTED_AT: ${{ steps.commit-timestamp.outputs.result }} diff --git a/tests/performance/compare-results.js b/tests/performance/compare-results.js index 6af85a2f122f8..c9d51e1a11117 100644 --- a/tests/performance/compare-results.js +++ b/tests/performance/compare-results.js @@ -3,193 +3,155 @@ /** * External dependencies. */ -const fs = require( 'node:fs' ); -const path = require( 'node:path' ); +const { readFileSync, writeFileSync, existsSync } = require( 'node:fs' ); +const { join } = require( 'node:path' ); /** * Internal dependencies */ -const { median } = require( './utils' ); - -/** - * Parse test files into JSON objects. - * - * @param {string} fileName The name of the file. - * @returns An array of parsed objects from each file. - */ -const parseFile = ( fileName ) => - JSON.parse( - fs.readFileSync( path.join( __dirname, '/specs/', fileName ), 'utf8' ) - ); - -// The list of test suites to log. -const testSuites = [ - 'admin', - 'admin-l10n', - 'home-block-theme', - 'home-block-theme-l10n', - 'home-classic-theme', - 'home-classic-theme-l10n', -]; - -// The current commit's results. -const testResults = Object.fromEntries( - testSuites - .filter( ( key ) => fs.existsSync( path.join( __dirname, '/specs/', `${ key }.test.results.json` ) ) ) - .map( ( key ) => [ key, parseFile( `${ key }.test.results.json` ) ] ) -); - -// The previous commit's results. -const prevResults = Object.fromEntries( - testSuites - .filter( ( key ) => fs.existsSync( path.join( __dirname, '/specs/', `before-${ key }.test.results.json` ) ) ) - .map( ( key ) => [ key, parseFile( `before-${ key }.test.results.json` ) ] ) -); +const { + median, + formatAsMarkdownTable, + formatValue, + linkToSha, + standardDeviation, + medianAbsoluteDeviation, + accumulateValues, +} = require( './utils' ); + +process.env.WP_ARTIFACTS_PATH ??= join( process.cwd(), 'artifacts' ); const args = process.argv.slice( 2 ); - const summaryFile = args[ 0 ]; /** - * Formats an array of objects as a Markdown table. - * - * For example, this array: - * - * [ - * { - * foo: 123, - * bar: 456, - * baz: 'Yes', - * }, - * { - * foo: 777, - * bar: 999, - * baz: 'No', - * } - * ] - * - * Will result in the following table: - * - * | foo | bar | baz | - * |-----|-----|-----| - * | 123 | 456 | Yes | - * | 777 | 999 | No | + * Parse test files into JSON objects. * - * @param {Array} rows Table rows. - * @returns {string} Markdown table content. + * @param {string} fileName The name of the file. + * @return {Array<{file: string, title: string, results: Record[]}>} Parsed object. */ -function formatAsMarkdownTable( rows ) { - let result = ''; - const headers = Object.keys( rows[ 0 ] ); - for ( const header of headers ) { - result += `| ${ header } `; - } - result += '|\n'; - for ( const header of headers ) { - result += '| ------ '; - } - result += '|\n'; - - for ( const row of rows ) { - for ( const value of Object.values( row ) ) { - result += `| ${ value } `; - } - result += '|\n'; +function parseFile( fileName ) { + const file = join( process.env.WP_ARTIFACTS_PATH, fileName ); + if ( ! existsSync( file ) ) { + return []; } - return result; + return JSON.parse( readFileSync( file, 'utf8' ) ); } /** - * Returns a Markdown link to a Git commit on the current GitHub repository. - * - * For example, turns `a5c3785ed8d6a35868bc169f07e40e889087fd2e` - * into (https://github.com/wordpress/wordpress-develop/commit/36fe58a8c64dcc83fc21bddd5fcf054aef4efb27)[36fe58a]. - * - * @param {string} sha Commit SHA. - * @return string Link + * @type {Array<{file: string, title: string, results: Record[]}>} */ -function linkToSha(sha) { - const repoName = process.env.GITHUB_REPOSITORY || 'wordpress/wordpress-develop'; +const beforeStats = parseFile( 'before-performance-results.json' ); - return `[${sha.slice(0, 7)}](https://github.com/${repoName}/commit/${sha})`; -} +/** + * @type {Array<{file: string, title: string, results: Record[]}>} + */ +const afterStats = parseFile( 'performance-results.json' ); -let summaryMarkdown = `# Performance Test Results\n\n`; - -if ( process.env.GITHUB_SHA ) { - summaryMarkdown += `🛎️ Performance test results for ${ linkToSha( process.env.GITHUB_SHA ) } are in!\n\n`; -} else { - summaryMarkdown += `🛎️ Performance test results are in!\n\n`; -} +let summaryMarkdown = `## Performance Test Results\n\n`; if ( process.env.TARGET_SHA ) { - summaryMarkdown += `This compares the results from this commit with the ones from ${ linkToSha( process.env.TARGET_SHA ) }.\n\n`; + if ( beforeStats.length > 0 ) { + if (process.env.GITHUB_SHA) { + summaryMarkdown += `This compares the results from this commit (${linkToSha( + process.env.GITHUB_SHA + )}) with the ones from ${linkToSha(process.env.TARGET_SHA)}.\n\n`; + } else { + summaryMarkdown += `This compares the results from this commit with the ones from ${linkToSha( + process.env.TARGET_SHA + )}.\n\n`; + } + } else { + summaryMarkdown += `Note: no build was found for the target commit ${linkToSha(process.env.TARGET_SHA)}. No comparison is possible.\n\n`; + } } +const numberOfRepetitions = afterStats[ 0 ].results.length; +const numberOfIterations = Object.values( afterStats[ 0 ].results[ 0 ] )[ 0 ] + .length; + +const repetitions = `${ numberOfRepetitions } ${ + numberOfRepetitions === 1 ? 'repetition' : 'repetitions' +}`; +const iterations = `${ numberOfIterations } ${ + numberOfIterations === 1 ? 'iteration' : 'iterations' +}`; + +summaryMarkdown += `All numbers are median values over ${ repetitions } with ${ iterations } each.\n\n`; + if ( process.env.GITHUB_SHA ) { summaryMarkdown += `**Note:** Due to the nature of how GitHub Actions work, some variance in the results is expected.\n\n`; } console.log( 'Performance Test Results\n' ); -console.log( 'Note: Due to the nature of how GitHub Actions work, some variance in the results is expected.\n' ); - -/** - * Nicely formats a given value. - * - * @param {string} metric Metric. - * @param {number} value - */ -function formatValue( metric, value) { - if ( null === value ) { - return 'N/A'; - } - if ( 'wpMemoryUsage' === metric ) { - return `${ ( value / Math.pow( 10, 6 ) ).toFixed( 2 ) } MB`; - } +console.log( + `All numbers are median values over ${ repetitions } with ${ iterations } each.\n` +); - return `${ value.toFixed( 2 ) } ms`; +if ( process.env.GITHUB_SHA ) { + console.log( + 'Note: Due to the nature of how GitHub Actions work, some variance in the results is expected.\n' + ); } -for ( const key of testSuites ) { - const current = testResults[ key ] || {}; - const prev = prevResults[ key ] || {}; - - const title = ( key.charAt( 0 ).toUpperCase() + key.slice( 1 ) ).replace( - /-+/g, - ' ' - ); +for ( const { title, results } of afterStats ) { + const prevStat = beforeStats.find( ( s ) => s.title === title ); + /** + * @type {Array>} + */ const rows = []; - for ( const [ metric, values ] of Object.entries( current ) ) { - const value = median( values ); - const prevValue = prev[ metric ] ? median( prev[ metric ] ) : null; + const newResults = accumulateValues( results ); + // Only do comparison if the number of results is the same. + const prevResults = + prevStat && prevStat.results.length === results.length + ? accumulateValues( prevStat.results ) + : {}; + + for ( const [ metric, values ] of Object.entries( newResults ) ) { + const prevValues = prevResults[ metric ] ? prevResults[ metric ] : null; - const delta = null !== prevValue ? value - prevValue : 0 + const value = median( values ); + const prevValue = prevValues ? median( prevValues ) : 0; + const delta = value - prevValue; const percentage = ( delta / value ) * 100; + const showDiff = + metric !== 'wpExtObjCache' && ! Number.isNaN( percentage ); + rows.push( { Metric: metric, - Before: formatValue( metric, prevValue ), + Before: prevValues ? formatValue( metric, prevValue ) : 'N/A', After: formatValue( metric, value ), - 'Diff abs.': formatValue( metric, delta ), - 'Diff %': `${ percentage.toFixed( 2 ) } %`, + 'Diff abs.': showDiff ? formatValue( metric, delta ) : '', + 'Diff %': showDiff ? `${ percentage.toFixed( 2 ) } %` : '', + STD: showDiff + ? formatValue( metric, standardDeviation( values ) ) + : '', + MAD: showDiff + ? formatValue( metric, medianAbsoluteDeviation( values ) ) + : '', } ); } + console.log( title ); if ( rows.length > 0 ) { - summaryMarkdown += `## ${ title }\n\n`; - summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`; - - console.log( title ); console.table( rows ); + } else { + console.log( '(no results)' ); } + + summaryMarkdown += `**${ title }**\n\n`; + summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`; } +writeFileSync( + join( process.env.WP_ARTIFACTS_PATH, '/performance-results.md' ), + summaryMarkdown +); + if ( summaryFile ) { - fs.writeFileSync( - summaryFile, - summaryMarkdown - ); + writeFileSync( summaryFile, summaryMarkdown ); } diff --git a/tests/performance/config/global-setup.js b/tests/performance/config/global-setup.js index f3a0a4f26a691..25e99a47d8457 100644 --- a/tests/performance/config/global-setup.js +++ b/tests/performance/config/global-setup.js @@ -30,9 +30,7 @@ async function globalSetup( config ) { await requestUtils.setupRest(); // Reset the test environment before running the tests. - await Promise.all( [ - requestUtils.activateTheme( 'twentytwentyone' ), - ] ); + await Promise.all( [ requestUtils.activateTheme( 'twentytwentyone' ) ] ); await requestContext.dispose(); } diff --git a/tests/performance/config/performance-reporter.js b/tests/performance/config/performance-reporter.js index e557faa135cbd..9617625eedc7c 100644 --- a/tests/performance/config/performance-reporter.js +++ b/tests/performance/config/performance-reporter.js @@ -1,19 +1,23 @@ /** * External dependencies */ -import { join, dirname, basename } from 'node:path'; -import { writeFileSync } from 'node:fs'; - -/** - * Internal dependencies - */ -import { getResultsFilename } from '../utils'; +import { join } from 'node:path'; +import { writeFileSync, existsSync, mkdirSync } from 'node:fs'; /** * @implements {import('@playwright/test/reporter').Reporter} */ class PerformanceReporter { /** + * + * @type {Record[];}>} + */ + allResults = {}; + + /** + * Called after a test has been finished in the worker process. + * + * Used to add test results to the final summary of all tests. * * @param {import('@playwright/test/reporter').TestCase} test * @param {import('@playwright/test/reporter').TestResult} result @@ -24,15 +28,59 @@ class PerformanceReporter { ); if ( performanceResults?.body ) { - writeFileSync( - join( - dirname( test.location.file ), - getResultsFilename( basename( test.location.file, '.js' ) ) - ), - performanceResults.body.toString( 'utf-8' ) + // 0 = empty, 1 = browser, 2 = file name, 3 = test suite name, 4 = test name. + const titlePath = test.titlePath(); + const title = `${ titlePath[ 3 ] } › ${ titlePath[ 4 ] }`; + + // results is an array in case repeatEach is > 1. + + this.allResults[ title ] ??= { + file: test.location.file, // Unused, but useful for debugging. + results: [], + }; + + this.allResults[ title ].results.push( + JSON.parse( performanceResults.body.toString( 'utf-8' ) ) ); } } + + /** + * Called after all tests have been run, or testing has been interrupted. + * + * Writes all raw numbers to a file for further processing, + * for example to compare with a previous run. + * + * @param {import('@playwright/test/reporter').FullResult} result + */ + onEnd( result ) { + const summary = []; + + for ( const [ title, { file, results } ] of Object.entries( + this.allResults + ) ) { + summary.push( { + file, + title, + results, + } ); + } + + if ( ! existsSync( process.env.WP_ARTIFACTS_PATH ) ) { + mkdirSync( process.env.WP_ARTIFACTS_PATH ); + } + + const prefix = process.env.TEST_RESULTS_PREFIX; + const fileNamePrefix = prefix ? `${ prefix }-` : ''; + + writeFileSync( + join( + process.env.WP_ARTIFACTS_PATH, + `${ fileNamePrefix }performance-results.json` + ), + JSON.stringify( summary, null, 2 ) + ); + } } export default PerformanceReporter; diff --git a/tests/performance/log-results.js b/tests/performance/log-results.js index 14c836ff671a4..66fe1e5291d09 100644 --- a/tests/performance/log-results.js +++ b/tests/performance/log-results.js @@ -1,71 +1,92 @@ #!/usr/bin/env node +/* + * Get the test results and format them in the way required by the API. + * + * Contains some backward compatibility logic for the original test suite format, + * see #59900 for details. + */ + /** * External dependencies. */ -const fs = require( 'fs' ); -const path = require( 'path' ); const https = require( 'https' ); -const [ token, branch, hash, baseHash, timestamp, host ] = process.argv.slice( 2 ); -const { median } = require( './utils' ); - -// The list of test suites to log. -const testSuites = [ - 'admin', - 'admin-l10n', - 'home-block-theme', - 'home-block-theme-l10n', - 'home-classic-theme', - 'home-classic-theme-l10n', -]; - -// A list of results to parse based on test suites. -const testResults = testSuites.map(( key ) => ({ - key, - file: `${ key }.test.results.json`, -})); - -// A list of base results to parse based on test suites. -const baseResults = testSuites.map(( key ) => ({ - key, - file: `base-${ key }.test.results.json`, -})); +const [ token, branch, hash, baseHash, timestamp, host ] = + process.argv.slice( 2 ); +const { median, parseFile, accumulateValues } = require( './utils' ); + +const testSuiteMap = { + 'Admin › Locale: en_US': 'admin', + 'Admin › Locale: de_DE': 'admin-l10n', + 'Front End › Theme: twentytwentyone, Locale: en_US': 'home-classic-theme', + 'Front End › Theme: twentytwentyone, Locale: de_DE': + 'home-classic-theme-l10n', + 'Front End › Theme: twentytwentythree, Locale: en_US': 'home-block-theme', + 'Front End › Theme: twentytwentythree, Locale: de_DE': + 'home-block-theme-l10n', +}; /** - * Parse test files into JSON objects. - * - * @param {string} fileName The name of the file. - * @returns An array of parsed objects from each file. + * @type {Array<{file: string, title: string, results: Record[]}>} */ -const parseFile = ( fileName ) => ( - JSON.parse( - fs.readFileSync( path.join( __dirname, '/specs/', fileName ), 'utf8' ) - ) -); +const afterStats = parseFile( 'performance-results.json' ); + +/** + * @type {Array<{file: string, title: string, results: Record[]}>} + */ +const baseStats = parseFile( 'base-performance-results.json' ); + +/** + * @type {Record} + */ +const metrics = {}; +/** + * @type {Record} + */ +const baseMetrics = {}; + +for ( const { title, results } of afterStats ) { + const testSuiteName = testSuiteMap[ title ]; + if ( ! testSuiteName ) { + continue; + } + + const baseStat = baseStats.find( ( s ) => s.title === title ); + + const currResults = accumulateValues( results ); + const baseResults = accumulateValues( baseStat.results ); + + for ( const [ metric, values ] of Object.entries( currResults ) ) { + metrics[ `${ testSuiteName }-${ metric }` ] = median( values ); + } + + for ( const [ metric, values ] of Object.entries( baseResults ) ) { + baseMetrics[ `${ testSuiteName }-${ metric }` ] = median( values ); + } +} + +process.exit( 0 ); /** * Gets the array of metrics from a list of results. * * @param {Object[]} results A list of results to format. - * @return {Object[]} Metrics. + * @return {Object} Metrics. */ const formatResults = ( results ) => { - return results.reduce( - ( result, { key, file } ) => { - return { - ...result, - ...Object.fromEntries( - Object.entries( - parseFile( file ) ?? {} - ).map( ( [ metric, value ] ) => [ + return results.reduce( ( result, { key, file } ) => { + return { + ...result, + ...Object.fromEntries( + Object.entries( parseFile( file ) ?? {} ).map( + ( [ metric, value ] ) => [ key + '-' + metric, - median ( value ), - ] ) - ), - }; - }, - {} - ); + median( value ), + ] + ) + ), + }; + }, {} ); }; const data = new TextEncoder().encode( diff --git a/tests/performance/playwright.config.js b/tests/performance/playwright.config.js index 1d2781f73ca4d..c4df0e2872953 100644 --- a/tests/performance/playwright.config.js +++ b/tests/performance/playwright.config.js @@ -23,9 +23,11 @@ const config = defineConfig( { forbidOnly: !! process.env.CI, workers: 1, retries: 0, + repeatEach: 2, timeout: parseInt( process.env.TIMEOUT || '', 10 ) || 600_000, // Defaults to 10 minutes. // Don't report slow test "files", as we will be running our tests in serial. reportSlowTests: null, + preserveOutput: 'never', webServer: { ...baseConfig.webServer, command: 'npm run env:start', @@ -37,4 +39,3 @@ const config = defineConfig( { } ); export default config; - diff --git a/tests/performance/results.js b/tests/performance/results.js deleted file mode 100644 index d9f981f5e7a0e..0000000000000 --- a/tests/performance/results.js +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env node - -/** - * External dependencies. - */ -const fs = require( 'node:fs' ); -const { join } = require( 'node:path' ); -const { median, getResultsFilename } = require( './utils' ); - -const testSuites = [ - 'admin', - 'admin-l10n', - 'home-classic-theme', - 'home-classic-theme-l10n', - 'home-block-theme', - 'home-block-theme-l10n', -]; - -console.log( '\n>> 🎉 Results 🎉 \n' ); - -for ( const testSuite of testSuites ) { - const resultsFileName = getResultsFilename( testSuite + '.test' ); - const resultsPath = join( __dirname, '/specs/', resultsFileName ); - fs.readFile( resultsPath, "utf8", ( err, data ) => { - if ( err ) { - console.log( "File read failed:", err ); - return; - } - const convertString = testSuite.charAt( 0 ).toUpperCase() + testSuite.slice( 1 ); - console.log( convertString.replace( /[-]+/g, " " ) + ':' ); - - tableData = JSON.parse( data ); - const rawResults = []; - - for ( var key in tableData ) { - if ( tableData.hasOwnProperty( key ) ) { - rawResults[ key ] = median( tableData[ key ] ); - } - } - console.table( rawResults ); - }); -} diff --git a/tests/performance/specs/admin-l10n.test.js b/tests/performance/specs/admin-l10n.test.js deleted file mode 100644 index a8c9be09975a8..0000000000000 --- a/tests/performance/specs/admin-l10n.test.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * WordPress dependencies - */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { camelCaseDashes } from '../utils'; - -const results = { - timeToFirstByte: [], -}; - -test.describe( 'Admin (L10N)', () => { - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - await requestUtils.updateSiteSettings( { - language: 'de_DE', - } ); - } ); - - test.afterAll( async ( { requestUtils }, testInfo ) => { - await testInfo.attach( 'results', { - body: JSON.stringify( results, null, 2 ), - contentType: 'application/json', - } ); - await requestUtils.updateSiteSettings( { - language: '', - } ); - } ); - - const iterations = Number( process.env.TEST_RUNS ); - for ( let i = 1; i <= iterations; i++ ) { - test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { - admin, - metrics, - } ) => { - await admin.visitAdminPage( '/' ); - - const serverTiming = await metrics.getServerTiming(); - - for ( const [ key, value ] of Object.entries( serverTiming ) ) { - results[ camelCaseDashes( key ) ] ??= []; - results[ camelCaseDashes( key ) ].push( value ); - } - - const ttfb = await metrics.getTimeToFirstByte(); - results.timeToFirstByte.push( ttfb ); - } ); - } -} ); diff --git a/tests/performance/specs/admin.test.js b/tests/performance/specs/admin.test.js index 98602291142dc..36bdd9c628aa1 100644 --- a/tests/performance/specs/admin.test.js +++ b/tests/performance/specs/admin.test.js @@ -12,35 +12,59 @@ const results = { timeToFirstByte: [], }; +const locales = [ 'en_US', 'de_DE' ]; + test.describe( 'Admin', () => { - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - } ); - - test.afterAll( async ( {}, testInfo ) => { - await testInfo.attach( 'results', { - body: JSON.stringify( results, null, 2 ), - contentType: 'application/json', - } ); - } ); + for ( const locale of locales ) { + test.describe( `Locale: ${ locale }`, () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + await requestUtils.updateSiteSettings( { + language: 'en_US' === locale ? '' : locale, + } ); + } ); - const iterations = Number( process.env.TEST_RUNS ); - for ( let i = 1; i <= iterations; i++ ) { - test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { - admin, - metrics, - } ) => { - await admin.visitAdminPage( '/' ); + test.afterAll( async ( { requestUtils }, testInfo ) => { + await testInfo.attach( 'results', { + body: JSON.stringify( results, null, 2 ), + contentType: 'application/json', + } ); - const serverTiming = await metrics.getServerTiming(); + await requestUtils.updateSiteSettings( { + language: '', + } ); - for ( const [ key, value ] of Object.entries( serverTiming ) ) { - results[ camelCaseDashes( key ) ] ??= []; - results[ camelCaseDashes( key ) ].push( value ); - } + results.timeToFirstByte = []; + } ); + + test.afterAll( async ( {}, testInfo ) => { + await testInfo.attach( 'results', { + body: JSON.stringify( results, null, 2 ), + contentType: 'application/json', + } ); + } ); - const ttfb = await metrics.getTimeToFirstByte(); - results.timeToFirstByte.push( ttfb ); + const iterations = Number( process.env.TEST_RUNS ); + for ( let i = 1; i <= iterations; i++ ) { + test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { + admin, + metrics, + } ) => { + await admin.visitAdminPage( '/' ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( + serverTiming + ) ) { + results[ camelCaseDashes( key ) ] ??= []; + results[ camelCaseDashes( key ) ].push( value ); + } + + const ttfb = await metrics.getTimeToFirstByte(); + results.timeToFirstByte.push( ttfb ); + } ); + } } ); } } ); diff --git a/tests/performance/specs/home-block-theme-l10n.test.js b/tests/performance/specs/home-block-theme-l10n.test.js deleted file mode 100644 index 591925056ffc5..0000000000000 --- a/tests/performance/specs/home-block-theme-l10n.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * WordPress dependencies - */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { camelCaseDashes } from '../utils'; - -const results = { - timeToFirstByte: [], - largestContentfulPaint: [], - lcpMinusTtfb: [], -}; - -test.describe( 'Front End - Twenty Twenty Three (L10N)', () => { - test.use( { - storageState: {}, // User will be logged out. - } ); - - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentythree' ); - await requestUtils.updateSiteSettings( { - language: 'de_DE', - } ); - } ); - - test.afterAll( async ( { requestUtils }, testInfo ) => { - await testInfo.attach( 'results', { - body: JSON.stringify( results, null, 2 ), - contentType: 'application/json', - } ); - await requestUtils.activateTheme( 'twentytwentyone' ); - await requestUtils.updateSiteSettings( { - language: '', - } ); - } ); - - const iterations = Number( process.env.TEST_RUNS ); - for ( let i = 1; i <= iterations; i++ ) { - test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { - page, - metrics, - } ) => { - await page.goto( '/' ); - - const serverTiming = await metrics.getServerTiming(); - - for ( const [ key, value ] of Object.entries( serverTiming ) ) { - results[ camelCaseDashes( key ) ] ??= []; - results[ camelCaseDashes( key ) ].push( value ); - } - - const ttfb = await metrics.getTimeToFirstByte(); - const lcp = await metrics.getLargestContentfulPaint(); - - results.largestContentfulPaint.push( lcp ); - results.timeToFirstByte.push( ttfb ); - results.lcpMinusTtfb.push( lcp - ttfb ); - } ); - } -} ); diff --git a/tests/performance/specs/home-block-theme.test.js b/tests/performance/specs/home-block-theme.test.js deleted file mode 100644 index 00bccc6996e35..0000000000000 --- a/tests/performance/specs/home-block-theme.test.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * WordPress dependencies - */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { camelCaseDashes } from '../utils'; - -const results = { - timeToFirstByte: [], - largestContentfulPaint: [], - lcpMinusTtfb: [], -}; - -test.describe( 'Front End - Twenty Twenty Three', () => { - test.use( { - storageState: {}, // User will be logged out. - } ); - - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentythree' ); - } ); - - test.afterAll( async ( { requestUtils }, testInfo ) => { - await testInfo.attach( 'results', { - body: JSON.stringify( results, null, 2 ), - contentType: 'application/json', - } ); - await requestUtils.activateTheme( 'twentytwentyone' ); - } ); - - const iterations = Number( process.env.TEST_RUNS ); - for ( let i = 1; i <= iterations; i++ ) { - test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { - page, - metrics, - } ) => { - await page.goto( '/' ); - - const serverTiming = await metrics.getServerTiming(); - - for ( const [ key, value ] of Object.entries( serverTiming ) ) { - results[ camelCaseDashes( key ) ] ??= []; - results[ camelCaseDashes( key ) ].push( value ); - } - - const ttfb = await metrics.getTimeToFirstByte(); - const lcp = await metrics.getLargestContentfulPaint(); - - results.largestContentfulPaint.push( lcp ); - results.timeToFirstByte.push( ttfb ); - results.lcpMinusTtfb.push( lcp - ttfb ); - } ); - } -} ); diff --git a/tests/performance/specs/home-classic-theme-l10n.test.js b/tests/performance/specs/home-classic-theme-l10n.test.js deleted file mode 100644 index e6f6e1cbb9818..0000000000000 --- a/tests/performance/specs/home-classic-theme-l10n.test.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * WordPress dependencies - */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { camelCaseDashes } from '../utils'; - -const results = { - timeToFirstByte: [], - largestContentfulPaint: [], - lcpMinusTtfb: [], -}; - -test.describe( 'Front End - Twenty Twenty One (L10N)', () => { - test.use( { - storageState: {}, // User will be logged out. - } ); - - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - await requestUtils.updateSiteSettings( { - language: 'de_DE', - } ); - } ); - - test.afterAll( async ( { requestUtils }, testInfo ) => { - await testInfo.attach( 'results', { - body: JSON.stringify( results, null, 2 ), - contentType: 'application/json', - } ); - await requestUtils.updateSiteSettings( { - language: '', - } ); - } ); - - const iterations = Number( process.env.TEST_RUNS ); - for ( let i = 1; i <= iterations; i++ ) { - test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { - page, - metrics, - } ) => { - await page.goto( '/' ); - - const serverTiming = await metrics.getServerTiming(); - - for ( const [ key, value ] of Object.entries( serverTiming ) ) { - results[ camelCaseDashes( key ) ] ??= []; - results[ camelCaseDashes( key ) ].push( value ); - } - - const ttfb = await metrics.getTimeToFirstByte(); - const lcp = await metrics.getLargestContentfulPaint(); - - results.largestContentfulPaint.push( lcp ); - results.timeToFirstByte.push( ttfb ); - results.lcpMinusTtfb.push( lcp - ttfb ); - } ); - } -} ); diff --git a/tests/performance/specs/home-classic-theme.test.js b/tests/performance/specs/home-classic-theme.test.js deleted file mode 100644 index a95e50fa06b9e..0000000000000 --- a/tests/performance/specs/home-classic-theme.test.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { test } from '@wordpress/e2e-test-utils-playwright'; - -/** - * Internal dependencies - */ -import { camelCaseDashes } from '../utils'; - -const results = { - timeToFirstByte: [], - largestContentfulPaint: [], - lcpMinusTtfb: [], -}; - -test.describe( 'Front End - Twenty Twenty One', () => { - test.use( { - storageState: {}, // User will be logged out. - } ); - - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'twentytwentyone' ); - } ); - - test.afterAll( async ( {}, testInfo ) => { - await testInfo.attach( 'results', { - body: JSON.stringify( results, null, 2 ), - contentType: 'application/json', - } ); - } ); - - const iterations = Number( process.env.TEST_RUNS ); - for ( let i = 1; i <= iterations; i++ ) { - test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { - page, - metrics, - } ) => { - await page.goto( '/' ); - - const serverTiming = await metrics.getServerTiming(); - - for ( const [ key, value ] of Object.entries( serverTiming ) ) { - results[ camelCaseDashes( key ) ] ??= []; - results[ camelCaseDashes( key ) ].push( value ); - } - - const ttfb = await metrics.getTimeToFirstByte(); - const lcp = await metrics.getLargestContentfulPaint(); - - results.largestContentfulPaint.push( lcp ); - results.timeToFirstByte.push( ttfb ); - results.lcpMinusTtfb.push( lcp - ttfb ); - } ); - } -} ); diff --git a/tests/performance/specs/home.test.js b/tests/performance/specs/home.test.js new file mode 100644 index 0000000000000..b88b6adb9f362 --- /dev/null +++ b/tests/performance/specs/home.test.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies + */ +import { test } from '@wordpress/e2e-test-utils-playwright'; + +/** + * Internal dependencies + */ +import { camelCaseDashes } from '../utils'; + +const results = { + timeToFirstByte: [], + largestContentfulPaint: [], + lcpMinusTtfb: [], +}; + +const themes = [ 'twentytwentyone', 'twentytwentythree', 'twentytwentyfour' ]; + +const locales = [ 'en_US', 'de_DE' ]; + +test.describe( 'Front End', () => { + test.use( { + storageState: {}, // User will be logged out. + } ); + + for ( const theme of themes ) { + for ( const locale of locales ) { + test.describe( `Theme: ${ theme }, Locale: ${ locale }`, () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( theme ); + await requestUtils.updateSiteSettings( { + language: 'en_US' === locale ? '' : locale, + } ); + } ); + + test.afterAll( async ( { requestUtils }, testInfo ) => { + await testInfo.attach( 'results', { + body: JSON.stringify( results, null, 2 ), + contentType: 'application/json', + } ); + + await requestUtils.updateSiteSettings( { + language: '', + } ); + + results.largestContentfulPaint = []; + results.timeToFirstByte = []; + results.lcpMinusTtfb = []; + } ); + + const iterations = Number( process.env.TEST_RUNS ); + for ( let i = 1; i <= iterations; i++ ) { + test( `Measure load time metrics (${ i } of ${ iterations })`, async ( { + page, + metrics, + } ) => { + await page.goto( '/' ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( + serverTiming + ) ) { + results[ camelCaseDashes( key ) ] ??= []; + results[ camelCaseDashes( key ) ].push( value ); + } + + const ttfb = await metrics.getTimeToFirstByte(); + const lcp = await metrics.getLargestContentfulPaint(); + + results.largestContentfulPaint.push( lcp ); + results.timeToFirstByte.push( ttfb ); + results.lcpMinusTtfb.push( lcp - ttfb ); + } ); + } + } ); + } + } +} ); diff --git a/tests/performance/utils.js b/tests/performance/utils.js index f56380e9c241a..4d023be586048 100644 --- a/tests/performance/utils.js +++ b/tests/performance/utils.js @@ -1,3 +1,26 @@ +/** + * External dependencies. + */ +const { readFileSync, existsSync } = require( 'node:fs' ); +const { join } = require( 'node:path' ); + +process.env.WP_ARTIFACTS_PATH ??= join( process.cwd(), 'artifacts' ); + +/** + * Parse test files into JSON objects. + * + * @param {string} fileName The name of the file. + * @return {Array<{file: string, title: string, results: Record[]}>} Parsed object. + */ +function parseFile( fileName ) { + const file = join( process.env.WP_ARTIFACTS_PATH, fileName ); + if ( ! existsSync( file ) ) { + return []; + } + + return JSON.parse( readFileSync( file, 'utf8' ) ); +} + /** * Computes the median number from an array numbers. * @@ -13,27 +36,157 @@ function median( array ) { : ( numbers[ mid - 1 ] + numbers[ mid ] ) / 2; } +function camelCaseDashes( str ) { + return str.replace( /-([a-z])/g, function ( g ) { + return g[ 1 ].toUpperCase(); + } ); +} + /** - * Gets the result file name. + * Formats an array of objects as a Markdown table. + * + * For example, this array: * - * @param {string} fileName File name. + * [ + * { + * foo: 123, + * bar: 456, + * baz: 'Yes', + * }, + * { + * foo: 777, + * bar: 999, + * baz: 'No', + * } + * ] * - * @return {string} Result file name. + * Will result in the following table: + * + * | foo | bar | baz | + * |-----|-----|-----| + * | 123 | 456 | Yes | + * | 777 | 999 | No | + * + * @param {Array} rows Table rows. + * @returns {string} Markdown table content. */ -function getResultsFilename( fileName ) { - const prefix = process.env.TEST_RESULTS_PREFIX; - const fileNamePrefix = prefix ? `${ prefix }-` : ''; - return `${fileNamePrefix + fileName}.results.json`; +function formatAsMarkdownTable( rows ) { + let result = ''; + + if ( ! rows.length ) { + return result; + } + + const headers = Object.keys( rows[ 0 ] ); + for ( const header of headers ) { + result += `| ${ header } `; + } + result += '|\n'; + for ( const header of headers ) { + result += '| ------ '; + } + result += '|\n'; + + for ( const row of rows ) { + for ( const value of Object.values( row ) ) { + result += `| ${ value } `; + } + result += '|\n'; + } + + return result; } -function camelCaseDashes( str ) { - return str.replace( /-([a-z])/g, function( g ) { - return g[ 1 ].toUpperCase(); - } ); +/** + * Nicely formats a given value. + * + * @param {string} metric Metric. + * @param {number} value + */ +function formatValue( metric, value ) { + if ( null === value ) { + return 'N/A'; + } + + if ( 'wpMemoryUsage' === metric ) { + return `${ ( value / Math.pow( 10, 6 ) ).toFixed( 2 ) } MB`; + } + + if ( 'wpExtObjCache' === metric ) { + return 1 === value ? 'yes' : 'no'; + } + + if ( 'wpDbQueries' === metric ) { + return value; + } + + return `${ value.toFixed( 2 ) } ms`; +} + +/** + * Returns a Markdown link to a Git commit on the current GitHub repository. + * + * For example, turns `a5c3785ed8d6a35868bc169f07e40e889087fd2e` + * into (https://github.com/wordpress/wordpress-develop/commit/36fe58a8c64dcc83fc21bddd5fcf054aef4efb27)[36fe58a]. + * + * @param {string} sha Commit SHA. + * @return string Link + */ +function linkToSha( sha ) { + const repoName = + process.env.GITHUB_REPOSITORY || 'wordpress/wordpress-develop'; + + return `[${ sha.slice( + 0, + 7 + ) }](https://github.com/${ repoName }/commit/${ sha })`; +} + +function standardDeviation( array = [] ) { + if ( ! array.length ) { + return 0; + } + + const mean = array.reduce( ( a, b ) => a + b ) / array.length; + return Math.sqrt( + array + .map( ( x ) => Math.pow( x - mean, 2 ) ) + .reduce( ( a, b ) => a + b ) / array.length + ); +} + +function medianAbsoluteDeviation( array = [] ) { + if ( ! array.length ) { + return 0; + } + + const med = median( array ); + return median( array.map( ( a ) => Math.abs( a - med ) ) ); +} + +/** + * + * @param {Array>} results + * @returns {Record} + */ +function accumulateValues( results ) { + return results.reduce( ( acc, result ) => { + for ( const [ metric, values ] of Object.entries( result ) ) { + acc[ metric ] = acc[ metric ] ?? []; + acc[ metric ].push( ...values ); + } + return acc; + }, {} ); } module.exports = { + parseFile, median, - getResultsFilename, camelCaseDashes, + formatAsMarkdownTable, + formatValue, + linkToSha, + standardDeviation, + medianAbsoluteDeviation, + accumulateValues, }; diff --git a/tests/performance/wp-content/mu-plugins/server-timing.php b/tests/performance/wp-content/mu-plugins/server-timing.php index 53f83fea796c3..abf166fcc8e73 100644 --- a/tests/performance/wp-content/mu-plugins/server-timing.php +++ b/tests/performance/wp-content/mu-plugins/server-timing.php @@ -4,7 +4,7 @@ 'template_include', static function ( $template ) { - global $timestart; + global $timestart, $wpdb; $server_timing_values = array(); $template_start = microtime( true ); @@ -15,10 +15,7 @@ static function ( $template ) { add_action( 'shutdown', - static function () use ( $server_timing_values, $template_start ) { - - global $timestart; - + static function () use ( $server_timing_values, $template_start, $wpdb ) { $output = ob_get_clean(); $server_timing_values['template'] = microtime( true ) - $template_start; @@ -30,7 +27,9 @@ static function () use ( $server_timing_values, $template_start ) { * any numeric value can actually be passed. * This is a nice little trick as it allows to easily get this information in JS. */ - $server_timing_values['memory-usage'] = memory_get_usage(); + $server_timing_values['memory-usage'] = memory_get_usage(); + $server_timing_values['db-queries'] = $wpdb->num_queries; + $server_timing_values['ext-obj-cache'] = wp_using_ext_object_cache() ? 1 : 0; $header_values = array(); foreach ( $server_timing_values as $slug => $value ) { @@ -50,3 +49,45 @@ static function () use ( $server_timing_values, $template_start ) { }, PHP_INT_MAX ); + +add_action( + 'admin_init', + static function () { + global $timestart, $wpdb; + + ob_start(); + + add_action( + 'shutdown', + static function () use ( $wpdb, $timestart ) { + $output = ob_get_clean(); + + $server_timing_values = array(); + + $server_timing_values['total'] = microtime( true ) - $timestart; + + /* + * While values passed via Server-Timing are intended to be durations, + * any numeric value can actually be passed. + * This is a nice little trick as it allows to easily get this information in JS. + */ + $server_timing_values['memory-usage'] = memory_get_usage(); + $server_timing_values['db-queries'] = $wpdb->num_queries; + $server_timing_values['ext-obj-cache'] = wp_using_ext_object_cache() ? 1 : 0; + + $header_values = array(); + foreach ( $server_timing_values as $slug => $value ) { + if ( is_float( $value ) ) { + $value = round( $value * 1000.0, 2 ); + } + $header_values[] = sprintf( 'wp-%1$s;dur=%2$s', $slug, $value ); + } + header( 'Server-Timing: ' . implode( ', ', $header_values ) ); + + echo $output; + }, + PHP_INT_MIN + ); + }, + PHP_INT_MAX +); From 73b7808db1865a1690ae035fa422cf9abe207c54 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 May 2024 14:40:30 +0000 Subject: [PATCH 11/21] Build/Test Tools: Fix performance tests logging script after [58076]. Removes some unintended debug cruft, whoops! See #59900. git-svn-id: https://develop.svn.wordpress.org/trunk@58077 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/performance/log-results.js | 39 +++++++++++--------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/tests/performance/log-results.js b/tests/performance/log-results.js index 66fe1e5291d09..bf62721c1bfd8 100644 --- a/tests/performance/log-results.js +++ b/tests/performance/log-results.js @@ -31,15 +31,26 @@ const testSuiteMap = { */ const afterStats = parseFile( 'performance-results.json' ); +if ( ! afterStats.length ) { + console.error( 'No results file found' ); + process.exit( 1 ); +} + /** * @type {Array<{file: string, title: string, results: Record[]}>} */ const baseStats = parseFile( 'base-performance-results.json' ); +if ( ! baseStats.length ) { + console.error( 'No base results file found' ); + process.exit( 1 ); +} + /** * @type {Record} */ const metrics = {}; + /** * @type {Record} */ @@ -65,38 +76,14 @@ for ( const { title, results } of afterStats ) { } } -process.exit( 0 ); - -/** - * Gets the array of metrics from a list of results. - * - * @param {Object[]} results A list of results to format. - * @return {Object} Metrics. - */ -const formatResults = ( results ) => { - return results.reduce( ( result, { key, file } ) => { - return { - ...result, - ...Object.fromEntries( - Object.entries( parseFile( file ) ?? {} ).map( - ( [ metric, value ] ) => [ - key + '-' + metric, - median( value ), - ] - ) - ), - }; - }, {} ); -}; - const data = new TextEncoder().encode( JSON.stringify( { branch, hash, baseHash, timestamp: parseInt( timestamp, 10 ), - metrics: formatResults( testResults ), - baseMetrics: formatResults( baseResults ), + metrics: metrics, + baseMetrics: baseMetrics, } ) ); From 91c546d47a3fd8a04eaa5b624ae2b85784a50980 Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Thu, 2 May 2024 16:01:29 +0000 Subject: [PATCH 12/21] REST API: Return empty object when no fallback templates are found (wp/v2/templates/lookup) This prevents a number of php notices that are surfaced due to the endpoint being called on load of the post editor even when there are no templates. Props grantmkin, CookiesForDevo, britner, wildworks, jorbin. Fixes #60909. git-svn-id: https://develop.svn.wordpress.org/trunk@58079 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-templates-controller.php | 3 ++- .../tests/rest-api/wpRestTemplatesController.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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 9eb64707aea07..1c2a7697c4ec0 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 @@ -165,7 +165,8 @@ public function get_template_fallback( $request ) { array_shift( $hierarchy ); } while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) ); - $response = $this->prepare_item_for_response( $fallback_template, $request ); + // To maintain original behavior, return an empty object rather than a 404 error when no template is found. + $response = $fallback_template ? $this->prepare_item_for_response( $fallback_template, $request ) : new stdClass(); return rest_ensure_response( $response ); } diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index 50678a686d561..376daffee2989 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -909,6 +909,19 @@ public function test_get_template_fallback() { $this->assertSame( 'index', $response->get_data()['slug'], 'Should fallback to `index.html` when ignore_empty is `true`.' ); } + /** + * @ticket 60909 + * @covers WP_REST_Templates_Controller::get_template_fallback + */ + public function test_get_template_fallback_not_found() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/lookup' ); + $request->set_param( 'slug', 'not-found' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( new stdClass(), $data, 'Response should be an empty object when a fallback template is not found.' ); + } + /** * @ticket 57851 * From 530b73ca94947704dd8a044e71e2b7e12f92ed5d Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Thu, 2 May 2024 17:03:36 +0000 Subject: [PATCH 13/21] Plugins: Show an admin notice on successful activation. Plugin activation on the Plugins > Add New screen is performed using AJAX, no longer performing redirects. This means that users will not see a newly activated plugin's menu items, admin notices, or other UI elements until the user refreshes or navigates to another screen. Without adequate messaging and direction, users may be unsure of what to do next. This shows an admin notice when a plugin is activated from its plugin card or modal, informing the user that the plugin was activated, and that some changes may not occur until they refresh the page. Follow-up to [57545]. Props costdev, jorbin, jeherve, flixos90, joedolson, ironprogrammer, audrasjb, alanfuller, kevinwhoffman, devsahadat, afragen, adrianduffell, azaozz, jason_the_adams, JeffPaul, webdevmattcrom, DrewAPicture, justlevine, stevejonesdev, benlk, roytanck. Fixes #60992. See #22316. git-svn-id: https://develop.svn.wordpress.org/trunk@58081 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/wp/updates.js | 71 ++++++++++++++++++++++++++++------ src/wp-admin/css/common.css | 19 +++++++++ 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index 048ed9a0486fe..8abe2822fd2b8 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -1105,21 +1105,33 @@ * * @since 6.5.0 * - * @param {Object} response Response from the server. - * @param {string} response.slug Slug of the activated plugin. - * @param {string} response.pluginName Name of the activated plugin. - * @param {string} response.plugin The plugin file, relative to the plugins directory. + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the activated plugin. + * @param {string} response.pluginName Name of the activated plugin. + * @param {string} response.plugin The plugin file, relative to the plugins directory. */ wp.updates.activatePluginSuccess = function( response ) { var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ), + isInModal = 'plugin-information-footer' === $message.parent().attr( 'id' ), buttonText = _x( 'Activated!', 'plugin' ), ariaLabel = sprintf( /* translators: %s: The plugin name. */ '%s activated successfully.', response.pluginName - ); + ), + noticeData = { + id: 'plugin-activated-successfully', + className: 'notice-success', + message: sprintf( + /* translators: %s: The refresh link's attributes. */ + __( 'Plugin activated. Some changes may not occur until you refresh the page. Refresh Now' ), + 'href="#" class="button button-secondary refresh-page"' + ), + slug: response.slug + }, + noticeTarget; - wp.a11y.speak( __( 'Activation completed successfully.' ) ); + wp.a11y.speak( __( 'Activation completed successfully. Some changes may not occur until you refresh the page.' ) ); $document.trigger( 'wp-plugin-activate-success', response ); $message @@ -1128,7 +1140,7 @@ .attr( 'aria-label', ariaLabel ) .text( buttonText ); - if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { + if ( isInModal ) { wp.updates.setCardButtonStatus( { status: 'activated-plugin', @@ -1139,13 +1151,26 @@ ariaLabel: ariaLabel } ); + + // Add a notice to the modal's footer. + $message.replaceWith( wp.updates.adminNotice( noticeData ) ); + + // Send notice information back to the parent screen. + noticeTarget = window.parent === window ? null : window.parent; + $.support.postMessage = !! window.postMessage; + if ( false !== $.support.postMessage && null !== noticeTarget && -1 === window.parent.location.pathname.indexOf( 'index.php' ) ) { + noticeTarget.postMessage( + JSON.stringify( noticeData ), + window.location.origin + ); + } + } else { + // Add a notice to the top of the screen. + wp.updates.addAdminNotice( noticeData ); } setTimeout( function() { - $message.removeClass( 'activated-message' ) - .text( _x( 'Active', 'plugin' ) ); - - if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { + if ( isInModal ) { wp.updates.setCardButtonStatus( { status: 'plugin-active', @@ -1159,6 +1184,8 @@ ) } ); + } else { + $message.removeClass( 'activated-message' ).text( _x( 'Active', 'plugin' ) ); } }, 1000 ); }; @@ -3254,6 +3281,11 @@ return; } + if ( 'undefined' !== typeof message.id && 'plugin-activated-successfully' === message.id ) { + wp.updates.addAdminNotice( message ); + return; + } + if ( 'undefined' !== typeof message.status && 'undefined' !== typeof message.slug && @@ -3486,5 +3518,22 @@ } ); } ); + + /** + * Click handler for page refresh link. + * + * @since 6.5.3 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.refresh-page', function( event ) { + event.preventDefault(); + + if ( window.parent === window ) { + window.location.reload(); + } else { + window.parent.location.reload(); + } + } ); } ); })( jQuery, window.wp, window._wpUpdatesSettings ); diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 53151a4f845df..08298452f0d9a 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1513,6 +1513,25 @@ div.error { margin-top: -5px; } +#plugin-information-footer #plugin-activated-successfully { + margin-bottom: 0; +} + +#plugin-information-footer #plugin-activated-successfully p { + display: flex; + gap: 1em; + align-items: center; + justify-content: space-between; + margin: 0; +} + +#plugin-information-footer #plugin-activated-successfully .refresh-page { + flex-grow: 0; + line-height: 2.15384615; + min-height: 0; + margin-bottom: 0; +} + .update-message p:before, .updating-message p:before, .updated-message p:before, From e147f6d67b8144a9b6de8dd43dbd82bf329d1867 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 2 May 2024 17:18:53 +0000 Subject: [PATCH 14/21] Docs: Add missing documentation for various upgrade/install class methods. Follow-up to [13602], [13686], [14879], [25806], [28495], [32655], [48661], [53952]. Props yagniksangani, audrasjb, SergeyBiryukov. Fixes #61124. git-svn-id: https://develop.svn.wordpress.org/trunk@58082 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-bulk-plugin-upgrader-skin.php | 17 ++++++ .../class-bulk-theme-upgrader-skin.php | 17 ++++++ .../includes/class-bulk-upgrader-skin.php | 52 +++++++++++++++++++ .../class-language-pack-upgrader-skin.php | 18 +++++++ .../includes/class-plugin-installer-skin.php | 6 +++ .../includes/class-theme-installer-skin.php | 6 +++ .../includes/class-wp-upgrader-skin.php | 23 +++++--- 7 files changed, 133 insertions(+), 6 deletions(-) diff --git a/src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php b/src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php index 7cbf334ee2fa2..bb62928fa6c2e 100644 --- a/src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php +++ b/src/wp-admin/includes/class-bulk-plugin-upgrader-skin.php @@ -23,10 +23,16 @@ class Bulk_Plugin_Upgrader_Skin extends Bulk_Upgrader_Skin { * The Plugin_Upgrader::bulk_upgrade() method will fill this in * with info retrieved from the get_plugin_data() function. * + * @since 3.0.0 * @var array Plugin data. Values will be empty if not supplied by the plugin. */ public $plugin_info = array(); + /** + * Sets up the strings used in the update process. + * + * @since 3.0.0 + */ public function add_strings() { parent::add_strings(); /* translators: 1: Plugin name, 2: Number of the plugin, 3: Total number of plugins being updated. */ @@ -34,6 +40,10 @@ public function add_strings() { } /** + * Performs an action before a bulk plugin update. + * + * @since 3.0.0 + * * @param string $title */ public function before( $title = '' ) { @@ -41,6 +51,10 @@ public function before( $title = '' ) { } /** + * Performs an action following a bulk plugin update. + * + * @since 3.0.0 + * * @param string $title */ public function after( $title = '' ) { @@ -49,6 +63,9 @@ public function after( $title = '' ) { } /** + * Displays the footer following the bulk update process. + * + * @since 3.0.0 */ public function bulk_footer() { parent::bulk_footer(); diff --git a/src/wp-admin/includes/class-bulk-theme-upgrader-skin.php b/src/wp-admin/includes/class-bulk-theme-upgrader-skin.php index 8ec3bbf5f5928..f2b9b95025d3c 100644 --- a/src/wp-admin/includes/class-bulk-theme-upgrader-skin.php +++ b/src/wp-admin/includes/class-bulk-theme-upgrader-skin.php @@ -24,10 +24,16 @@ class Bulk_Theme_Upgrader_Skin extends Bulk_Upgrader_Skin { * with info retrieved from the Theme_Upgrader::theme_info() method, * which in turn calls the wp_get_theme() function. * + * @since 3.0.0 * @var WP_Theme|false The theme's info object, or false. */ public $theme_info = false; + /** + * Sets up the strings used in the update process. + * + * @since 3.0.0 + */ public function add_strings() { parent::add_strings(); /* translators: 1: Theme name, 2: Number of the theme, 3: Total number of themes being updated. */ @@ -35,6 +41,10 @@ public function add_strings() { } /** + * Performs an action before a bulk theme update. + * + * @since 3.0.0 + * * @param string $title */ public function before( $title = '' ) { @@ -42,6 +52,10 @@ public function before( $title = '' ) { } /** + * Performs an action following a bulk theme update. + * + * @since 3.0.0 + * * @param string $title */ public function after( $title = '' ) { @@ -50,6 +64,9 @@ public function after( $title = '' ) { } /** + * Displays the footer following the bulk update process. + * + * @since 3.0.0 */ public function bulk_footer() { parent::bulk_footer(); diff --git a/src/wp-admin/includes/class-bulk-upgrader-skin.php b/src/wp-admin/includes/class-bulk-upgrader-skin.php index 461311962d411..5cdd2a56ced67 100644 --- a/src/wp-admin/includes/class-bulk-upgrader-skin.php +++ b/src/wp-admin/includes/class-bulk-upgrader-skin.php @@ -16,13 +16,30 @@ * @see WP_Upgrader_Skin */ class Bulk_Upgrader_Skin extends WP_Upgrader_Skin { + + /** + * Whether the bulk update process has started. + * + * @since 3.0.0 + * @var bool + */ public $in_loop = false; + /** + * Stores an error message about the update. + * + * @since 3.0.0 * @var string|false */ public $error = false; /** + * Constructor. + * + * Sets up the generic skin for the Bulk Upgrader classes. + * + * @since 3.0.0 + * * @param array $args */ public function __construct( $args = array() ) { @@ -36,6 +53,9 @@ public function __construct( $args = array() ) { } /** + * Sets up the strings used in the update process. + * + * @since 3.0.0 */ public function add_strings() { $this->upgrader->strings['skin_upgrade_start'] = __( 'The update process is starting. This process may take a while on some hosts, so please be patient.' ); @@ -49,6 +69,9 @@ public function add_strings() { } /** + * Displays a message about the update. + * + * @since 3.0.0 * @since 5.9.0 Renamed `$string` (a PHP reserved keyword) to `$feedback` for PHP 8 named parameter support. * * @param string $feedback Message data. @@ -77,18 +100,27 @@ public function feedback( $feedback, ...$args ) { } /** + * Displays the header before the update process. + * + * @since 3.0.0 */ public function header() { // Nothing. This will be displayed within an iframe. } /** + * Displays the footer following the update process. + * + * @since 3.0.0 */ public function footer() { // Nothing. This will be displayed within an iframe. } /** + * Displays an error message about the update. + * + * @since 3.0.0 * @since 5.9.0 Renamed `$error` to `$errors` for PHP 8 named parameter support. * * @param string|WP_Error $errors Errors. @@ -113,18 +145,28 @@ public function error( $errors ) { } /** + * Displays the header before the bulk update process. + * + * @since 3.0.0 */ public function bulk_header() { $this->feedback( 'skin_upgrade_start' ); } /** + * Displays the footer following the bulk update process. + * + * @since 3.0.0 */ public function bulk_footer() { $this->feedback( 'skin_upgrade_end' ); } /** + * Performs an action before a bulk update. + * + * @since 3.0.0 + * * @param string $title */ public function before( $title = '' ) { @@ -137,6 +179,10 @@ public function before( $title = '' ) { } /** + * Performs an action following a bulk update. + * + * @since 3.0.0 + * * @param string $title */ public function after( $title = '' ) { @@ -172,6 +218,9 @@ public function after( $title = '' ) { } /** + * Resets the properties used in the update process. + * + * @since 3.0.0 */ public function reset() { $this->in_loop = false; @@ -179,6 +228,9 @@ public function reset() { } /** + * Flushes all output buffers. + * + * @since 3.0.0 */ public function flush_output() { wp_ob_end_flush_all(); diff --git a/src/wp-admin/includes/class-language-pack-upgrader-skin.php b/src/wp-admin/includes/class-language-pack-upgrader-skin.php index 57b0a1c3764c6..b93ed6c8bc32e 100644 --- a/src/wp-admin/includes/class-language-pack-upgrader-skin.php +++ b/src/wp-admin/includes/class-language-pack-upgrader-skin.php @@ -22,6 +22,12 @@ class Language_Pack_Upgrader_Skin extends WP_Upgrader_Skin { public $display_footer_actions = true; /** + * Constructor. + * + * Sets up the language pack upgrader skin. + * + * @since 3.7.0 + * * @param array $args */ public function __construct( $args = array() ) { @@ -41,6 +47,9 @@ public function __construct( $args = array() ) { } /** + * Performs an action before a language pack update. + * + * @since 3.7.0 */ public function before() { $name = $this->upgrader->get_name_for_update( $this->language_update ); @@ -52,6 +61,9 @@ public function before() { } /** + * Displays an error message about the update. + * + * @since 3.7.0 * @since 5.9.0 Renamed `$error` to `$errors` for PHP 8 named parameter support. * * @param string|WP_Error $errors Errors. @@ -63,12 +75,18 @@ public function error( $errors ) { } /** + * Performs an action following a language pack update. + * + * @since 3.7.0 */ public function after() { echo ''; } /** + * Displays the footer following the bulk update process. + * + * @since 3.7.0 */ public function bulk_footer() { $this->decrement_update_count( 'translation' ); diff --git a/src/wp-admin/includes/class-plugin-installer-skin.php b/src/wp-admin/includes/class-plugin-installer-skin.php index 9fa033a95ffe4..d8e7e3e8cb236 100644 --- a/src/wp-admin/includes/class-plugin-installer-skin.php +++ b/src/wp-admin/includes/class-plugin-installer-skin.php @@ -24,6 +24,12 @@ class Plugin_Installer_Skin extends WP_Upgrader_Skin { private $is_downgrading = false; /** + * Constructor. + * + * Sets up the plugin installer skin. + * + * @since 2.8.0 + * * @param array $args */ public function __construct( $args = array() ) { diff --git a/src/wp-admin/includes/class-theme-installer-skin.php b/src/wp-admin/includes/class-theme-installer-skin.php index 93c626c617b04..85f87977b2b7b 100644 --- a/src/wp-admin/includes/class-theme-installer-skin.php +++ b/src/wp-admin/includes/class-theme-installer-skin.php @@ -24,6 +24,12 @@ class Theme_Installer_Skin extends WP_Upgrader_Skin { private $is_downgrading = false; /** + * Constructor. + * + * Sets up the theme installer skin. + * + * @since 2.8.0 + * * @param array $args */ public function __construct( $args = array() ) { diff --git a/src/wp-admin/includes/class-wp-upgrader-skin.php b/src/wp-admin/includes/class-wp-upgrader-skin.php index 83b4ba472ebc8..a5c80fad6dde6 100644 --- a/src/wp-admin/includes/class-wp-upgrader-skin.php +++ b/src/wp-admin/includes/class-wp-upgrader-skin.php @@ -20,7 +20,6 @@ class WP_Upgrader_Skin { * Holds the upgrader data. * * @since 2.8.0 - * * @var WP_Upgrader */ public $upgrader; @@ -29,7 +28,6 @@ class WP_Upgrader_Skin { * Whether header is done. * * @since 2.8.0 - * * @var bool */ public $done_header = false; @@ -38,7 +36,6 @@ class WP_Upgrader_Skin { * Whether footer is done. * * @since 2.8.0 - * * @var bool */ public $done_footer = false; @@ -47,7 +44,6 @@ class WP_Upgrader_Skin { * Holds the result of an upgrade. * * @since 2.8.0 - * * @var string|bool|WP_Error */ public $result = false; @@ -56,7 +52,6 @@ class WP_Upgrader_Skin { * Holds the options of an upgrade. * * @since 2.8.0 - * * @var array */ public $options = array(); @@ -82,6 +77,8 @@ public function __construct( $args = array() ) { } /** + * Sets the relationship between the skin being used and the upgrader. + * * @since 2.8.0 * * @param WP_Upgrader $upgrader @@ -94,6 +91,8 @@ public function set_upgrader( &$upgrader ) { } /** + * Sets up the strings used in the update process. + * * @since 3.0.0 */ public function add_strings() { @@ -141,6 +140,8 @@ public function request_filesystem_credentials( $error = false, $context = '', $ } /** + * Displays the header before the update process. + * * @since 2.8.0 */ public function header() { @@ -153,6 +154,8 @@ public function header() { } /** + * Displays the footer following the update process. + * * @since 2.8.0 */ public function footer() { @@ -164,6 +167,8 @@ public function footer() { } /** + * Displays an error message about the update. + * * @since 2.8.0 * * @param string|WP_Error $errors Errors. @@ -186,6 +191,8 @@ public function error( $errors ) { } /** + * Displays a message about the update. + * * @since 2.8.0 * @since 5.9.0 Renamed `$string` (a PHP reserved keyword) to `$feedback` for PHP 8 named parameter support. * @@ -218,7 +225,7 @@ public function feedback( $feedback, ...$args ) { public function before() {} /** - * Performs and action following an update. + * Performs an action following an update. * * @since 2.8.0 */ @@ -262,11 +269,15 @@ protected function decrement_update_count( $type ) { } /** + * Displays the header before the bulk update process. + * * @since 3.0.0 */ public function bulk_header() {} /** + * Displays the footer following the bulk update process. + * * @since 3.0.0 */ public function bulk_footer() {} From f6a6e68d3c2e21ee94edec35a6841484d751ff5c Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 2 May 2024 19:52:42 +0000 Subject: [PATCH 15/21] Docs: Document the array shapes for parsed blocks, template part areas, and template types. See #60699 git-svn-id: https://develop.svn.wordpress.org/trunk@58084 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-template-utils.php | 50 ++++++++++- src/wp-includes/blocks.php | 105 ++++++++++++++++++++-- src/wp-includes/class-wp-block-parser.php | 18 +++- src/wp-includes/class-wp-block.php | 11 ++- 4 files changed, 169 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index a7586061adcdd..b953993b67c3c 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -53,7 +53,19 @@ function get_block_theme_folders( $theme_stylesheet = null ) { * * @since 5.9.0 * - * @return array[] The supported template part area values. + * @return array[] { + * The allowed template part area values. + * + * @type array ...$0 { + * Data for the allowed template part area. + * + * @type string $area Template part area name. + * @type string $label Template part area label. + * @type string $description Template part area description. + * @type string $icon Template part area icon. + * @type string $area_tag Template part area tag. + * } + * } */ function get_allowed_block_template_part_areas() { $default_area_definitions = array( @@ -91,7 +103,19 @@ function get_allowed_block_template_part_areas() { * * @since 5.9.0 * - * @param array[] $default_area_definitions An array of supported area objects. + * @param array[] $default_area_definitions { + * The allowed template part area values. + * + * @type array ...$0 { + * Data for the template part area. + * + * @type string $area Template part area name. + * @type string $label Template part area label. + * @type string $description Template part area description. + * @type string $icon Template part area icon. + * @type string $area_tag Template part area tag. + * } + * } */ return apply_filters( 'default_wp_template_part_areas', $default_area_definitions ); } @@ -103,7 +127,16 @@ function get_allowed_block_template_part_areas() { * * @since 5.9.0 * - * @return array[] The default template types. + * @return array[] { + * The default template types. + * + * @type array ...$0 { + * Data for the template type. + * + * @type string $title Template type title. + * @type string $description Template type description. + * } + * } */ function get_default_block_template_types() { $default_template_types = array( @@ -178,7 +211,16 @@ function get_default_block_template_types() { * * @since 5.9.0 * - * @param array[] $default_template_types An array of template types, formatted as [ slug => [ title, description ] ]. + * @param array[] $default_template_types { + * The default template types. + * + * @type array ...$0 { + * Data for the template type. + * + * @type string $title Template type title. + * @type string $description Template type description. + * } + * } */ return apply_filters( 'default_template_types', $default_template_types ); } diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 854992de57c91..986af9a865b7c 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1201,7 +1201,17 @@ function get_comment_delimited_block_content( $block_name, $block_attributes, $b * * @since 5.3.1 * - * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. + * @param array $block { + * A representative array of a single parsed block object. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } * @return string String of rendered HTML. */ function serialize_block( $block ) { @@ -1229,7 +1239,21 @@ function serialize_block( $block ) { * * @since 5.3.1 * - * @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). + * @param array[] $blocks { + * Array of block structures. + * + * @type array ...$0 { + * A representative array of a single parsed block object. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } + * } * @return string String of rendered HTML. */ function serialize_blocks( $blocks ) { @@ -1638,7 +1662,17 @@ function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { * * @global WP_Post $post The post to edit. * - * @param array $parsed_block A single parsed block object. + * @param array $parsed_block { + * A representative array of the block being rendered. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } * @return string String of rendered HTML. */ function render_block( $parsed_block ) { @@ -1652,7 +1686,17 @@ function render_block( $parsed_block ) { * @since 5.9.0 The `$parent_block` parameter was added. * * @param string|null $pre_render The pre-rendered content. Default null. - * @param array $parsed_block The block being rendered. + * @param array $parsed_block { + * A representative array of the block being rendered. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. */ $pre_render = apply_filters( 'pre_render_block', null, $parsed_block, $parent_block ); @@ -1668,8 +1712,29 @@ function render_block( $parsed_block ) { * @since 5.1.0 * @since 5.9.0 The `$parent_block` parameter was added. * - * @param array $parsed_block The block being rendered. - * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. + * @param array $parsed_block { + * A representative array of the block being rendered. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } + * @param array $source_block { + * An un-modified copy of `$parsed_block`, as it appeared in the source content. + * See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. */ $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block, $parent_block ); @@ -1695,7 +1760,17 @@ function render_block( $parsed_block ) { * @since 5.9.0 The `$parent_block` parameter was added. * * @param array $context Default context. - * @param array $parsed_block Block being rendered, filtered by `render_block_data`. + * @param array $parsed_block { + * A representative array of the block being rendered. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. */ $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); @@ -1711,7 +1786,21 @@ function render_block( $parsed_block ) { * @since 5.0.0 * * @param string $content Post content. - * @return array[] Array of parsed block objects. + * @return array[] { + * Array of block structures. + * + * @type array ...$0 { + * A representative array of a single parsed block object. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } + * } */ function parse_blocks( $content ) { /** diff --git a/src/wp-includes/class-wp-block-parser.php b/src/wp-includes/class-wp-block-parser.php index 543f53691ccb1..c4ffc5eb994b9 100644 --- a/src/wp-includes/class-wp-block-parser.php +++ b/src/wp-includes/class-wp-block-parser.php @@ -49,7 +49,7 @@ class WP_Block_Parser { public $stack; /** - * Parses a document and returns a list of block structures + * Parses a document and returns a list of block structures. * * When encountering an invalid parse will return a best-effort * parse. In contrast to the specification parser this does not @@ -58,7 +58,21 @@ class WP_Block_Parser { * @since 5.0.0 * * @param string $document Input document being parsed. - * @return array[] + * @return array[] { + * Array of block structures. + * + * @type array ...$0 { + * A representative array of a single parsed block object. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array[] $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where + * inner blocks were found. + * } + * } */ public function parse( $document ) { $this->document = $document; diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index 60c026e0ac458..4885b9921900c 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -113,7 +113,16 @@ class WP_Block { * * @since 5.5.0 * - * @param array $block Array of parsed block properties. + * @param array $block { + * A representative array of a single parsed block object. See WP_Block_Parser_Block. + * + * @type string $blockName Name of block. + * @type array $attrs Attributes from block comment delimiters. + * @type array $innerBlocks List of inner blocks. An array of arrays that + * have the same structure as this one. + * @type string $innerHTML HTML from inside block comment delimiters. + * @type array $innerContent List of string fragments and null markers where inner blocks were found. + * } * @param array $available_context Optional array of ancestry context values. * @param WP_Block_Type_Registry $registry Optional block type registry. */ From 65b4b6ad285fe9aa63b3e808f4ba353d623fd80e Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 2 May 2024 20:07:46 +0000 Subject: [PATCH 16/21] Docs: Revert the documentation change to `WP_Block_Parser::parse()` made in [58084]. This file needs to be synced from the Gutenberg repository. See #60699 git-svn-id: https://develop.svn.wordpress.org/trunk@58085 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-block-parser.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/class-wp-block-parser.php b/src/wp-includes/class-wp-block-parser.php index c4ffc5eb994b9..543f53691ccb1 100644 --- a/src/wp-includes/class-wp-block-parser.php +++ b/src/wp-includes/class-wp-block-parser.php @@ -49,7 +49,7 @@ class WP_Block_Parser { public $stack; /** - * Parses a document and returns a list of block structures. + * Parses a document and returns a list of block structures * * When encountering an invalid parse will return a best-effort * parse. In contrast to the specification parser this does not @@ -58,21 +58,7 @@ class WP_Block_Parser { * @since 5.0.0 * * @param string $document Input document being parsed. - * @return array[] { - * Array of block structures. - * - * @type array ...$0 { - * A representative array of a single parsed block object. See WP_Block_Parser_Block. - * - * @type string $blockName Name of block. - * @type array $attrs Attributes from block comment delimiters. - * @type array[] $innerBlocks List of inner blocks. An array of arrays that - * have the same structure as this one. - * @type string $innerHTML HTML from inside block comment delimiters. - * @type array $innerContent List of string fragments and null markers where - * inner blocks were found. - * } - * } + * @return array[] */ public function parse( $document ) { $this->document = $document; From 6b328e69153bd57c02dfef582a6eaa22f7376ba4 Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Fri, 3 May 2024 04:45:20 +0000 Subject: [PATCH 17/21] Editor: add Style Engine support for nested CSS rules. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for passing a `$rules_group` string to wp_style_engine_get_stylesheet_from_css_rules(), so rules can be nested under a media query, layer or other rule. Props isabel_brison, ramonopoly. Fixes #61099. git-svn-id: https://develop.svn.wordpress.org/trunk@58089 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/style-engine.php | 8 +- .../class-wp-style-engine-css-rule.php | 64 ++++++++++++-- .../class-wp-style-engine-css-rules-store.php | 15 +++- .../class-wp-style-engine-processor.php | 21 ++++- .../style-engine/class-wp-style-engine.php | 7 +- .../tests/style-engine/styleEngine.php | 64 ++++++++++++++ .../style-engine/wpStyleEngineCssRule.php | 18 ++++ .../wpStyleEngineCssRulesStore.php | 18 ++++ .../style-engine/wpStyleEngineProcessor.php | 85 +++++++++++++++++++ 9 files changed, 286 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/style-engine.php b/src/wp-includes/style-engine.php index 5b5545c85e09f..53627612f9504 100644 --- a/src/wp-includes/style-engine.php +++ b/src/wp-includes/style-engine.php @@ -113,11 +113,14 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) { * .elephant-are-cool{color:gray;width:3em} * * @since 6.1.0 + * @since 6.6.0 Added support for `$rules_group` in the `$css_rules` array. * * @param array $css_rules { * Required. A collection of CSS rules. * * @type array ...$0 { + * @type string $rules_group A parent CSS selector in the case of nested CSS, + * or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. * @type string $selector A CSS selector. * @type string[] $declarations An associative array of CSS definitions, * e.g. `array( "$property" => "$value", "$property" => "$value" )`. @@ -154,11 +157,12 @@ function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = a continue; } + $rules_group = $css_rule['rules_group'] ?? null; if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'] ); + WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $rules_group ); } - $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'] ); + $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $rules_group ); } if ( empty( $css_rule_objects ) ) { diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-rule.php b/src/wp-includes/style-engine/class-wp-style-engine-css-rule.php index 2b31cff0b48c5..291652a615016 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-rule.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-rule.php @@ -35,20 +35,33 @@ class WP_Style_Engine_CSS_Rule { */ protected $declarations; + /** + * A parent CSS selector in the case of nested CSS, or a CSS nested @rule, + * such as `@media (min-width: 80rem)` or `@layer module`. + * + * @since 6.6.0 + * @var string + */ + protected $rules_group; + /** * Constructor. * * @since 6.1.0 + * @since 6.6.0 Added the `$rules_group` parameter. * * @param string $selector Optional. The CSS selector. Default empty string. * @param string[]|WP_Style_Engine_CSS_Declarations $declarations Optional. An associative array of CSS definitions, * e.g. `array( "$property" => "$value", "$property" => "$value" )`, * or a WP_Style_Engine_CSS_Declarations object. * Default empty array. + * @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, + * such as `@media (min-width: 80rem)` or `@layer module`. */ - public function __construct( $selector = '', $declarations = array() ) { + public function __construct( $selector = '', $declarations = array(), $rules_group = '' ) { $this->set_selector( $selector ); $this->add_declarations( $declarations ); + $this->set_rules_group( $rules_group ); } /** @@ -89,6 +102,31 @@ public function add_declarations( $declarations ) { return $this; } + /** + * Sets the rules group. + * + * @since 6.6.0 + * + * @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, + * such as `@media (min-width: 80rem)` or `@layer module`. + * @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods. + */ + public function set_rules_group( $rules_group ) { + $this->rules_group = $rules_group; + return $this; + } + + /** + * Gets the rules group. + * + * @since 6.6.0 + * + * @return string + */ + public function get_rules_group() { + return $this->rules_group; + } + /** * Gets the declarations object. * @@ -115,6 +153,7 @@ public function get_selector() { * Gets the CSS. * * @since 6.1.0 + * @since 6.6.0 Added support for nested CSS with rules groups. * * @param bool $should_prettify Optional. Whether to add spacing, new lines and indents. * Default false. @@ -123,17 +162,28 @@ public function get_selector() { * @return string */ public function get_css( $should_prettify = false, $indent_count = 0 ) { - $rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : ''; - $declarations_indent = $should_prettify ? $indent_count + 1 : 0; - $suffix = $should_prettify ? "\n" : ''; - $spacer = $should_prettify ? ' ' : ''; - $selector = $should_prettify ? str_replace( ',', ",\n", $this->get_selector() ) : $this->get_selector(); - $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $declarations_indent ); + $rule_indent = $should_prettify ? str_repeat( "\t", $indent_count ) : ''; + $nested_rule_indent = $should_prettify ? str_repeat( "\t", $indent_count + 1 ) : ''; + $declarations_indent = $should_prettify ? $indent_count + 1 : 0; + $nested_declarations_indent = $should_prettify ? $indent_count + 2 : 0; + $suffix = $should_prettify ? "\n" : ''; + $spacer = $should_prettify ? ' ' : ''; + // Trims any multiple selectors strings. + $selector = $should_prettify ? implode( ',', array_map( 'trim', explode( ',', $this->get_selector() ) ) ) : $this->get_selector(); + $selector = $should_prettify ? str_replace( array( ',' ), ",\n", $selector ) : $selector; + $rules_group = $this->get_rules_group(); + $has_rules_group = ! empty( $rules_group ); + $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $has_rules_group ? $nested_declarations_indent : $declarations_indent ); if ( empty( $css_declarations ) ) { return ''; } + if ( $has_rules_group ) { + $selector = "{$rule_indent}{$rules_group}{$spacer}{{$suffix}{$nested_rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$nested_rule_indent}}{$suffix}{$rule_indent}}"; + return $selector; + } + return "{$rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$rule_indent}}"; } } diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php b/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php index 371e59fb8b112..4a82f28b8a41e 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php @@ -121,19 +121,30 @@ public function get_all_rules() { * If the rule does not exist, it will be created. * * @since 6.1.0 + * @since 6.6.0 Added the $rules_group parameter. * * @param string $selector The CSS selector. + * @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, + * such as `@media (min-width: 80rem)` or `@layer module`. * @return WP_Style_Engine_CSS_Rule|void Returns a WP_Style_Engine_CSS_Rule object, * or void if the selector is empty. */ - public function add_rule( $selector ) { - $selector = trim( $selector ); + public function add_rule( $selector, $rules_group = '' ) { + $selector = $selector ? trim( $selector ) : ''; + $rules_group = $rules_group ? trim( $rules_group ) : ''; // Bail early if there is no selector. if ( empty( $selector ) ) { return; } + if ( ! empty( $rules_group ) ) { + if ( empty( $this->rules[ "$rules_group $selector" ] ) ) { + $this->rules[ "$rules_group $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $rules_group ); + } + return $this->rules[ "$rules_group $selector" ]; + } + // Create the rule if it doesn't exist. if ( empty( $this->rules[ $selector ] ) ) { $this->rules[ $selector ] = new WP_Style_Engine_CSS_Rule( $selector ); diff --git a/src/wp-includes/style-engine/class-wp-style-engine-processor.php b/src/wp-includes/style-engine/class-wp-style-engine-processor.php index 0778748498886..d5e6c73f815ca 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-processor.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-processor.php @@ -58,6 +58,7 @@ public function add_store( $store ) { * Adds rules to be processed. * * @since 6.1.0 + * @since 6.6.0 Added support for rules_group. * * @param WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rule[] $css_rules A single, or an array of, * WP_Style_Engine_CSS_Rule objects @@ -70,7 +71,24 @@ public function add_rules( $css_rules ) { } foreach ( $css_rules as $rule ) { - $selector = $rule->get_selector(); + $selector = $rule->get_selector(); + $rules_group = $rule->get_rules_group(); + + /** + * If there is a rules_group and it already exists in the css_rules array, + * add the rule to it. + * Otherwise, create a new entry for the rules_group. + */ + if ( ! empty( $rules_group ) ) { + if ( isset( $this->css_rules[ "$rules_group $selector" ] ) ) { + $this->css_rules[ "$rules_group $selector" ]->add_declarations( $rule->get_declarations() ); + continue; + } + $this->css_rules[ "$rules_group $selector" ] = $rule; + continue; + } + + // If the selector already exists, add the declarations to it. if ( isset( $this->css_rules[ $selector ] ) ) { $this->css_rules[ $selector ]->add_declarations( $rule->get_declarations() ); continue; @@ -117,6 +135,7 @@ public function get_css( $options = array() ) { // Build the CSS. $css = ''; foreach ( $this->css_rules as $rule ) { + // See class WP_Style_Engine_CSS_Rule for the get_css method. $css .= $rule->get_css( $options['prettify'] ); $css .= $options['prettify'] ? "\n" : ''; } diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 99372b5d70c32..8b16cdd4677bb 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -364,6 +364,7 @@ protected static function is_valid_style_value( $style_value ) { * Stores a CSS rule using the provided CSS selector and CSS declarations. * * @since 6.1.0 + * @since 6.6.0 Added the `$rules_group` parameter. * * @param string $store_name A valid store key. * @param string $css_selector When a selector is passed, the function will return @@ -371,12 +372,14 @@ protected static function is_valid_style_value( $style_value ) { * otherwise a concatenated string of properties and values. * @param string[] $css_declarations An associative array of CSS definitions, * e.g. `array( "$property" => "$value", "$property" => "$value" )`. + * @param string $rules_group Optional. A parent CSS selector in the case of nested CSS, or a CSS nested @rule, + * such as `@media (min-width: 80rem)` or `@layer module`. */ - public static function store_css_rule( $store_name, $css_selector, $css_declarations ) { + public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) { if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) { return; } - static::get_store( $store_name )->add_rule( $css_selector )->add_declarations( $css_declarations ); + static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations ); } /** diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index 794f540baf422..9092ce5b6df03 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -749,4 +749,68 @@ public function test_should_dedupe_and_merge_css_rules() { $this->assertSame( '.gandalf{color:white;height:190px;border-style:dotted;padding:10px;margin-bottom:100px;}.dumbledore{color:grey;height:90px;border-style:dotted;}.rincewind{color:grey;height:90px;border-style:dotted;}', $compiled_stylesheet ); } + + /** + * Tests returning a generated stylesheet from a set of nested rules and merging their declarations. + * + * @ticket 61099 + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + */ + public function test_should_merge_declarations_for_rules_groups() { + $css_rules = array( + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'stretch', + ), + ), + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'black', + 'font-family' => 'The-Great-Eye', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '@container (min-width: 700px){.saruman{color:black;height:100px;border-style:solid;align-self:stretch;font-family:The-Great-Eye;}}', $compiled_stylesheet ); + } + + /** + * Tests returning a generated stylesheet from a set of nested rules. + * + * @ticket 61099 + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + */ + public function test_should_return_stylesheet_with_nested_rules() { + $css_rules = array( + array( + 'rules_group' => '.foo', + 'selector' => '@media (orientation: landscape)', + 'declarations' => array( + 'background-color' => 'blue', + ), + ), + array( + 'rules_group' => '.foo', + 'selector' => '@media (min-width > 1024px)', + 'declarations' => array( + 'background-color' => 'cotton-blue', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.foo{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); + } } diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php index debb09d8d1fc6..a9de39392ade0 100644 --- a/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php @@ -37,6 +37,24 @@ public function test_should_instantiate_with_selector_and_rules() { $this->assertSame( $expected, $css_rule->get_css(), 'Value returned by get_css() does not match expected declarations string.' ); } + /** + * Tests setting and getting a rules group. + * + * @ticket 61099 + * + * @covers ::set_rules_group + * @covers ::get_rules_group + */ + public function test_should_set_rules_group() { + $rule = new WP_Style_Engine_CSS_Rule( '.heres-johnny', array(), '@layer state' ); + + $this->assertSame( '@layer state', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to constructor.' ); + + $rule->set_rules_group( '@layer pony' ); + + $this->assertSame( '@layer pony', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to set_rules_group().' ); + } + /** * Tests that declaration properties are deduplicated. * diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php index 4fe6c4c6e2a34..1be7804780052 100644 --- a/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php @@ -187,4 +187,22 @@ public function test_should_get_all_rule_objects_for_a_store() { $this->assertSame( $expected, $new_pizza_store->get_all_rules(), 'Return value for get_all_rules() does not match expectations after adding new rules to store.' ); } + + /** + * Tests adding rules group keys to store. + * + * @ticket 61099 + * + * @covers ::add_rule + */ + public function test_should_store_as_concatenated_rules_groups_and_selector() { + $store_one = WP_Style_Engine_CSS_Rules_Store::get_store( 'one' ); + $store_one_rule = $store_one->add_rule( '.tony', '.one' ); + + $this->assertSame( + '.one .tony', + "{$store_one_rule->get_rules_group()} {$store_one_rule->get_selector()}", + 'add_rule() does not concatenate rules group and selector.' + ); + } } diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php b/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php index d38f46d0de5b3..070aefe6886f7 100644 --- a/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php +++ b/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php @@ -48,6 +48,43 @@ public function test_should_return_rules_as_compiled_css() { ); } + /** + * Tests adding nested rules with at-rules and returning compiled CSS rules. + * + * @ticket 61099 + * + * @covers ::add_rules + * @covers ::get_css + */ + public function test_should_return_nested_rules_as_compiled_css() { + $a_nice_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nice-rule' ); + $a_nice_css_rule->add_declarations( + array( + 'color' => 'var(--nice-color)', + 'background-color' => 'purple', + ) + ); + $a_nice_css_rule->set_rules_group( '@media (min-width: 80rem)' ); + + $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nicer-rule' ); + $a_nicer_css_rule->add_declarations( + array( + 'font-family' => 'Nice sans', + 'font-size' => '1em', + 'background-color' => 'purple', + ) + ); + $a_nicer_css_rule->set_rules_group( '@layer nicety' ); + + $a_nice_processor = new WP_Style_Engine_Processor(); + $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); + + $this->assertSame( + '@media (min-width: 80rem){.a-nice-rule{color:var(--nice-color);background-color:purple;}}@layer nicety{.a-nicer-rule{font-family:Nice sans;font-size:1em;background-color:purple;}}', + $a_nice_processor->get_css( array( 'prettify' => false ) ) + ); + } + /** * Tests compiling CSS rules and formatting them with new lines and indents. * @@ -101,6 +138,54 @@ public function test_should_return_prettified_css_rules() { ); } + /** + * Tests compiling nested CSS rules and formatting them with new lines and indents. + * + * @ticket 61099 + * + * @covers ::get_css + */ + public function test_should_return_prettified_nested_css_rules() { + $a_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-wonderful-rule' ); + $a_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_wonderful_css_rule->set_rules_group( '@media (min-width: 80rem)' ); + + $a_very_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-very_wonderful-rule' ); + $a_very_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_very_wonderful_css_rule->set_rules_group( '@layer wonderfulness' ); + + $a_wonderful_processor = new WP_Style_Engine_Processor(); + $a_wonderful_processor->add_rules( array( $a_wonderful_css_rule, $a_very_wonderful_css_rule ) ); + + $expected = '@media (min-width: 80rem) { + .a-wonderful-rule { + color: var(--wonderful-color); + background-color: orange; + } +} +@layer wonderfulness { + .a-very_wonderful-rule { + color: var(--wonderful-color); + background-color: orange; + } +} +'; + $this->assertSame( + $expected, + $a_wonderful_processor->get_css( array( 'prettify' => true ) ) + ); + } + /** * Tests adding a store and compiling CSS rules from that store. * From 5207502ecf9ca6c61b7afa2726371e2abf4f7fa3 Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Fri, 3 May 2024 05:23:42 +0000 Subject: [PATCH 18/21] Editor: Fix coding standards and move deprecated function to correct file. Follow-up to [58074], formats docblocks correctly and moves `wp_render_elements_support` to the deprecated file. Props aaronrobertshaw, isabel_brison, mukesh27, spacedmonkey. See #60681. git-svn-id: https://develop.svn.wordpress.org/trunk@58090 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-supports/elements.php | 21 +-------------------- src/wp-includes/deprecated.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/block-supports/elements.php b/src/wp-includes/block-supports/elements.php index afa31eb618cf8..5678dfc0fc897 100644 --- a/src/wp-includes/block-supports/elements.php +++ b/src/wp-includes/block-supports/elements.php @@ -19,24 +19,6 @@ function wp_get_elements_class_name( $block ) { return 'wp-elements-' . md5( serialize( $block ) ); } -/** - * Updates the block content with elements class names. - * - * @deprecated 6.6.0 Generation of element class name is handled via `render_block_data` filter. - * - * @since 5.8.0 - * @since 6.4.0 Added support for button and heading element styling. - * @access private - * - * @param string $block_content Rendered block content. - * @param array $block Block object. - * @return string Filtered block content. - */ -function wp_render_elements_support( $block_content, $block ) { - _deprecated_function( __FUNCTION__, '6.6.0', 'wp_render_elements_class_name' ); - return $block_content; -} - /** * Determines whether an elements class name should be added to the block. * @@ -134,7 +116,7 @@ function wp_render_elements_support_styles( $parsed_block ) { * `render_block_data` filter in 6.6.0 to avoid filtered attributes * breaking the application of the elements CSS class. * - * @see https://github.com/WordPress/gutenberg/pull/59535. + * @see https://github.com/WordPress/gutenberg/pull/59535 * * The change in filter means, the argument types for this function * have changed and require deprecating. @@ -259,7 +241,6 @@ function wp_render_elements_support_styles( $parsed_block ) { * * @param string $block_content Rendered block content. * @param array $block Block object. - * * @return string Filtered block content. */ function wp_render_elements_class_name( $block_content, $block ) { diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 155ce0fef0aed..c490fe06ef38e 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -6300,3 +6300,21 @@ function block_core_image_ensure_interactivity_dependency() { $wp_scripts->registered['wp-block-image-view']->deps[] = 'wp-interactivity'; } } + +/** + * Updates the block content with elements class names. + * + * @deprecated 6.6.0 Generation of element class name is handled via `render_block_data` filter. + * + * @since 5.8.0 + * @since 6.4.0 Added support for button and heading element styling. + * @access private + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function wp_render_elements_support( $block_content, $block ) { + _deprecated_function( __FUNCTION__, '6.6.0', 'wp_render_elements_class_name' ); + return $block_content; +} From 1911cbabb2cd4462763262facf8ee5449479c2f0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 May 2024 07:56:16 +0000 Subject: [PATCH 19/21] Embeds: Add Bluesky as a trusted oEmbed provider. Props swissspidy, thelovekesh, peterwilsoncc, bnewboldbsky. Fixes #61020. git-svn-id: https://develop.svn.wordpress.org/trunk@58091 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-oembed.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/class-wp-oembed.php b/src/wp-includes/class-wp-oembed.php index c428723537562..546fa41f28656 100644 --- a/src/wp-includes/class-wp-oembed.php +++ b/src/wp-includes/class-wp-oembed.php @@ -110,6 +110,7 @@ public function __construct() { '#https?://(www\.)?wolframcloud\.com/obj/.+#i' => array( 'https://www.wolframcloud.com/oembed', true ), '#https?://pca\.st/.+#i' => array( 'https://pca.st/oembed.json', true ), '#https?://((play|www)\.)?anghami\.com/.*#i' => array( 'https://api.anghami.com/rest/v1/oembed.view', true ), + '#https?://bsky.app/profile/.*/post/.*#i' => array( 'https://embed.bsky.app/oembed', true ), ); if ( ! empty( self::$early_providers['add'] ) ) { @@ -190,6 +191,7 @@ public function __construct() { * | Pocket Casts | pocketcasts.com | 6.1.0 | * | Crowdsignal | crowdsignal.net | 6.2.0 | * | Anghami | anghami.com | 6.3.0 | + * | Bluesky | bsky.app | 6.6.0 | * * No longer supported providers: * From 8175698ba5aaa7472ca9683b550253221fd9005e Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 3 May 2024 12:37:05 +0000 Subject: [PATCH 20/21] Build/Test Tools: Remind contributors to include a Trac ticket link. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributing to WordPress using `wordpress-develop` on GitHub is a useful way collaborate, test, and review suggested changes to the code base. One of the required criteria, though, is including a link to a corresponding Trac ticket. This ensures the PR and associated activity is listed on the Trac ticket, which serves as the source of truth. It’s easy to forget this and newer contributors aren’t always aware of this requirement. This adds a GitHub Actions job that will add a comment as a reminder when no Trac ticket is included. Is the waiting really ended? Two thousand years. Props anamarijapapic, peterwilsoncc. Fixes #60129. git-svn-id: https://develop.svn.wordpress.org/trunk@58092 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/pull-request-comments.yml | 49 ++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-comments.yml b/.github/workflows/pull-request-comments.yml index df276270f1396..189eb1c1737d0 100644 --- a/.github/workflows/pull-request-comments.yml +++ b/.github/workflows/pull-request-comments.yml @@ -49,7 +49,7 @@ jobs: **Pull requests are never merged on GitHub.** The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. - More information about how GitHub pull requests can be used to contribute to WordPress can be found in [this blog post](https://make.wordpress.org/core/2020/02/21/working-on-trac-tickets-using-github-pull-requests/). + More information about how GitHub pull requests can be used to contribute to WordPress can be found in [the Core Handbook](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/). **Please include automated tests.** Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the [Automated Testing](https://make.wordpress.org/core/handbook/testing/automated-testing/) page in the handbook. @@ -163,3 +163,50 @@ jobs: `; github.rest.issues.createComment( commentInfo ); + + # Leaves a comment on a pull request when no Trac ticket is included in the pull request description. + trac-ticket-check: + name: Comment on a pull request when no Trac ticket is included + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name == 'pull_request_target' && ! github.event.pull_request.draft && github.event.pull_request.state == 'open' }} + steps: + - name: Check for Trac ticket and comment if missing + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const { owner, repo } = context.repo; + const { number } = context.issue; + + // Check for the presence of a comment and bail early. + const comments = ( await github.rest.issues.listComments( { owner, repo, issue_number: number } ) ).data; + + const hasMissingTicketComment = comments.some( comment => + comment.user.type === 'Bot' && comment.body.includes( 'Trac Ticket Missing' ) + ); + + if ( hasMissingTicketComment ) return; + + // No comment was found. Create one. + const pr = ( await github.rest.pulls.get( { owner, repo, pull_number: number } ) ).data; + + const prBody = pr.body ?? ''; + const prTitle = pr.title ?? ''; + + const tracTicketRegex = new RegExp( 'https?://core.trac.wordpress.org/ticket/([0-9]+)', 'g' ); + const tracTicketMatches = prBody.match( tracTicketRegex ) || prTitle.match( tracTicketRegex ); + + if ( ! tracTicketMatches ) { + github.rest.issues.createComment( { + owner, + repo, + issue_number: number, + body: `## Trac Ticket Missing + This pull request is missing a link to a [Trac ticket](https://core.trac.wordpress.org/). For a contribution to be considered, there must be a corresponding ticket in Trac. + + To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. More information about contributing to WordPress on GitHub can be found in [the Core Handbook](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/). + `, + } ); + } From a4123728068e09200d7dfe2d5a88f803c1adf2aa Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 3 May 2024 14:30:56 +0000 Subject: [PATCH 21/21] Login and Registration: Check that `post_password` is a string in `wp-login.php`. This prevents a fatal error if an array is passed instead. Follow-up to [19925], [34909], [58023]. Props dd32, swissspidy. Fixes #61136. git-svn-id: https://develop.svn.wordpress.org/trunk@58093 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-login.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-login.php b/src/wp-login.php index bd4568a4690b5..18811aae57b74 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -753,7 +753,7 @@ function wp_login_viewport_meta() { break; case 'postpass': - if ( ! array_key_exists( 'post_password', $_POST ) ) { + if ( ! isset( $_POST['post_password'] ) || ! is_string( $_POST['post_password'] ) ) { wp_safe_redirect( wp_get_referer() ); exit; }