From d6bff10c4ff703944135e9a31635925e621be842 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 4 Jun 2024 05:34:12 +0000 Subject: [PATCH 01/21] Editor: add textAlign block support. See https://github.com/WordPress/gutenberg/pull/59531. See https://github.com/WordPress/gutenberg/pull/61182. See https://github.com/WordPress/gutenberg/pull/61717. See https://github.com/WordPress/wordpress-develop/pull/6590. Fixes #61256. Props wildworks, ellatrix. git-svn-id: https://develop.svn.wordpress.org/trunk@58314 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-supports/typography.php | 21 ++++++++++++++++++- src/wp-includes/class-wp-theme-json.php | 3 +++ src/wp-includes/theme.json | 1 + tests/phpunit/tests/theme/wpThemeJson.php | 7 ++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/block-supports/typography.php b/src/wp-includes/block-supports/typography.php index 6e7e093931623..066efba6b1385 100644 --- a/src/wp-includes/block-supports/typography.php +++ b/src/wp-includes/block-supports/typography.php @@ -31,6 +31,7 @@ function wp_register_typography_support( $block_type ) { $has_font_weight_support = isset( $typography_supports['__experimentalFontWeight'] ) ? $typography_supports['__experimentalFontWeight'] : false; $has_letter_spacing_support = isset( $typography_supports['__experimentalLetterSpacing'] ) ? $typography_supports['__experimentalLetterSpacing'] : false; $has_line_height_support = isset( $typography_supports['lineHeight'] ) ? $typography_supports['lineHeight'] : false; + $has_text_align_support = isset( $typography_supports['textAlign'] ) ? $typography_supports['textAlign'] : false; $has_text_columns_support = isset( $typography_supports['textColumns'] ) ? $typography_supports['textColumns'] : false; $has_text_decoration_support = isset( $typography_supports['__experimentalTextDecoration'] ) ? $typography_supports['__experimentalTextDecoration'] : false; $has_text_transform_support = isset( $typography_supports['__experimentalTextTransform'] ) ? $typography_supports['__experimentalTextTransform'] : false; @@ -42,6 +43,7 @@ function wp_register_typography_support( $block_type ) { || $has_font_weight_support || $has_letter_spacing_support || $has_line_height_support + || $has_text_align_support || $has_text_columns_support || $has_text_decoration_support || $has_text_transform_support @@ -106,6 +108,7 @@ function wp_apply_typography_support( $block_type, $block_attributes ) { $has_font_weight_support = isset( $typography_supports['__experimentalFontWeight'] ) ? $typography_supports['__experimentalFontWeight'] : false; $has_letter_spacing_support = isset( $typography_supports['__experimentalLetterSpacing'] ) ? $typography_supports['__experimentalLetterSpacing'] : false; $has_line_height_support = isset( $typography_supports['lineHeight'] ) ? $typography_supports['lineHeight'] : false; + $has_text_align_support = isset( $typography_supports['textAlign'] ) ? $typography_supports['textAlign'] : false; $has_text_columns_support = isset( $typography_supports['textColumns'] ) ? $typography_supports['textColumns'] : false; $has_text_decoration_support = isset( $typography_supports['__experimentalTextDecoration'] ) ? $typography_supports['__experimentalTextDecoration'] : false; $has_text_transform_support = isset( $typography_supports['__experimentalTextTransform'] ) ? $typography_supports['__experimentalTextTransform'] : false; @@ -117,6 +120,7 @@ function wp_apply_typography_support( $block_type, $block_attributes ) { $should_skip_font_style = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'fontStyle' ); $should_skip_font_weight = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'fontWeight' ); $should_skip_line_height = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'lineHeight' ); + $should_skip_text_align = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'textAlign' ); $should_skip_text_columns = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'textColumns' ); $should_skip_text_decoration = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'textDecoration' ); $should_skip_text_transform = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'textTransform' ); @@ -176,6 +180,12 @@ function wp_apply_typography_support( $block_type, $block_attributes ) { : null; } + if ( $has_text_align_support && ! $should_skip_text_align ) { + $typography_block_styles['textAlign'] = isset( $block_attributes['style']['typography']['textAlign'] ) + ? $block_attributes['style']['typography']['textAlign'] + : null; + } + if ( $has_text_columns_support && ! $should_skip_text_columns && isset( $block_attributes['style']['typography']['textColumns'] ) ) { $typography_block_styles['textColumns'] = isset( $block_attributes['style']['typography']['textColumns'] ) ? $block_attributes['style']['typography']['textColumns'] @@ -225,13 +235,22 @@ function wp_apply_typography_support( $block_type, $block_attributes ) { } $attributes = array(); + $classnames = array(); $styles = wp_style_engine_get_styles( array( 'typography' => $typography_block_styles ), array( 'convert_vars_to_classnames' => true ) ); if ( ! empty( $styles['classnames'] ) ) { - $attributes['class'] = $styles['classnames']; + $classnames[] = $styles['classnames']; + } + + if ( $has_text_align_support && ! $should_skip_text_align && isset( $block_attributes['style']['typography']['textAlign'] ) ) { + $classnames[] = 'has-text-align-' . $block_attributes['style']['typography']['textAlign']; + } + + if ( ! empty( $classnames ) ) { + $attributes['class'] = implode( ' ', $classnames ); } if ( ! empty( $styles['css'] ) ) { diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 4da9580271ff8..9aa2b97b8b274 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -256,6 +256,7 @@ class WP_Theme_JSON { 'border-left-width' => array( 'border', 'left', 'width' ), 'border-left-style' => array( 'border', 'left', 'style' ), 'color' => array( 'color', 'text' ), + 'text-align' => array( 'typography', 'textAlign' ), 'column-count' => array( 'typography', 'textColumns' ), 'font-family' => array( 'typography', 'fontFamily' ), 'font-size' => array( 'typography', 'fontSize' ), @@ -454,6 +455,7 @@ class WP_Theme_JSON { 'fontWeight' => null, 'letterSpacing' => null, 'lineHeight' => null, + 'textAlign' => null, 'textColumns' => null, 'textDecoration' => null, 'textTransform' => null, @@ -558,6 +560,7 @@ class WP_Theme_JSON { 'fontWeight' => null, 'letterSpacing' => null, 'lineHeight' => null, + 'textAlign' => null, 'textColumns' => null, 'textDecoration' => null, 'textTransform' => null, diff --git a/src/wp-includes/theme.json b/src/wp-includes/theme.json index 47aea2d24b9a4..485d01247d636 100644 --- a/src/wp-includes/theme.json +++ b/src/wp-includes/theme.json @@ -303,6 +303,7 @@ "fontWeight": true, "letterSpacing": true, "lineHeight": false, + "textAlign": true, "textDecoration": true, "textTransform": true, "writingMode": false diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 8e6ec027de3b4..5707e6a804a00 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -516,6 +516,11 @@ public function test_get_stylesheet() { ), ), ), + 'core/media-text' => array( + 'typography' => array( + 'textAlign' => 'center', + ), + ), 'core/post-date' => array( 'color' => array( 'text' => '#123456', @@ -560,7 +565,7 @@ public function test_get_stylesheet() { ); $variables = ':root{--wp--preset--color--grey: grey;--wp--preset--gradient--custom-gradient: linear-gradient(135deg,rgba(0,0,0) 0%,rgb(0,0,0) 100%);--wp--preset--font-size--small: 14px;--wp--preset--font-size--big: 41px;--wp--preset--font-family--arial: Arial, serif;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}'; - $styles = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:root :where(body){color: var(--wp--preset--color--grey);}:root :where(a:where(:not(.wp-element-button))){background-color: #333;color: #111;}:root :where(.wp-element-button, .wp-block-button__link){box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.66);}:root :where(.wp-block-cover){min-height: unset;aspect-ratio: 16/9;}:root :where(.wp-block-group){background: var(--wp--preset--gradient--custom-gradient);border-radius: 10px;min-height: 50vh;padding: 24px;}:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: #111;}:root :where(.wp-block-heading){color: #123456;}:root :where(.wp-block-heading a:where(:not(.wp-element-button))){background-color: #333;color: #111;font-size: 60px;}:root :where(.wp-block-post-date){color: #123456;}:root :where(.wp-block-post-date a:where(:not(.wp-element-button))){background-color: #777;color: #555;}:root :where(.wp-block-post-excerpt){column-count: 2;}:root :where(.wp-block-image){margin-bottom: 30px;}:root :where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-top-left-radius: 10px;border-bottom-right-radius: 1em;}:root :where(.wp-block-image img, .wp-block-image .components-placeholder){filter: var(--wp--preset--duotone--custom-duotone);}'; + $styles = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:root :where(body){color: var(--wp--preset--color--grey);}:root :where(a:where(:not(.wp-element-button))){background-color: #333;color: #111;}:root :where(.wp-element-button, .wp-block-button__link){box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.66);}:root :where(.wp-block-cover){min-height: unset;aspect-ratio: 16/9;}:root :where(.wp-block-group){background: var(--wp--preset--gradient--custom-gradient);border-radius: 10px;min-height: 50vh;padding: 24px;}:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: #111;}:root :where(.wp-block-heading){color: #123456;}:root :where(.wp-block-heading a:where(:not(.wp-element-button))){background-color: #333;color: #111;font-size: 60px;}:root :where(.wp-block-media-text){text-align: center;}:root :where(.wp-block-post-date){color: #123456;}:root :where(.wp-block-post-date a:where(:not(.wp-element-button))){background-color: #777;color: #555;}:root :where(.wp-block-post-excerpt){column-count: 2;}:root :where(.wp-block-image){margin-bottom: 30px;}:root :where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-top-left-radius: 10px;border-bottom-right-radius: 1em;}:root :where(.wp-block-image img, .wp-block-image .components-placeholder){filter: var(--wp--preset--duotone--custom-duotone);}'; $presets = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-custom-gradient-gradient-background{background: var(--wp--preset--gradient--custom-gradient) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-big-font-size{font-size: var(--wp--preset--font-size--big) !important;}.has-arial-font-family{font-family: var(--wp--preset--font-family--arial) !important;}'; $all = $variables . $styles . $presets; From 2799437642b1037b20d2fbd911ea317a562c33fb Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Tue, 4 Jun 2024 05:41:44 +0000 Subject: [PATCH 02/21] Add ticket reference to `test_wp_apply_shadow_support`. Props mukesh27. Follows r58312. See #60784. git-svn-id: https://develop.svn.wordpress.org/trunk@58315 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/block-supports/shadow.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/phpunit/tests/block-supports/shadow.php b/tests/phpunit/tests/block-supports/shadow.php index 8def6850ca7f4..039b2a4e4e5aa 100644 --- a/tests/phpunit/tests/block-supports/shadow.php +++ b/tests/phpunit/tests/block-supports/shadow.php @@ -50,6 +50,8 @@ private function register_shadow_block_with_support( $block_name, $supports = ar /** * Tests the generation of shadow block support styles. + * + * @ticket 60784 * * @dataProvider data_generate_shadow_fixtures * From 65f75977da5ea6f3f25f4af3d8e72f2332213252 Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Tue, 4 Jun 2024 05:48:55 +0000 Subject: [PATCH 03/21] Remove extra whitespace from `restore_image_outer_container` docblock. Props mukesh27. Follows r58313. See #61271. git-svn-id: https://develop.svn.wordpress.org/trunk@58316 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-duotone.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-duotone.php b/src/wp-includes/class-wp-duotone.php index 77afdce970e26..76ce342610659 100644 --- a/src/wp-includes/class-wp-duotone.php +++ b/src/wp-includes/class-wp-duotone.php @@ -1161,7 +1161,7 @@ public static function render_duotone_support( $block_content, $block, $wp_block * @since 6.6.0 * * @param string $block_content Rendered block content. - * @return string Filtered block content. + * @return string Filtered block content. */ public static function restore_image_outer_container( $block_content ) { if ( wp_theme_has_theme_json() ) { From fdd124d1adce3755c0295342d7b41f2a075005b0 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 4 Jun 2024 05:50:08 +0000 Subject: [PATCH 04/21] Docs: Fix docblock alignment in `class-wp-duotone.php`. Follow-up to [58313]. Props mukesh27. See #61271, #60699. git-svn-id: https://develop.svn.wordpress.org/trunk@58317 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-duotone.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-duotone.php b/src/wp-includes/class-wp-duotone.php index 76ce342610659..0260cab4f3bd6 100644 --- a/src/wp-includes/class-wp-duotone.php +++ b/src/wp-includes/class-wp-duotone.php @@ -1160,7 +1160,7 @@ public static function render_duotone_support( $block_content, $block, $wp_block * * @since 6.6.0 * - * @param string $block_content Rendered block content. + * @param string $block_content Rendered block content. * @return string Filtered block content. */ public static function restore_image_outer_container( $block_content ) { From 9073434d73bfd26ec77e7908220fc178c091fae5 Mon Sep 17 00:00:00 2001 From: Isabel Brison Date: Tue, 4 Jun 2024 05:53:21 +0000 Subject: [PATCH 05/21] Remove trailing whitespace from `test_wp_apply_shadow_support`. Follows r58315. See #60784. git-svn-id: https://develop.svn.wordpress.org/trunk@58318 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/block-supports/shadow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/block-supports/shadow.php b/tests/phpunit/tests/block-supports/shadow.php index 039b2a4e4e5aa..c925bb2f43fe4 100644 --- a/tests/phpunit/tests/block-supports/shadow.php +++ b/tests/phpunit/tests/block-supports/shadow.php @@ -50,7 +50,7 @@ private function register_shadow_block_with_support( $block_name, $supports = ar /** * Tests the generation of shadow block support styles. - * + * * @ticket 60784 * * @dataProvider data_generate_shadow_fixtures From 565c2f9446d53c773fd630488427d23abf512db6 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 4 Jun 2024 06:28:47 +0000 Subject: [PATCH 06/21] Upgrade/Install: Remove the download authenticity message from Core/Plugins/Themes updates. This changeset deactivates the download authenticity message by disabling package signature verification, at least until software signing is fully implemented on wordpress.org. The provided message had no actionability and only led to more support. Props jipmoors, afercia, bridgetwillard, s0what, rajinsharwar, audrasjb, johnbillion, peterwilsoncc. Fixes #47315. git-svn-id: https://develop.svn.wordpress.org/trunk@58319 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-core-upgrader.php | 2 +- src/wp-admin/includes/class-wp-upgrader.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-core-upgrader.php b/src/wp-admin/includes/class-core-upgrader.php index 165e1f7baee5d..2655c27c3b07e 100644 --- a/src/wp-admin/includes/class-core-upgrader.php +++ b/src/wp-admin/includes/class-core-upgrader.php @@ -121,7 +121,7 @@ public function upgrade( $current, $args = array() ) { return new WP_Error( 'locked', $this->strings['locked'] ); } - $download = $this->download_package( $current->packages->$to_download, true ); + $download = $this->download_package( $current->packages->$to_download, false ); /* * Allow for signature soft-fail. diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php index ae583adc1db7f..9474ce0b9bdea 100644 --- a/src/wp-admin/includes/class-wp-upgrader.php +++ b/src/wp-admin/includes/class-wp-upgrader.php @@ -828,7 +828,7 @@ public function run( $options ) { * Download the package. Note: If the package is the full path * to an existing local file, it will be returned untouched. */ - $download = $this->download_package( $options['package'], true, $options['hook_extra'] ); + $download = $this->download_package( $options['package'], false, $options['hook_extra'] ); /* * Allow for signature soft-fail. From 069e2879ff1d50b3bed6bd9f362e9b65a581cbcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Tue, 4 Jun 2024 06:56:25 +0000 Subject: [PATCH 07/21] Interactivity API: Some property access does not work well in server directives Ensures property access in PHP works for object properties or associative array values correctly when processing Interactivity API directives. Props narenin, cbravobernal, jonsurrell, gziolo, czapla. Fixes #61039. git-svn-id: https://develop.svn.wordpress.org/trunk@58320 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-interactivity-api.php | 4 ++- .../interactivity-api/wpInteractivityAPI.php | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index ac9a48982a5c5..23675a9683bfa 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -423,8 +423,10 @@ private function evaluate( $directive_value, string $default_namespace, $context $path_segments = explode( '.', $path ); $current = $store; foreach ( $path_segments as $path_segment ) { - if ( isset( $current[ $path_segment ] ) ) { + if ( ( is_array( $current ) || $current instanceof ArrayAccess ) && isset( $current[ $path_segment ] ) ) { $current = $current[ $path_segment ]; + } elseif ( is_object( $current ) && isset( $current->$path_segment ) ) { + $current = $current->$path_segment; } else { return null; } diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index 3c2050cab38ed..a28a6a94e62d0 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -816,9 +816,26 @@ public function test_process_directives_does_not_change_inner_html_in_math() { */ private function evaluate( $directive_value ) { $generate_state = function ( $name ) { + $obj = new stdClass(); + $obj->prop = $name; return array( - 'key' => $name, - 'nested' => array( 'key' => $name . '-nested' ), + 'key' => $name, + 'nested' => array( 'key' => $name . '-nested' ), + 'obj' => $obj, + 'arrAccess' => new class() implements ArrayAccess { + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + return true; + } + + public function offsetGet( $offset ): string { + return $offset; + } + + public function offsetSet( $offset, $value ): void {} + + public function offsetUnset( $offset ): void {} + }, ); }; $this->interactivity->state( 'myPlugin', $generate_state( 'myPlugin-state' ) ); @@ -826,6 +843,7 @@ private function evaluate( $directive_value ) { $context = array( 'myPlugin' => $generate_state( 'myPlugin-context' ), 'otherPlugin' => $generate_state( 'otherPlugin-context' ), + 'obj' => new stdClass(), ); $evaluate = new ReflectionMethod( $this->interactivity, 'evaluate' ); $evaluate->setAccessible( true ); @@ -851,6 +869,12 @@ public function test_evaluate_value() { $result = $this->evaluate( 'otherPlugin::context.key' ); $this->assertEquals( 'otherPlugin-context', $result ); + + $result = $this->evaluate( 'state.obj.prop' ); + $this->assertSame( 'myPlugin-state', $result ); + + $result = $this->evaluate( 'state.arrAccess.1' ); + $this->assertSame( '1', $result ); } /** From fec55f4201726f5a3b720b488909e30382c0fd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Tue, 4 Jun 2024 07:16:48 +0000 Subject: [PATCH 08/21] Interactivity API: Print debug warning when server directives processing encounters errors Aims to improve the developer experience of the Interactivity API server directives processing. Props cbravobernal, jonsurrell, westonruter, darerodz, czapla, gziolo. Fixes #61044. git-svn-id: https://develop.svn.wordpress.org/trunk@58321 602fd350-edb4-49c9-b593-d223f7449a82 --- ...interactivity-api-directives-processor.php | 21 +++++----- .../class-wp-interactivity-api.php | 28 ++++++++++--- .../wpInteractivityAPI-wp-bind.php | 2 + .../wpInteractivityAPI-wp-class.php | 2 + .../wpInteractivityAPI-wp-context.php | 1 + .../wpInteractivityAPI-wp-each.php | 4 ++ .../wpInteractivityAPI-wp-style.php | 2 + .../wpInteractivityAPI-wp-text.php | 4 +- .../interactivity-api/wpInteractivityAPI.php | 39 ++++++++++++++----- 9 files changed, 79 insertions(+), 24 deletions(-) diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php index b12dcb4b3b158..590cf967cf471 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api-directives-processor.php @@ -198,16 +198,19 @@ private function get_balanced_tag_bookmarks() { public function skip_to_tag_closer(): bool { $depth = 1; $tag_name = $this->get_tag(); - while ( $depth > 0 && $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) ) { - if ( $this->has_self_closing_flag() ) { - continue; + + while ( $depth > 0 && $this->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + if ( ! $this->is_tag_closer() && $this->get_attribute_names_with_prefix( 'data-wp-' ) ) { + /* translators: 1: SVG or MATH HTML tag. */ + $message = sprintf( __( 'Interactivity directives were detected inside an incompatible %1$s tag. These directives will be ignored in the server side render.' ), $tag_name ); + _doing_it_wrong( __METHOD__, $message, '6.6.0' ); + } + if ( $this->get_tag() === $tag_name ) { + if ( $this->has_self_closing_flag() ) { + continue; + } + $depth += $this->is_tag_closer() ? -1 : 1; } - $depth += $this->is_tag_closer() ? -1 : 1; } return 0 === $depth; diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 23675a9683bfa..77abe19244246 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -272,6 +272,7 @@ public function process_directives( string $html ): string { * it returns null if the HTML contains unbalanced tags. * * @since 6.5.0 + * @since 6.6.0 The function displays a warning message when the HTML contains unbalanced tags or a directive appears in a MATH or SVG tag. * * @param string $html The HTML content to process. * @param array $context_stack The reference to the array used to keep track of contexts during processing. @@ -295,6 +296,11 @@ private function process_directives_args( string $html, array &$context_stack, a * We still process the rest of the HTML. */ if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) { + if ( $p->get_attribute_names_with_prefix( 'data-wp-' ) ) { + /* translators: 1: SVG or MATH HTML tag, 2: Namespace of the interactive block. */ + $message = sprintf( __( 'Interactivity directives were detected on an incompatible %1$s tag when processing "%2$s". These directives will be ignored in the server side render.' ), $tag_name, end( $namespace_stack ) ); + _doing_it_wrong( __METHOD__, $message, '6.6.0' ); + } $p->skip_to_tag_closer(); continue; } @@ -382,13 +388,21 @@ private function process_directives_args( string $html, array &$context_stack, a } } } - /* * It returns null if the HTML is unbalanced because unbalanced HTML is * not safe to process. In that case, the Interactivity API runtime will - * update the HTML on the client side during the hydration. + * update the HTML on the client side during the hydration. It will also + * display a notice to the developer to inform them about the issue. */ - return $unbalanced || 0 < count( $tag_stack ) ? null : $p->get_updated_html(); + if ( $unbalanced || 0 < count( $tag_stack ) ) { + $tag_errored = 0 < count( $tag_stack ) ? end( $tag_stack )[0] : $tag_name; + /* translators: %1s: Namespace processed, %2s: The tag that caused the error; could be any HTML tag. */ + $message = sprintf( __( 'Interactivity directives failed to process in "%1$s" due to a missing "%2$s" end tag.' ), end( $namespace_stack ), $tag_errored ); + _doing_it_wrong( __METHOD__, $message, '6.6.0' ); + return null; + } + + return $p->get_updated_html(); } /** @@ -396,17 +410,21 @@ private function process_directives_args( string $html, array &$context_stack, a * store namespace, state and context. * * @since 6.5.0 + * @since 6.6.0 The function now adds a warning when the namespace is null, falsy, or the directive value is empty. * * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. * @param string $default_namespace The default namespace to use if none is explicitly defined in the directive * value. * @param array|false $context The current context for evaluating the directive or false if there is no * context. - * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist. + * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist or the namespace is falsy. */ private function evaluate( $directive_value, string $default_namespace, $context = false ) { list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace ); - if ( empty( $path ) ) { + if ( ! $ns || ! $path ) { + /* translators: %s: The directive value referenced. */ + $message = sprintf( __( 'Namespace or reference path cannot be empty. Directive value referenced: %s' ), $directive_value ); + _doing_it_wrong( __METHOD__, $message, '6.6.0' ); return null; } diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php index ef1b79326d949..0a6ffcb2a5da0 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-bind.php @@ -154,6 +154,7 @@ public function test_wp_bind_doesnt_do_anything_on_non_existent_references() { * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_bind_ignores_empty_value() { $html = '
Text
'; @@ -167,6 +168,7 @@ public function test_wp_bind_ignores_empty_value() { * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_bind_ignores_without_value() { $html = '
Text
'; diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php index 95fdaed6f7504..b9cf16952be91 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-class.php @@ -237,6 +237,7 @@ public function test_wp_class_doesnt_change_class_attribute_with_empty_directive * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_class_doesnt_change_class_attribute_with_empty_value() { $html = '
Text
'; @@ -251,6 +252,7 @@ public function test_wp_class_doesnt_change_class_attribute_with_empty_value() { * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_class_doesnt_change_class_attribute_without_value() { $html = '
Text
'; diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php index 469e1d6e1418a..93e2528126215 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-context.php @@ -317,6 +317,7 @@ public function test_wp_context_works_with_multiple_directives() { * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_context_directive_doesnt_work_without_any_namespace() { $html = ' diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php index 42edd9f1b3838..fa166c1799d33 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php @@ -580,6 +580,8 @@ public function test_wp_each_nested_template_tags_using_previous_item_as_list() * @ticket 60356 * * @covers ::process_directives + * + * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args */ public function test_wp_each_unbalanced_tags() { $original = '' . @@ -598,6 +600,8 @@ public function test_wp_each_unbalanced_tags() { * @ticket 60356 * * @covers ::process_directives + * + * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args */ public function test_wp_each_unbalanced_tags_in_nested_template_tags() { $this->interactivity->state( 'myPlugin', array( 'list2' => array( 3, 4 ) ) ); diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php index d3ac196eae484..3b645b0dd4df3 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-style.php @@ -365,6 +365,7 @@ public function test_wp_style_doesnt_change_style_attribute_with_empty_directive * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_style_doesnt_change_style_attribute_with_empty_value() { $html = '
Text
'; @@ -379,6 +380,7 @@ public function test_wp_style_doesnt_change_style_attribute_with_empty_value() { * @ticket 60356 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_wp_style_doesnt_change_style_attribute_without_value() { $html = '
Text
'; diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php index 2fa9363ac93f4..fa283f8f2063d 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php @@ -12,7 +12,7 @@ * * @group interactivity-api */ -class Tests_WP_Interactivity_API_WP_Text extends WP_UnitTestCase { +class Tests_Interactivity_API_WpInteractivityAPIWPText extends WP_UnitTestCase { /** * Instance of WP_Interactivity_API. * @@ -131,6 +131,8 @@ public function test_wp_text_sets_inner_content_even_with_unbalanced_but_differe * @ticket 60356 * * @covers ::process_directives + * + * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args */ public function test_wp_text_fails_with_unbalanced_and_same_tags_inside_content() { $html = '
Text
'; diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index a28a6a94e62d0..a2b92e43a9b34 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -649,6 +649,8 @@ public function test_process_directives_process_the_directives_in_the_correct_or * * @dataProvider data_html_with_unbalanced_tags * + * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * * @param string $html HTML containing unbalanced tags and also a directive. */ public function test_process_directives_doesnt_change_html_if_contains_unbalanced_tags( $html ) { @@ -696,22 +698,17 @@ public function test_process_directives_changes_html_if_contains_svgs() { ); $html = '
- + Red Circle
-
'; $processed_html = $this->interactivity->process_directives( $html ); $p = new WP_HTML_Tag_Processor( $processed_html ); - $p->next_tag( 'svg' ); - $this->assertNull( $p->get_attribute( 'width' ) ); $p->next_tag( 'div' ); $this->assertEquals( 'some-id', $p->get_attribute( 'id' ) ); - $p->next_tag( 'div' ); - $this->assertEquals( '100', $p->get_attribute( 'id' ) ); } /** @@ -721,6 +718,7 @@ public function test_process_directives_changes_html_if_contains_svgs() { * @ticket 60517 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer */ public function test_process_directives_does_not_change_inner_html_in_svgs() { $this->interactivity->state( @@ -750,6 +748,7 @@ public function test_process_directives_does_not_change_inner_html_in_svgs() { * @ticket 60517 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args */ public function test_process_directives_change_html_if_contains_math() { $this->interactivity->state( @@ -784,6 +783,8 @@ public function test_process_directives_change_html_if_contains_math() { * @ticket 60517 * * @covers ::process_directives + * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer */ public function test_process_directives_does_not_change_inner_html_in_math() { $this->interactivity->state( @@ -811,10 +812,11 @@ public function test_process_directives_does_not_change_inner_html_in_math() { /** * Invokes the private `evaluate` method of WP_Interactivity_API class. * - * @param string $directive_value The directive attribute value to evaluate. + * @param string $directive_value The directive attribute value to evaluate. + * @param string $default_namespace The default namespace used with directives. * @return mixed The result of the evaluate method. */ - private function evaluate( $directive_value ) { + private function evaluate( $directive_value, $default_namespace = 'myPlugin' ) { $generate_state = function ( $name ) { $obj = new stdClass(); $obj->prop = $name; @@ -847,7 +849,7 @@ public function offsetUnset( $offset ): void {} ); $evaluate = new ReflectionMethod( $this->interactivity, 'evaluate' ); $evaluate->setAccessible( true ); - return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, 'myPlugin', $context ) ); + return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, $default_namespace, $context ) ); } /** @@ -947,6 +949,25 @@ public function test_evaluate_nested_value() { $this->assertEquals( 'otherPlugin-context-nested', $result ); } + /** + * Tests the `evaluate` method for non valid namespace values. + * + * @ticket 61044 + * + * @covers ::evaluate + * @expectedIncorrectUsage WP_Interactivity_API::evaluate + */ + public function test_evaluate_unvalid_namespaces() { + $result = $this->evaluate( 'path', 'null' ); + $this->assertNull( $result ); + + $result = $this->evaluate( 'path', '' ); + $this->assertNull( $result ); + + $result = $this->evaluate( 'path', '{}' ); + $this->assertNull( $result ); + } + /** * Tests the `kebab_to_camel_case` method. * From 21a178d75b394dd87f6e886962c3cec9fcb722f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=83=C2=B3=C3=85=E2=80=9Akowski?= Date: Tue, 4 Jun 2024 07:37:06 +0000 Subject: [PATCH 09/21] Editor: Remove unnecessary code for ensuring interactivity API dependency in block core functions Removing old code for registering the private version of the Interactivity API pre-6.5. Props czapla, gziolo, shailu25, cbravobernal. Fixes #60913. git-svn-id: https://develop.svn.wordpress.org/trunk@58322 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/deprecated.php | 27 --------------------------- src/wp-includes/script-loader.php | 4 ---- 2 files changed, 31 deletions(-) diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 863131714d579..8084cdd96cfac 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -6249,18 +6249,9 @@ function the_block_template_skip_link() { * * @since 6.4.0 * @deprecated 6.5.0 - * - * @global WP_Scripts $wp_scripts */ function block_core_query_ensure_interactivity_dependency() { _deprecated_function( __FUNCTION__, '6.5.0', 'wp_register_script_module' ); - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-query-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-query-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-query-view']->deps[] = 'wp-interactivity'; - } } /** @@ -6268,18 +6259,9 @@ function block_core_query_ensure_interactivity_dependency() { * * @since 6.4.0 * @deprecated 6.5.0 - * - * @global WP_Scripts $wp_scripts */ function block_core_file_ensure_interactivity_dependency() { _deprecated_function( __FUNCTION__, '6.5.0', 'wp_register_script_module' ); - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-file-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-file-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-file-view']->deps[] = 'wp-interactivity'; - } } /** @@ -6287,18 +6269,9 @@ function block_core_file_ensure_interactivity_dependency() { * * @since 6.4.0 * @deprecated 6.5.0 - * - * @global WP_Scripts $wp_scripts */ function block_core_image_ensure_interactivity_dependency() { _deprecated_function( __FUNCTION__, '6.5.0', 'wp_register_script_module' ); - global $wp_scripts; - if ( - isset( $wp_scripts->registered['wp-block-image-view'] ) && - ! in_array( 'wp-interactivity', $wp_scripts->registered['wp-block-image-view']->deps, true ) - ) { - $wp_scripts->registered['wp-block-image-view']->deps[] = 'wp-interactivity'; - } } /** diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 7c960a2d913a9..b62496324b344 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -283,10 +283,6 @@ function wp_default_packages_scripts( $scripts ) { */ $assets = include ABSPATH . WPINC . "/assets/script-loader-packages{$suffix}.php"; - // Add the private version of the Interactivity API manually. - $scripts->add( 'wp-interactivity', '/wp-includes/js/dist/interactivity.min.js' ); - did_action( 'init' ) && $scripts->add_data( 'wp-interactivity', 'strategy', 'defer' ); - foreach ( $assets as $file_name => $package_data ) { $basename = str_replace( $suffix . '.js', '', basename( $file_name ) ); $handle = 'wp-' . $basename; From a9a32b4385a029d9bdae11b0a08d8e9158b972d5 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 4 Jun 2024 08:13:01 +0000 Subject: [PATCH 10/21] Editor: Fix block template files query for a post-type. See https://github.com/WordPress/gutenberg/pull/61244. See https://github.com/WordPress/wordpress-develop/pull/6468. Fixes #61110. Props mamaduka, mukesh27, grantmkin, vcanales, ellatrix, oandregal. git-svn-id: https://develop.svn.wordpress.org/trunk@58323 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-template-utils.php | 12 ++++++++++++ tests/phpunit/tests/block-template.php | 1 + tests/phpunit/tests/blocks/getBlockTemplates.php | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 58ee129d2a1c7..10ba863c62218 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -362,6 +362,11 @@ function _get_block_templates_files( $template_type, $query = array() ) { return null; } + $default_template_types = array(); + if ( 'wp_template' === $template_type ) { + $default_template_types = get_default_block_template_types(); + } + // Prepare metadata from $query. $slugs_to_include = isset( $query['slug__in'] ) ? $query['slug__in'] : array(); $slugs_to_skip = isset( $query['slug__not_in'] ) ? $query['slug__not_in'] : array(); @@ -425,12 +430,19 @@ function _get_block_templates_files( $template_type, $query = array() ) { if ( 'wp_template' === $template_type ) { $candidate = _add_block_template_info( $new_template_item ); + $is_custom = ! isset( $default_template_types[ $candidate['slug'] ] ); + if ( ! $post_type || ( $post_type && isset( $candidate['postTypes'] ) && in_array( $post_type, $candidate['postTypes'], true ) ) ) { $template_files[ $template_slug ] = $candidate; } + + // The custom templates with no associated post types are available for all post types. + if ( $post_type && ! isset( $candidate['postTypes'] ) && $is_custom ) { + $template_files[ $template_slug ] = $candidate; + } } } } diff --git a/tests/phpunit/tests/block-template.php b/tests/phpunit/tests/block-template.php index dcc3f6282dd40..708c7be779ef3 100644 --- a/tests/phpunit/tests/block-template.php +++ b/tests/phpunit/tests/block-template.php @@ -399,6 +399,7 @@ public function test_get_block_templates_paths_dir_exists() { // Templates in the current theme. $templates = array( 'parts/small-header.html', + 'templates/custom-hero-template.html', 'templates/custom-single-post-template.html', 'templates/index.html', 'templates/page-home.html', diff --git a/tests/phpunit/tests/blocks/getBlockTemplates.php b/tests/phpunit/tests/blocks/getBlockTemplates.php index 92e0f14821ba2..4db9093aae4a2 100644 --- a/tests/phpunit/tests/blocks/getBlockTemplates.php +++ b/tests/phpunit/tests/blocks/getBlockTemplates.php @@ -188,6 +188,7 @@ public function data_get_block_templates_returns_unique_entities() { /** * @dataProvider data_get_block_templates_should_respect_posttypes_property * @ticket 55881 + * @ticket 61110 * * @param string $post_type Post type for query. * @param array $expected Expected template IDs. @@ -204,6 +205,9 @@ public function test_get_block_templates_should_respect_posttypes_property( $pos /** * Data provider. * + * The `custom-hero-template` is intentionally omitted from the theme.json's `customTemplates`. + * See: https://core.trac.wordpress.org/ticket/61110. + * * @return array */ public function data_get_block_templates_should_respect_posttypes_property() { @@ -211,12 +215,14 @@ public function data_get_block_templates_should_respect_posttypes_property() { 'post' => array( 'post_type' => 'post', 'expected' => array( + 'block-theme//custom-hero-template', 'block-theme//custom-single-post-template', ), ), 'page' => array( 'post_type' => 'page', 'expected' => array( + 'block-theme//custom-hero-template', 'block-theme//page-home', ), ), From a0e93ac20f90141b197cd98e8d053449bc5fe3f7 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 4 Jun 2024 08:49:24 +0000 Subject: [PATCH 11/21] Editor: add missing test template file. Add a missing file for r58323. See https://github.com/WordPress/wordpress-develop/pull/6468. See #61110. git-svn-id: https://develop.svn.wordpress.org/trunk@58324 602fd350-edb4-49c9-b593-d223f7449a82 --- .../themedir1/block-theme/templates/custom-hero-template.html | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/phpunit/data/themedir1/block-theme/templates/custom-hero-template.html diff --git a/tests/phpunit/data/themedir1/block-theme/templates/custom-hero-template.html b/tests/phpunit/data/themedir1/block-theme/templates/custom-hero-template.html new file mode 100644 index 0000000000000..0a62cda60641a --- /dev/null +++ b/tests/phpunit/data/themedir1/block-theme/templates/custom-hero-template.html @@ -0,0 +1,3 @@ + +

Custom Hero template

+ From 98281eeb4c74e62cad427fc1e836158b6bb66ba8 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 4 Jun 2024 09:11:19 +0000 Subject: [PATCH 12/21] Quick/Bulk Edit: Adjust label width to accommodate longer translations. Specifically handles `de_*` locales. Previously: [33598] / #33212. Props zodiac1978, oglekler. Fixes #60851. git-svn-id: https://develop.svn.wordpress.org/trunk@58325 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/l10n.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wp-admin/css/l10n.css b/src/wp-admin/css/l10n.css index 967fb4c6c6553..311892ac7a6bb 100644 --- a/src/wp-admin/css/l10n.css +++ b/src/wp-admin/css/l10n.css @@ -66,6 +66,12 @@ body.locale-he-il .press-this a.wp-switch-editor { .locale-de-de-formal #customize-header-actions .spinner { margin: 16px 3px 0; /* default 16px 4px 0 5px */ } +body[class*="locale-de-"] .inline-edit-row fieldset label span.title { + width: 7em; /* default 6em */ +} +body[class*="locale-de-"] .inline-edit-row fieldset label span.title{ + margin-left: 7em; /* default 6em */ +} /* ru_RU: Text needs more room to breathe. */ .locale-ru-ru #adminmenu { From 278ec2fb3a0c82ff7a32cdd705b7d2fca98928f2 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 4 Jun 2024 10:21:11 +0000 Subject: [PATCH 13/21] REST API: Add post class list field. See https://github.com/WordPress/gutenberg/pull/60642. See https://github.com/WordPress/wordpress-develop/pull/6716. Fixes #61360. Props antonvlasenko, timothyblynjacobs, ellatrix, oandregal. git-svn-id: https://develop.svn.wordpress.org/trunk@58326 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-posts-controller.php | 14 ++++++ .../rest-api/rest-attachments-controller.php | 3 +- .../tests/rest-api/rest-pages-controller.php | 3 +- .../tests/rest-api/rest-posts-controller.php | 5 +- .../tests/rest-api/rest-schema-setup.php | 24 +++++---- tests/qunit/fixtures/wp-api-generated.js | 50 ++++++++++++++++++- 6 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index ec1588b02fe57..a29f92c56fec7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -1998,6 +1998,10 @@ public function prepare_item_for_response( $item, $request ) { $data['generated_slug'] = $sample_permalink[1]; } } + + if ( rest_is_field_included( 'class_list', $fields ) ) { + $data['class_list'] = get_post_class( array(), $post->ID ); + } } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -2353,6 +2357,16 @@ public function get_item_schema() { 'context' => array( 'edit' ), 'readonly' => true, ); + + $schema['properties']['class_list'] = array( + 'description' => __( 'An array of the class names for the post container element.' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ); } if ( $post_type_obj->hierarchical ) { diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 956e8afd300b9..feaf1900d0830 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1646,7 +1646,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 28, $properties ); + $this->assertCount( 29, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'alt_text', $properties ); $this->assertArrayHasKey( 'caption', $properties ); @@ -1681,6 +1681,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'type', $properties ); $this->assertArrayHasKey( 'missing_image_sizes', $properties ); $this->assertArrayHasKey( 'featured_media', $properties ); + $this->assertArrayHasKey( 'class_list', $properties ); } public function test_get_additional_field_registration() { diff --git a/tests/phpunit/tests/rest-api/rest-pages-controller.php b/tests/phpunit/tests/rest-api/rest-pages-controller.php index 75c005b275152..209229256a11f 100644 --- a/tests/phpunit/tests/rest-api/rest-pages-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pages-controller.php @@ -748,7 +748,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 24, $properties ); + $this->assertCount( 25, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'comment_status', $properties ); $this->assertArrayHasKey( 'content', $properties ); @@ -773,6 +773,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'template', $properties ); $this->assertArrayHasKey( 'title', $properties ); $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'class_list', $properties ); } public function filter_theme_page_templates( $page_templates ) { diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 12fac4dbebfa2..8b81b9f648651 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -4362,7 +4362,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 26, $properties ); + $this->assertCount( 27, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'comment_status', $properties ); $this->assertArrayHasKey( 'content', $properties ); @@ -4389,6 +4389,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'type', $properties ); $this->assertArrayHasKey( 'tags', $properties ); $this->assertArrayHasKey( 'categories', $properties ); + $this->assertArrayHasKey( 'class_list', $properties ); } /** @@ -4418,6 +4419,7 @@ public function test_get_post_view_context_properties() { $expected_keys = array( 'author', 'categories', + 'class_list', 'comment_status', 'content', 'date', @@ -4456,6 +4458,7 @@ public function test_get_post_edit_context_properties() { $expected_keys = array( 'author', 'categories', + 'class_list', 'comment_status', 'content', 'date', diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 3296f15905c44..1569e24e42f3b 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -759,17 +759,23 @@ private function normalize_fixture( $data, $path ) { return $data; } + $datetime_keys = array( 'date', 'date_gmt', 'modified', 'modified_gmt' ); + foreach ( $data as $key => $value ) { - if ( is_string( $value ) && ( - 'date' === $key || - 'date_gmt' === $key || - 'modified' === $key || - 'modified_gmt' === $key - ) ) { - $data[ $key ] = '2017-02-14T00:00:00'; - } else { - $data[ $key ] = $this->normalize_fixture( $value, "$path.$key" ); + if ( is_string( $value ) ) { + if ( in_array( $key, $datetime_keys, true ) ) { + $data[ $key ] = '2017-02-14T00:00:00'; + continue; + } + + if ( 1 === preg_match( '/^post-\d+$/', $value ) ) { + // Normalize the class value to ensure test stability. + $data[ $key ] = 'post-1073'; + continue; + } } + + $data[ $key ] = $this->normalize_fixture( $value, "$path.$key" ); } return $data; diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index ed7842acd09c4..250c64cff1e5d 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12322,6 +12322,15 @@ mockedApiResponse.PostsCollection = [ 1 ], "tags": [], + "class_list": [ + "post-1073", + "post", + "type-post", + "status-publish", + "format-standard", + "hentry", + "category-uncategorized" + ], "_links": { "self": [ { @@ -12421,7 +12430,16 @@ mockedApiResponse.PostModel = { "categories": [ 1 ], - "tags": [] + "tags": [], + "class_list": [ + "post-1073", + "post", + "type-post", + "status-publish", + "format-standard", + "hentry", + "category-uncategorized" + ] }; mockedApiResponse.postRevisions = [ @@ -12613,6 +12631,13 @@ mockedApiResponse.PagesCollection = [ "meta": { "meta_key": "" }, + "class_list": [ + "post-1073", + "page", + "type-page", + "status-publish", + "hentry" + ], "_links": { "self": [ { @@ -12696,7 +12721,14 @@ mockedApiResponse.PageModel = { "template": "", "meta": { "meta_key": "" - } + }, + "class_list": [ + "post-1073", + "page", + "type-page", + "status-publish", + "hentry" + ] }; mockedApiResponse.pageRevisions = [ @@ -12878,6 +12910,13 @@ mockedApiResponse.MediaCollection = [ "meta": { "meta_key": "" }, + "class_list": [ + "post-1073", + "attachment", + "type-attachment", + "status-inherit", + "hentry" + ], "description": { "rendered": "

" }, @@ -12940,6 +12979,13 @@ mockedApiResponse.MediaModel = { "meta": { "meta_key": "" }, + "class_list": [ + "post-1073", + "attachment", + "type-attachment", + "status-inherit", + "hentry" + ], "description": { "rendered": "

" }, From 3a9ecd2cc84fe24e2db8b537a73314c5398b5312 Mon Sep 17 00:00:00 2001 From: gziolo Date: Tue, 4 Jun 2024 10:59:01 +0000 Subject: [PATCH 14/21] Interactivity API: Directives cannot derive state on the server The Interactivity API has a concept of "derived state" but it only worked on the client (JavaScript). This is the implementation that mirrors it, so derived state has good server-side solution. Props jonsurrell, darerodz, gziolo, luisherranz, cbravobernal. Fixes #61037. git-svn-id: https://develop.svn.wordpress.org/trunk@58327 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-interactivity-api.php | 256 ++++++--- .../interactivity-api/interactivity-api.php | 24 +- .../wpInteractivityAPI-wp-each.php | 4 +- .../wpInteractivityAPI-wp-text.php | 2 +- .../interactivity-api/wpInteractivityAPI.php | 526 ++++++++++++++++-- 5 files changed, 706 insertions(+), 106 deletions(-) diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 77abe19244246..3924e877638a7 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -73,6 +73,28 @@ final class WP_Interactivity_API { */ private $has_processed_router_region = false; + /** + * Stack of namespaces defined by `data-wp-interactive` directives, in + * the order they are processed. + * + * This is only available during directive processing, otherwise it is `null`. + * + * @since 6.6.0 + * @var array|null + */ + private $namespace_stack = null; + + /** + * Stack of contexts defined by `data-wp-context` directives, in + * the order they are processed. + * + * This is only available during directive processing, otherwise it is `null`. + * + * @since 6.6.0 + * @var array>|null + */ + private $context_stack = null; + /** * Gets and/or sets the initial state of an Interactivity API store for a * given namespace. @@ -80,15 +102,47 @@ final class WP_Interactivity_API { * If state for that store namespace already exists, it merges the new * provided state with the existing one. * + * When no namespace is specified, it returns the state defined for the + * current value in the internal namespace stack during a `process_directives` call. + * * @since 6.5.0 + * @since 6.6.0 The `$store_namespace` param is optional. * - * @param string $store_namespace The unique store namespace identifier. + * @param string $store_namespace Optional. The unique store namespace identifier. * @param array $state Optional. The array that will be merged with the existing state for the specified * store namespace. * @return array The current state for the specified store namespace. This will be the updated state if a $state * argument was provided. */ - public function state( string $store_namespace, array $state = array() ): array { + public function state( ?string $store_namespace = null, ?array $state = null ): array { + if ( ! $store_namespace ) { + if ( $state ) { + _doing_it_wrong( + __METHOD__, + __( 'The namespace is required when state data is passed.' ), + '6.6.0' + ); + return array(); + } + if ( null !== $store_namespace ) { + _doing_it_wrong( + __METHOD__, + __( 'The namespace should be a non-empty string.' ), + '6.6.0' + ); + return array(); + } + if ( null === $this->namespace_stack ) { + _doing_it_wrong( + __METHOD__, + __( 'The namespace can only be omitted during directive processing.' ), + '6.6.0' + ); + return array(); + } + + $store_namespace = end( $this->namespace_stack ); + } if ( ! isset( $this->state_data[ $store_namespace ] ) ) { $this->state_data[ $store_namespace ] = array(); } @@ -211,6 +265,46 @@ public function print_client_interactivity_data() { } } + /** + * Returns the latest value on the context stack with the passed namespace. + * + * When the namespace is omitted, it uses the current namespace on the + * namespace stack during a `process_directives` call. + * + * @since 6.6.0 + * + * @param string $store_namespace Optional. The unique store namespace identifier. + */ + public function get_context( ?string $store_namespace = null ): array { + if ( null === $this->context_stack ) { + _doing_it_wrong( + __METHOD__, + __( 'The context can only be read during directive processing.' ), + '6.6.0' + ); + return array(); + } + + if ( ! $store_namespace ) { + if ( null !== $store_namespace ) { + _doing_it_wrong( + __METHOD__, + __( 'The namespace should be a non-empty string.' ), + '6.6.0' + ); + return array(); + } + + $store_namespace = end( $this->namespace_stack ); + } + + $context = end( $this->context_stack ); + + return ( $store_namespace && $context && isset( $context[ $store_namespace ] ) ) + ? $context[ $store_namespace ] + : array(); + } + /** * Registers the `@wordpress/interactivity` script modules. * @@ -258,9 +352,14 @@ public function process_directives( string $html ): string { return $html; } - $context_stack = array(); - $namespace_stack = array(); - $result = $this->process_directives_args( $html, $context_stack, $namespace_stack ); + $this->namespace_stack = array(); + $this->context_stack = array(); + + $result = $this->_process_directives( $html ); + + $this->namespace_stack = null; + $this->context_stack = null; + return null === $result ? $html : $result; } @@ -268,18 +367,17 @@ public function process_directives( string $html ): string { * Processes the interactivity directives contained within the HTML content * and updates the markup accordingly. * - * It needs the context and namespace stacks to be passed by reference, and - * it returns null if the HTML contains unbalanced tags. + * It uses the WP_Interactivity_API instance's context and namespace stacks, + * which are shared between all calls. * - * @since 6.5.0 - * @since 6.6.0 The function displays a warning message when the HTML contains unbalanced tags or a directive appears in a MATH or SVG tag. + * This method returns null if the HTML contains unbalanced tags. * - * @param string $html The HTML content to process. - * @param array $context_stack The reference to the array used to keep track of contexts during processing. - * @param array $namespace_stack The reference to the array used to manage namespaces during processing. + * @since 6.6.0 + * + * @param string $html The HTML content to process. * @return string|null The processed HTML content. It returns null when the HTML contains unbalanced tags. */ - private function process_directives_args( string $html, array &$context_stack, array &$namespace_stack ) { + private function _process_directives( string $html ) { $p = new WP_Interactivity_API_Directives_Processor( $html ); $tag_stack = array(); $unbalanced = false; @@ -287,6 +385,13 @@ private function process_directives_args( string $html, array &$context_stack, a $directive_processor_prefixes = array_keys( self::$directive_processors ); $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes ); + /* + * Save the current size for each stack to restore them in case + * the processing finds unbalanced tags. + */ + $namespace_stack_size = count( $this->namespace_stack ); + $context_stack_size = count( $this->context_stack ); + while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag_name = $p->get_tag(); @@ -298,7 +403,7 @@ private function process_directives_args( string $html, array &$context_stack, a if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) { if ( $p->get_attribute_names_with_prefix( 'data-wp-' ) ) { /* translators: 1: SVG or MATH HTML tag, 2: Namespace of the interactive block. */ - $message = sprintf( __( 'Interactivity directives were detected on an incompatible %1$s tag when processing "%2$s". These directives will be ignored in the server side render.' ), $tag_name, end( $namespace_stack ) ); + $message = sprintf( __( 'Interactivity directives were detected on an incompatible %1$s tag when processing "%2$s". These directives will be ignored in the server side render.' ), $tag_name, end( $this->namespace_stack ) ); _doing_it_wrong( __METHOD__, $message, '6.6.0' ); } $p->skip_to_tag_closer(); @@ -381,13 +486,17 @@ private function process_directives_args( string $html, array &$context_stack, a ? self::$directive_processors[ $directive_prefix ] : array( $this, self::$directive_processors[ $directive_prefix ] ); - call_user_func_array( - $func, - array( $p, $mode, &$context_stack, &$namespace_stack, &$tag_stack ) - ); + call_user_func_array( $func, array( $p, $mode, &$tag_stack ) ); } } } + + if ( $unbalanced ) { + // Reset the namespace and context stacks to their previous values. + array_splice( $this->namespace_stack, $namespace_stack_size ); + array_splice( $this->context_stack, $context_stack_size ); + } + /* * It returns null if the HTML is unbalanced because unbalanced HTML is * not safe to process. In that case, the Interactivity API runtime will @@ -397,7 +506,7 @@ private function process_directives_args( string $html, array &$context_stack, a if ( $unbalanced || 0 < count( $tag_stack ) ) { $tag_errored = 0 < count( $tag_stack ) ? end( $tag_stack )[0] : $tag_name; /* translators: %1s: Namespace processed, %2s: The tag that caused the error; could be any HTML tag. */ - $message = sprintf( __( 'Interactivity directives failed to process in "%1$s" due to a missing "%2$s" end tag.' ), end( $namespace_stack ), $tag_errored ); + $message = sprintf( __( 'Interactivity directives failed to process in "%1$s" due to a missing "%2$s" end tag.' ), end( $this->namespace_stack ), $tag_errored ); _doing_it_wrong( __METHOD__, $message, '6.6.0' ); return null; } @@ -411,15 +520,15 @@ private function process_directives_args( string $html, array &$context_stack, a * * @since 6.5.0 * @since 6.6.0 The function now adds a warning when the namespace is null, falsy, or the directive value is empty. + * @since 6.6.0 Removed `default_namespace` and `context` arguments. * - * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. - * @param string $default_namespace The default namespace to use if none is explicitly defined in the directive - * value. - * @param array|false $context The current context for evaluating the directive or false if there is no - * context. + * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist or the namespace is falsy. */ - private function evaluate( $directive_value, string $default_namespace, $context = false ) { + private function evaluate( $directive_value ) { + $default_namespace = end( $this->namespace_stack ); + $context = end( $this->context_stack ); + list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace ); if ( ! $ns || ! $path ) { /* translators: %s: The directive value referenced. */ @@ -450,6 +559,33 @@ private function evaluate( $directive_value, string $default_namespace, $context } } + if ( $current instanceof Closure ) { + /* + * This state getter's namespace is added to the stack so that + * `state()` or `get_config()` read that namespace when called + * without specifying one. + */ + array_push( $this->namespace_stack, $ns ); + try { + $current = $current(); + } catch ( Throwable $e ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: Path pointing to an Interactivity API state property, 2: Namespace for an Interactivity API store. */ + __( 'Uncaught error executing a derived state callback with path "%1$s" and namespace "%2$s".' ), + $path, + $ns + ), + '6.6.0' + ); + return null; + } finally { + // Remove the property's namespace from the stack. + array_pop( $this->namespace_stack ); + } + } + // Returns the opposite if it contains a negation operator (!). return $should_negate_value ? ! $current : $current; } @@ -551,15 +687,13 @@ function ( $matches ) { * * @since 6.5.0 * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + * @param string $mode Whether the processing is entering or exiting the tag. */ - private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { // When exiting tags, it removes the last namespace from the stack. if ( 'exit' === $mode ) { - array_pop( $namespace_stack ); + array_pop( $this->namespace_stack ); return; } @@ -583,9 +717,9 @@ private function data_wp_interactive_processor( WP_Interactivity_API_Directives_ $new_namespace = $attribute_value; } } - $namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) ) + $this->namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) ) ? $new_namespace - : end( $namespace_stack ); + : end( $this->namespace_stack ); } /** @@ -598,18 +732,16 @@ private function data_wp_interactive_processor( WP_Interactivity_API_Directives_ * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. */ - private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { // When exiting tags, it removes the last context from the stack. if ( 'exit' === $mode ) { - array_pop( $context_stack ); + array_pop( $this->context_stack ); return; } $attribute_value = $p->get_attribute( 'data-wp-context' ); - $namespace_value = end( $namespace_stack ); + $namespace_value = end( $this->namespace_stack ); // Separates the namespace from the context JSON object. list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value ) @@ -621,8 +753,8 @@ private function data_wp_context_processor( WP_Interactivity_API_Directives_Proc * previous context with the new one. */ if ( is_string( $namespace_value ) ) { - $context_stack[] = array_replace_recursive( - end( $context_stack ) !== false ? end( $context_stack ) : array(), + $this->context_stack[] = array_replace_recursive( + end( $this->context_stack ) !== false ? end( $this->context_stack ) : array(), array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() ) ); } else { @@ -631,7 +763,7 @@ private function data_wp_context_processor( WP_Interactivity_API_Directives_Proc * It needs to do so because the function pops out the current context * from the stack whenever it finds a `data-wp-context`'s closing tag. */ - $context_stack[] = end( $context_stack ); + $this->context_stack[] = end( $this->context_stack ); } } @@ -645,10 +777,8 @@ private function data_wp_context_processor( WP_Interactivity_API_Directives_Proc * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. */ - private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' ); @@ -659,7 +789,7 @@ private function data_wp_bind_processor( WP_Interactivity_API_Directives_Process } $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + $result = $this->evaluate( $attribute_value ); if ( null !== $result && @@ -699,10 +829,8 @@ private function data_wp_bind_processor( WP_Interactivity_API_Directives_Process * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. */ - private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' ); @@ -713,7 +841,7 @@ private function data_wp_class_processor( WP_Interactivity_API_Directives_Proces } $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + $result = $this->evaluate( $attribute_value ); if ( $result ) { $p->add_class( $class_name ); @@ -734,10 +862,8 @@ private function data_wp_class_processor( WP_Interactivity_API_Directives_Proces * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. */ - private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' ); @@ -748,7 +874,7 @@ private function data_wp_style_processor( WP_Interactivity_API_Directives_Proces } $directive_attribute_value = $p->get_attribute( $attribute_name ); - $style_property_value = $this->evaluate( $directive_attribute_value, end( $namespace_stack ), end( $context_stack ) ); + $style_property_value = $this->evaluate( $directive_attribute_value ); $style_attribute_value = $p->get_attribute( 'style' ); $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : ''; @@ -827,13 +953,11 @@ private function merge_style_property( string $style_attribute_value, string $st * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. */ - private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { + private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { if ( 'enter' === $mode ) { $attribute_value = $p->get_attribute( 'data-wp-text' ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + $result = $this->evaluate( $attribute_value ); /* * Follows the same logic as Preact in the client and only changes the @@ -965,17 +1089,15 @@ private function data_wp_router_region_processor( WP_Interactivity_API_Directive * * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. * @param array $tag_stack The reference to the tag stack. */ - private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack, array &$tag_stack ) { + private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$tag_stack ) { if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) { $attribute_name = $p->get_attribute_names_with_prefix( 'data-wp-each' )[0]; $extracted_suffix = $this->extract_prefix_and_suffix( $attribute_name ); $item_name = isset( $extracted_suffix[1] ) ? $this->kebab_to_camel_case( $extracted_suffix[1] ) : 'item'; $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); + $result = $this->evaluate( $attribute_value ); // Gets the content between the template tags and leaves the cursor in the closer tag. $inner_content = $p->get_content_between_balanced_template_tags(); @@ -1009,7 +1131,7 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process } // Extracts the namespace from the directive attribute value. - $namespace_value = end( $namespace_stack ); + $namespace_value = end( $this->namespace_stack ); list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value ) ? $this->extract_directive_value( $attribute_value, $namespace_value ) : array( $namespace_value, null ); @@ -1018,17 +1140,17 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process $processed_content = ''; foreach ( $result as $item ) { // Creates a new context that includes the current item of the array. - $context_stack[] = array_replace_recursive( - end( $context_stack ) !== false ? end( $context_stack ) : array(), + $this->context_stack[] = array_replace_recursive( + end( $this->context_stack ) !== false ? end( $this->context_stack ) : array(), array( $namespace_value => array( $item_name => $item ) ) ); // Processes the inner content with the new context. - $processed_item = $this->process_directives_args( $inner_content, $context_stack, $namespace_stack ); + $processed_item = $this->_process_directives( $inner_content ); if ( null === $processed_item ) { // If the HTML is unbalanced, stop processing it. - array_pop( $context_stack ); + array_pop( $this->context_stack ); return; } @@ -1041,7 +1163,7 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process $processed_content .= $i->get_updated_html(); // Removes the current context from the stack. - array_pop( $context_stack ); + array_pop( $this->context_stack ); } // Appends the processed content after the tag closer of the template. diff --git a/src/wp-includes/interactivity-api/interactivity-api.php b/src/wp-includes/interactivity-api/interactivity-api.php index 763440e1e3a07..e007d512f9bb4 100644 --- a/src/wp-includes/interactivity-api/interactivity-api.php +++ b/src/wp-includes/interactivity-api/interactivity-api.php @@ -47,7 +47,11 @@ function wp_interactivity_process_directives( string $html ): string { * If state for that store namespace already exists, it merges the new * provided state with the existing one. * + * The namespace can be omitted inside derived state getters, using the + * namespace where the getter is defined. + * * @since 6.5.0 + * @since 6.6.0 The namespace can be omitted when called inside derived state getters. * * @param string $store_namespace The unique store namespace identifier. * @param array $state Optional. The array that will be merged with the existing state for the specified @@ -55,7 +59,7 @@ function wp_interactivity_process_directives( string $html ): string { * @return array The state for the specified store namespace. This will be the updated state if a $state argument was * provided. */ -function wp_interactivity_state( string $store_namespace, array $state = array() ): array { +function wp_interactivity_state( ?string $store_namespace = null, array $state = array() ): array { return wp_interactivity()->state( $store_namespace, $state ); } @@ -103,3 +107,21 @@ function wp_interactivity_data_wp_context( array $context, string $store_namespa ( empty( $context ) ? '{}' : wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ) . '\''; } + +/** + * Gets the current Interactivity API context for a given namespace. + * + * The function should be used only during directive processing. If the + * `$store_namespace` parameter is omitted, it uses the current namespace value + * on the internal namespace stack. + * + * It returns an empty array when the specified namespace is not defined. + * + * @since 6.6.0 + * + * @param string $store_namespace Optional. The unique store namespace identifier. + * @return array The context for the specified store namespace. + */ +function wp_interactivity_get_context( ?string $store_namespace = null ): array { + return wp_interactivity()->get_context( $store_namespace ); +} diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php index fa166c1799d33..4628ec702a67b 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-each.php @@ -581,7 +581,7 @@ public function test_wp_each_nested_template_tags_using_previous_item_as_list() * * @covers ::process_directives * - * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API::_process_directives */ public function test_wp_each_unbalanced_tags() { $original = '' . @@ -601,7 +601,7 @@ public function test_wp_each_unbalanced_tags() { * * @covers ::process_directives * - * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API::_process_directives */ public function test_wp_each_unbalanced_tags_in_nested_template_tags() { $this->interactivity->state( 'myPlugin', array( 'list2' => array( 3, 4 ) ) ); diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php index fa283f8f2063d..d031698707960 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI-wp-text.php @@ -132,7 +132,7 @@ public function test_wp_text_sets_inner_content_even_with_unbalanced_but_differe * * @covers ::process_directives * - * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API::_process_directives */ public function test_wp_text_fails_with_unbalanced_and_same_tags_inside_content() { $html = '
Text
'; diff --git a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php index a2b92e43a9b34..25c8c2dc3abce 100644 --- a/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php +++ b/tests/phpunit/tests/interactivity-api/wpInteractivityAPI.php @@ -31,6 +31,34 @@ public function charset_iso_8859_1() { return 'iso-8859-1'; } + /** + * Modifies the internal namespace stack as if the WP_Interactivity_API + * instance had found `data-wp-interactive` directives during + * `process_directives` execution. + * + * @param array $stack Values for the internal namespace stack. + */ + private function set_internal_namespace_stack( ...$stack ) { + $interactivity = new ReflectionClass( $this->interactivity ); + $namespace_stack = $interactivity->getProperty( 'namespace_stack' ); + $namespace_stack->setAccessible( true ); + $namespace_stack->setValue( $this->interactivity, $stack ); + } + + /** + * Modifies the internal context stack as if the WP_Interactivity_API + * instance had found `data-wp-context` directives during + * `process_directives` execution. + * + * @param array> $stack Values for the internal context stack. + */ + private function set_internal_context_stack( ...$stack ) { + $interactivity = new ReflectionClass( $this->interactivity ); + $context_stack = $interactivity->getProperty( 'context_stack' ); + $context_stack->setAccessible( true ); + $context_stack->setValue( $this->interactivity, $stack ); + } + /** * Tests that the state and config methods return an empty array at the * beginning. @@ -425,6 +453,222 @@ public function test_state_and_config_escape_special_characters_non_utf8() { $this->assertEquals( $expected, $interactivity_data_string[1] ); } + /** + * Test that calling state without a namespace arg returns the state data + * for the current namespace in the internal namespace stack. + * + * @ticket 61037 + * + * @covers ::state + */ + public function test_state_without_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + + $this->interactivity->state( 'myPlugin', array( 'a' => 1 ) ); + $this->interactivity->state( 'otherPlugin', array( 'b' => 2 ) ); + + $this->assertEquals( + array( 'a' => 1 ), + $this->interactivity->state() + ); + } + + /** + * Test that passing state data without a valid namespace does nothing and + * just returns an empty array. + * + * @ticket 61037 + * + * @covers ::state + * @expectedIncorrectUsage WP_Interactivity_API::state + */ + public function test_state_with_data_and_invalid_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + + $this->interactivity->state( 'myPlugin', array( 'a' => 1 ) ); + $this->interactivity->state( 'otherPlugin', array( 'b' => 2 ) ); + + $this->assertEquals( + array(), + $this->interactivity->state( null, array( 'newProp' => 'value' ) ) + ); + } + + /** + * Test that calling state with an empty string as namespace is not allowed. + * + * @ticket 61037 + * + * @covers ::state + * @expectedIncorrectUsage WP_Interactivity_API::state + */ + public function test_state_with_empty_string_as_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + + $this->interactivity->state( 'myPlugin', array( 'a' => 1 ) ); + $this->interactivity->state( 'otherPlugin', array( 'b' => 2 ) ); + + $this->assertEquals( + array(), + $this->interactivity->state( '' ) + ); + } + + /** + * Tests that calling state without namespace outside of + * `process_directives` execution is not allowed. + * + * @ticket 61037 + * + * @covers ::state + * @expectedIncorrectUsage WP_Interactivity_API::state + */ + public function test_state_without_namespace_outside_directive_processing() { + $this->assertEquals( + array(), + $this->interactivity->state() + ); + } + + /** + * Test that `get_context` returns the latest context value for the given + * namespace. + * + * @ticket 61037 + * + * @covers ::get_context + */ + public function test_get_context_with_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'a' => 0 ), + ), + array( + 'myPlugin' => array( 'a' => 1 ), + 'otherPlugin' => array( 'b' => 2 ), + ) + ); + + $this->assertEquals( + array( 'a' => 1 ), + $this->interactivity->get_context( 'myPlugin' ) + ); + $this->assertEquals( + array( 'b' => 2 ), + $this->interactivity->get_context( 'otherPlugin' ) + ); + } + + /** + * Test that `get_context` uses the current namespace in the internal + * namespace stack when the parameter is omitted. + * + * @ticket 61037 + * + * @covers ::get_context + */ + public function test_get_context_without_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'a' => 0 ), + ), + array( + 'myPlugin' => array( 'a' => 1 ), + 'otherPlugin' => array( 'b' => 2 ), + ) + ); + + $this->assertEquals( + array( 'a' => 1 ), + $this->interactivity->get_context() + ); + } + + /** + * Test that `get_context` returns an empty array when the context stack is + * empty. + * + * @ticket 61037 + * + * @covers ::get_context + */ + public function test_get_context_with_empty_context_stack() { + $this->set_internal_namespace_stack( 'myPlugin' ); + $this->set_internal_context_stack(); + + $this->assertEquals( + array(), + $this->interactivity->get_context( 'myPlugin' ) + ); + } + + /** + * Test that `get_context` returns an empty array if the given namespace is + * not defined. + * + * @ticket 61037 + * + * @covers ::get_context + */ + public function test_get_context_with_undefined_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'a' => 0 ), + ), + array( + 'myPlugin' => array( 'a' => 1 ), + ) + ); + + $this->assertEquals( + array(), + $this->interactivity->get_context( 'otherPlugin' ) + ); + } + + /** + * Test that `get_context` should not be called with an empty string. + * + * @ticket 61037 + * + * @covers ::get_context + * @expectedIncorrectUsage WP_Interactivity_API::get_context + */ + public function test_get_context_with_empty_namespace() { + $this->set_internal_namespace_stack( 'myPlugin' ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'a' => 0 ), + ), + array( + 'myPlugin' => array( 'a' => 1 ), + ) + ); + + $this->assertEquals( + array(), + $this->interactivity->get_context( '' ) + ); + } + + + /** + * Tests that `get_context` should not be called outside of + * `process_directives` execution. + * + * @ticket 61037 + * + * @covers ::get_context + * @expectedIncorrectUsage WP_Interactivity_API::get_context + */ + public function test_get_context_outside_of_directive_processing() { + $context = $this->interactivity->get_context(); + $this->assertEquals( array(), $context ); + } + /** * Tests extracting directive values from different string formats. * @@ -649,7 +893,7 @@ public function test_process_directives_process_the_directives_in_the_correct_or * * @dataProvider data_html_with_unbalanced_tags * - * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API::_process_directives * * @param string $html HTML containing unbalanced tags and also a directive. */ @@ -748,7 +992,7 @@ public function test_process_directives_does_not_change_inner_html_in_svgs() { * @ticket 60517 * * @covers ::process_directives - * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API::_process_directives */ public function test_process_directives_change_html_if_contains_math() { $this->interactivity->state( @@ -783,7 +1027,7 @@ public function test_process_directives_change_html_if_contains_math() { * @ticket 60517 * * @covers ::process_directives - * @expectedIncorrectUsage WP_Interactivity_API::process_directives_args + * @expectedIncorrectUsage WP_Interactivity_API::_process_directives * @expectedIncorrectUsage WP_Interactivity_API_Directives_Processor::skip_to_tag_closer */ public function test_process_directives_does_not_change_inner_html_in_math() { @@ -813,24 +1057,51 @@ public function test_process_directives_does_not_change_inner_html_in_math() { * Invokes the private `evaluate` method of WP_Interactivity_API class. * * @param string $directive_value The directive attribute value to evaluate. - * @param string $default_namespace The default namespace used with directives. * @return mixed The result of the evaluate method. */ - private function evaluate( $directive_value, $default_namespace = 'myPlugin' ) { - $generate_state = function ( $name ) { - $obj = new stdClass(); - $obj->prop = $name; - return array( - 'key' => $name, - 'nested' => array( 'key' => $name . '-nested' ), + private function evaluate( $directive_value ) { + /* + * The global WP_Interactivity_API instance is momentarily replaced to + * make global functions like `wp_interactivity_state` and + * `wp_interactivity_get_config` work as expected. + */ + global $wp_interactivity; + $wp_interactivity_prev = $wp_interactivity; + $wp_interactivity = $this->interactivity; + + $evaluate = new ReflectionMethod( $this->interactivity, 'evaluate' ); + $evaluate->setAccessible( true ); + + $result = $evaluate->invokeArgs( $this->interactivity, array( $directive_value ) ); + + // Restore the original WP_Interactivity_API instance. + $wp_interactivity = $wp_interactivity_prev; + + return $result; + } + + /** + * Tests that the `evaluate` method operates correctly for valid expressions. + * + * @ticket 60356 + * + * @covers ::evaluate + */ + public function test_evaluate_value() { + $obj = new stdClass(); + $obj->prop = 'object property'; + $this->interactivity->state( + 'myPlugin', + array( + 'key' => 'myPlugin-state', 'obj' => $obj, 'arrAccess' => new class() implements ArrayAccess { - #[\ReturnTypeWillChange] - public function offsetExists( $offset ) { + public function offsetExists( $offset ): bool { return true; } - public function offsetGet( $offset ): string { + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { return $offset; } @@ -838,28 +1109,17 @@ public function offsetSet( $offset, $value ): void {} public function offsetUnset( $offset ): void {} }, - ); - }; - $this->interactivity->state( 'myPlugin', $generate_state( 'myPlugin-state' ) ); - $this->interactivity->state( 'otherPlugin', $generate_state( 'otherPlugin-state' ) ); - $context = array( - 'myPlugin' => $generate_state( 'myPlugin-context' ), - 'otherPlugin' => $generate_state( 'otherPlugin-context' ), - 'obj' => new stdClass(), + ) ); - $evaluate = new ReflectionMethod( $this->interactivity, 'evaluate' ); - $evaluate->setAccessible( true ); - return $evaluate->invokeArgs( $this->interactivity, array( $directive_value, $default_namespace, $context ) ); - } + $this->interactivity->state( 'otherPlugin', array( 'key' => 'otherPlugin-state' ) ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'key' => 'myPlugin-context' ), + 'otherPlugin' => array( 'key' => 'otherPlugin-context' ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); - /** - * Tests that the `evaluate` method operates correctly for valid expressions. - * - * @ticket 60356 - * - * @covers ::evaluate - */ - public function test_evaluate_value() { $result = $this->evaluate( 'state.key' ); $this->assertEquals( 'myPlugin-state', $result ); @@ -873,7 +1133,7 @@ public function test_evaluate_value() { $this->assertEquals( 'otherPlugin-context', $result ); $result = $this->evaluate( 'state.obj.prop' ); - $this->assertSame( 'myPlugin-state', $result ); + $this->assertSame( 'object property', $result ); $result = $this->evaluate( 'state.arrAccess.1' ); $this->assertSame( '1', $result ); @@ -888,6 +1148,16 @@ public function test_evaluate_value() { * @covers ::evaluate */ public function test_evaluate_value_negation() { + $this->interactivity->state( 'myPlugin', array( 'key' => 'myPlugin-state' ) ); + $this->interactivity->state( 'otherPlugin', array( 'key' => 'otherPlugin-state' ) ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'key' => 'myPlugin-context' ), + 'otherPlugin' => array( 'key' => 'otherPlugin-context' ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); + $result = $this->evaluate( '!state.key' ); $this->assertFalse( $result ); @@ -909,6 +1179,16 @@ public function test_evaluate_value_negation() { * @covers ::evaluate */ public function test_evaluate_non_existent_path() { + $this->interactivity->state( 'myPlugin', array( 'key' => 'myPlugin-state' ) ); + $this->interactivity->state( 'otherPlugin', array( 'key' => 'otherPlugin-state' ) ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( 'key' => 'myPlugin-context' ), + 'otherPlugin' => array( 'key' => 'otherPlugin-context' ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); + $result = $this->evaluate( 'state.nonExistentKey' ); $this->assertNull( $result ); @@ -936,6 +1216,30 @@ public function test_evaluate_non_existent_path() { * @covers ::evaluate */ public function test_evaluate_nested_value() { + $this->interactivity->state( + 'myPlugin', + array( + 'nested' => array( 'key' => 'myPlugin-state-nested' ), + ) + ); + $this->interactivity->state( + 'otherPlugin', + array( + 'nested' => array( 'key' => 'otherPlugin-state-nested' ), + ) + ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( + 'nested' => array( 'key' => 'myPlugin-context-nested' ), + ), + 'otherPlugin' => array( + 'nested' => array( 'key' => 'otherPlugin-context-nested' ), + ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); + $result = $this->evaluate( 'state.nested.key' ); $this->assertEquals( 'myPlugin-state-nested', $result ); @@ -958,6 +1262,9 @@ public function test_evaluate_nested_value() { * @expectedIncorrectUsage WP_Interactivity_API::evaluate */ public function test_evaluate_unvalid_namespaces() { + $this->set_internal_context_stack( array() ); + $this->set_internal_namespace_stack(); + $result = $this->evaluate( 'path', 'null' ); $this->assertNull( $result ); @@ -968,6 +1275,155 @@ public function test_evaluate_unvalid_namespaces() { $this->assertNull( $result ); } + /** + * Tests the `evaluate` method for derived state functions. + * + * @ticket 61037 + * + * @covers ::evaluate + * @covers wp_interactivity_state + * @covers wp_interactivity_get_context + */ + public function test_evaluate_derived_state() { + $this->interactivity->state( + 'myPlugin', + array( + 'key' => 'myPlugin-state', + 'derived' => function () { + $state = wp_interactivity_state(); + $context = wp_interactivity_get_context(); + return 'Derived state: ' . + $state['key'] . + "\n" . + 'Derived context: ' . + $context['key']; + }, + ) + ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( + 'key' => 'myPlugin-context', + ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); + + $result = $this->evaluate( 'state.derived' ); + $this->assertSame( "Derived state: myPlugin-state\nDerived context: myPlugin-context", $result ); + } + + /** + * Tests the `evaluate` method for derived state functions accessing a + * different namespace. + * + * @ticket 61037 + * + * @covers ::evaluate + * @covers wp_interactivity_state + * @covers wp_interactivity_get_context + */ + public function test_evaluate_derived_state_accessing_different_namespace() { + $this->interactivity->state( + 'myPlugin', + array( + 'key' => 'myPlugin-state', + 'derived' => function () { + $state = wp_interactivity_state( 'otherPlugin' ); + $context = wp_interactivity_get_context( 'otherPlugin' ); + return 'Derived state: ' . + $state['key'] . + "\n" . + 'Derived context: ' . + $context['key']; + }, + ) + ); + $this->interactivity->state( 'otherPlugin', array( 'key' => 'otherPlugin-state' ) ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( + 'key' => 'myPlugin-context', + ), + 'otherPlugin' => array( + 'key' => 'otherPlugin-context', + ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); + + $result = $this->evaluate( 'state.derived' ); + $this->assertSame( "Derived state: otherPlugin-state\nDerived context: otherPlugin-context", $result ); + } + + /** + * Tests the `evaluate` method for derived state functions defined in a + * different namespace. + * + * @ticket 61037 + * + * @covers ::evaluate + * @covers wp_interactivity_state + * @covers wp_interactivity_get_context + */ + public function test_evaluate_derived_state_defined_in_different_namespace() { + $this->interactivity->state( 'myPlugin', array( 'key' => 'myPlugin-state' ) ); + $this->interactivity->state( + 'otherPlugin', + array( + 'key' => 'otherPlugin-state', + 'derived' => function () { + $state = wp_interactivity_state(); + $context = wp_interactivity_get_context(); + return 'Derived state: ' . + $state['key'] . + "\n" . + 'Derived context: ' . + $context['key']; + }, + ) + ); + $this->set_internal_context_stack( + array( + 'myPlugin' => array( + 'key' => 'myPlugin-context', + ), + 'otherPlugin' => array( + 'key' => 'otherPlugin-context', + ), + ) + ); + $this->set_internal_namespace_stack( 'myPlugin' ); + + $result = $this->evaluate( 'otherPlugin::state.derived' ); + $this->assertSame( "Derived state: otherPlugin-state\nDerived context: otherPlugin-context", $result ); + } + + + /** + * Tests the `evaluate` method for derived state functions that throw. + * + * @ticket 61037 + * + * @covers ::evaluate + * @expectedIncorrectUsage WP_Interactivity_API::evaluate + */ + public function test_evaluate_derived_state_that_throws() { + $this->interactivity->state( + 'myPlugin', + array( + 'derivedThatThrows' => function () { + throw new Error( 'Something bad happened.' ); + }, + ) + ); + $this->set_internal_context_stack(); + $this->set_internal_namespace_stack( 'myPlugin' ); + + $result = $this->evaluate( 'state.derivedThatThrows' ); + $this->assertNull( $result ); + } + /** * Tests the `kebab_to_camel_case` method. * From 782fb1f1a7128ff50ac1beccccd8de6e71918382 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 4 Jun 2024 11:53:37 +0000 Subject: [PATCH 15/21] Editor: Add theme.json v3 migrations. See https://github.com/WordPress/wordpress-develop/pull/6616. See also the original Gutenberg PRs: * https://github.com/WordPress/gutenberg/pull/58409 * https://github.com/WordPress/gutenberg/pull/61328 * https://github.com/WordPress/gutenberg/pull/61842 * https://github.com/WordPress/gutenberg/pull/62199 * https://github.com/WordPress/gutenberg/pull/62252 Fixes #61282. Props ajlende, talldanwp, ramonopoly, ellatrix. git-svn-id: https://develop.svn.wordpress.org/trunk@58328 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-editor.php | 6 + src/wp-includes/class-wp-theme-json-data.php | 2 +- .../class-wp-theme-json-resolver.php | 45 ++- .../class-wp-theme-json-schema.php | 93 +++++- src/wp-includes/class-wp-theme-json.php | 268 ++++++++++++++---- .../class-wp-rest-font-faces-controller.php | 2 +- ...class-wp-rest-font-families-controller.php | 2 +- src/wp-includes/theme.json | 4 +- src/wp-includes/theme.php | 27 ++ .../theme.json | 2 +- .../theme.json | 2 +- .../theme.json | 2 +- .../block-theme-child/styles/variation-a.json | 2 +- .../block-theme-child/styles/variation-b.json | 2 +- .../themedir1/block-theme-child/theme.json | 2 +- .../styles/variation.json | 44 +-- .../block-theme-with-hooked-blocks/theme.json | 2 +- .../block-theme/styles/variation-a.json | 2 +- .../block-theme/styles/variation-b.json | 2 +- .../block-theme/styles/variation.json | 2 +- .../data/themedir1/block-theme/theme.json | 2 +- .../themedir1/empty-fontface-theme/theme.json | 146 +++++----- .../styles/variation-duplicate-fonts.json | 2 +- .../styles/variation-new-font-family.json | 2 +- .../styles/variation-new-font-variations.json | 2 +- .../styles/variation-no-fonts.json | 2 +- .../themedir1/fonts-block-theme/theme.json | 2 +- .../wpRestFontFacesController.php | 2 +- .../wpRestFontFamiliesController.php | 2 +- .../rest-global-styles-controller.php | 6 +- .../tests/rest-api/rest-themes-controller.php | 3 +- tests/phpunit/tests/theme/wpThemeJson.php | 163 +++++------ .../phpunit/tests/theme/wpThemeJsonSchema.php | 111 +++++++- tests/qunit/fixtures/wp-api-generated.js | 12 +- 34 files changed, 677 insertions(+), 293 deletions(-) diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index e04b012e7dd08..fdc4846f69f80 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -814,6 +814,7 @@ function get_block_editor_theme_styles() { * Returns the classic theme supports settings for block editor. * * @since 6.2.0 + * @since 6.6.0 Add support for 'editor-spacing-sizes' theme support. * * @return array The classic theme supports settings. */ @@ -844,5 +845,10 @@ function get_classic_theme_supports_block_editor_settings() { $theme_settings['gradients'] = $gradient_presets; } + $spacing_sizes = current( (array) get_theme_support( 'editor-spacing-sizes' ) ); + if ( false !== $spacing_sizes ) { + $theme_settings['spacingSizes'] = $spacing_sizes; + } + return $theme_settings; } diff --git a/src/wp-includes/class-wp-theme-json-data.php b/src/wp-includes/class-wp-theme-json-data.php index 30dab47185ca9..ca72ae81b5ab5 100644 --- a/src/wp-includes/class-wp-theme-json-data.php +++ b/src/wp-includes/class-wp-theme-json-data.php @@ -39,7 +39,7 @@ class WP_Theme_JSON_Data { * @param array $data Array following the theme.json specification. * @param string $origin The origin of the data: default, theme, user. */ - public function __construct( $data = array(), $origin = 'theme' ) { + public function __construct( $data = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ), $origin = 'theme' ) { $this->origin = $origin; $this->theme_json = new WP_Theme_JSON( $data, $this->origin ); } diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index 0bdf4a23db119..c3a5db45ad79a 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -220,6 +220,7 @@ protected static function has_same_registered_blocks( $origin ) { * @since 5.8.0 * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed. * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports. + * @since 6.6.0 Add support for 'default-font-sizes' and 'default-spacing-sizes' theme supports. * * @param array $deprecated Deprecated. Not used. * @param array $options { @@ -243,7 +244,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() $theme_json_data = static::read_json_file( $theme_json_file ); $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); } else { - $theme_json_data = array(); + $theme_json_data = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); } /** @@ -310,6 +311,32 @@ public static function get_theme_data( $deprecated = array(), $options = array() } $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; + if ( ! isset( $theme_support_data['settings']['typography'] ) ) { + $theme_support_data['settings']['typography'] = array(); + } + $default_font_sizes = false; + if ( current_theme_supports( 'default-font-sizes' ) ) { + $default_font_sizes = true; + } + if ( ! isset( $theme_support_data['settings']['typography']['fontSizes'] ) ) { + // If the theme does not have any font sizes, we still want to show the core one. + $default_font_sizes = true; + } + $theme_support_data['settings']['typography']['defaultFontSizes'] = $default_font_sizes; + + if ( ! isset( $theme_support_data['settings']['spacing'] ) ) { + $theme_support_data['settings']['spacing'] = array(); + } + $default_spacing_sizes = false; + if ( current_theme_supports( 'default-spacing-sizes' ) ) { + $default_spacing_sizes = true; + } + if ( ! isset( $theme_support_data['settings']['spacing']['spacingSizes'] ) ) { + // If the theme does not have any spacing sizes, we still want to show the core one. + $default_spacing_sizes = true; + } + $theme_support_data['settings']['spacing']['defaultSpacingSizes'] = $default_spacing_sizes; + if ( ! isset( $theme_support_data['settings']['shadow'] ) ) { $theme_support_data['settings']['shadow'] = array(); } @@ -359,7 +386,7 @@ public static function get_block_data() { return static::$blocks; } - $config = array( 'version' => 2 ); + $config = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); foreach ( $blocks as $block_name => $block_type ) { if ( isset( $block_type->supports['__experimentalStyle'] ) ) { $config['styles']['blocks'][ $block_name ] = static::remove_json_comments( $block_type->supports['__experimentalStyle'] ); @@ -494,6 +521,7 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post * Returns the user's origin config. * * @since 5.9.0 + * @since 6.6.0 The 'isGlobalStylesUserThemeJSON' flag is left on the user data. * * @return WP_Theme_JSON Entity that holds styles for user data. */ @@ -531,14 +559,18 @@ public static function get_user_data() { isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && $decoded_data['isGlobalStylesUserThemeJSON'] ) { - unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); $config = $decoded_data; } } /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */ - $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data( $config, 'custom' ) ); - static::$user = $theme_json->get_theme_json(); + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data( $config, 'custom' ) ); + $config = $theme_json->get_data(); + + // Needs to be set for schema migrations of user data. + $config['isGlobalStylesUserThemeJSON'] = true; + + static::$user = new WP_Theme_JSON( $config, 'custom' ); return static::$user; } @@ -586,7 +618,6 @@ public static function get_merged_data( $origin = 'custom' ) { $result = new WP_Theme_JSON(); $result->merge( static::get_core_data() ); if ( 'default' === $origin ) { - $result->set_spacing_sizes(); return $result; } @@ -597,12 +628,10 @@ public static function get_merged_data( $origin = 'custom' ) { $result->merge( static::get_theme_data() ); if ( 'theme' === $origin ) { - $result->set_spacing_sizes(); return $result; } $result->merge( static::get_user_data() ); - $result->set_spacing_sizes(); return $result; } diff --git a/src/wp-includes/class-wp-theme-json-schema.php b/src/wp-includes/class-wp-theme-json-schema.php index 46712a032bc2b..aa654f25979eb 100644 --- a/src/wp-includes/class-wp-theme-json-schema.php +++ b/src/wp-includes/class-wp-theme-json-schema.php @@ -35,6 +35,7 @@ class WP_Theme_JSON_Schema { * Function that migrates a given theme.json structure to the last version. * * @since 5.9.0 + * @since 6.6.0 Migrate up to v3. * * @param array $theme_json The structure to migrate. * @@ -47,8 +48,14 @@ public static function migrate( $theme_json ) { ); } - if ( 1 === $theme_json['version'] ) { - $theme_json = self::migrate_v1_to_v2( $theme_json ); + // Migrate each version in order starting with the current version. + switch ( $theme_json['version'] ) { + case 1: + $theme_json = self::migrate_v1_to_v2( $theme_json ); + // no break + case 2: + $theme_json = self::migrate_v2_to_v3( $theme_json ); + // no break } return $theme_json; @@ -84,6 +91,88 @@ private static function migrate_v1_to_v2( $old ) { return $new; } + /** + * Migrates from v2 to v3. + * + * - Sets settings.typography.defaultFontSizes to false. + * + * @since 6.6.0 + * + * @param array $old Data to migrate. + * + * @return array Data with defaultFontSizes set to false. + */ + private static function migrate_v2_to_v3( $old ) { + // Copy everything. + $new = $old; + + // Set the new version. + $new['version'] = 3; + + /* + * Remaining changes do not need to be applied to the custom origin, + * as they should take on the value of the theme origin. + */ + if ( + isset( $new['isGlobalStylesUserThemeJSON'] ) && + true === $new['isGlobalStylesUserThemeJSON'] + ) { + return $new; + } + + /* + * Even though defaultFontSizes and defaultSpacingSizes are new + * settings, we need to migrate them as they each control + * PRESETS_METADATA prevent_override values which were previously + * hardcoded to false. This only needs to happen when the theme provides + * fontSizes or spacingSizes as they could match the default ones and + * affect the generated CSS. + */ + if ( isset( $old['settings']['typography']['fontSizes'] ) ) { + if ( ! isset( $new['settings'] ) ) { + $new['settings'] = array(); + } + if ( ! isset( $new['settings']['typography'] ) ) { + $new['settings']['typography'] = array(); + } + $new['settings']['typography']['defaultFontSizes'] = false; + } + + /* + * Similarly to defaultFontSizes, we need to migrate defaultSpacingSizes + * as it controls the PRESETS_METADATA prevent_override which was + * previously hardcoded to false. This only needs to happen when the + * theme provided spacing sizes via spacingSizes or spacingScale. + */ + if ( + isset( $old['settings']['spacing']['spacingSizes'] ) || + isset( $old['settings']['spacing']['spacingScale'] ) + ) { + if ( ! isset( $new['settings'] ) ) { + $new['settings'] = array(); + } + if ( ! isset( $new['settings']['spacing'] ) ) { + $new['settings']['spacing'] = array(); + } + $new['settings']['spacing']['defaultSpacingSizes'] = false; + } + + /* + * In v3 spacingSizes is merged with the generated spacingScale sizes + * instead of completely replacing them. The v3 behavior is what was + * documented for the v2 schema, but the code never actually did work + * that way. Instead of surprising users with a behavior change two + * years after the fact at the same time as a v3 update is introduced, + * we'll continue using the "bugged" behavior for v2 themes. And treat + * the "bug fix" as a breaking change for v3. + */ + if ( isset( $old['settings']['spacing']['spacingSizes'] ) ) { + unset( $new['settings']['spacing']['spacingScale'] ); + } + + return $new; + } + /** * Processes the settings subtree. * diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 9aa2b97b8b274..18e0872eb4673 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -123,7 +123,9 @@ class WP_Theme_JSON { * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`. * @since 6.2.0 Added 'shadow' presets. * @since 6.3.0 Replaced value_func for duotone with `null`. Custom properties are handled by class-wp-duotone.php. - * @since 6.6.0 Added the `dimensions.aspectRatios` & `dimensions.defaultAspectRatios` preset. + * @since 6.6.0 Added the `dimensions.aspectRatios` and `dimensions.defaultAspectRatios` presets. + * Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' + * and spacing size presets to use `spacing.defaultSpacingSizes`. * @var array */ const PRESETS_METADATA = array( @@ -169,7 +171,7 @@ class WP_Theme_JSON { ), array( 'path' => array( 'typography', 'fontSizes' ), - 'prevent_override' => false, + 'prevent_override' => array( 'typography', 'defaultFontSizes' ), 'use_default_names' => true, 'value_func' => 'wp_get_typography_font_size_value', 'css_vars' => '--wp--preset--font-size--$slug', @@ -187,7 +189,7 @@ class WP_Theme_JSON { ), array( 'path' => array( 'spacing', 'spacingSizes' ), - 'prevent_override' => false, + 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ), 'use_default_names' => true, 'value_key' => 'size', 'css_vars' => '--wp--preset--spacing--$slug', @@ -378,7 +380,8 @@ class WP_Theme_JSON { * `typography.writingMode`, `lightbox.enabled` and `lightbox.allowEditing`. * @since 6.5.0 Added support for `layout.allowCustomContentAndWideSize`, * `background.backgroundSize` and `dimensions.aspectRatio`. - * @since 6.6.0 Added support for `dimensions.aspectRatios` and `dimensions.defaultAspectRatios`. + * @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios', + * 'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'. * @var array */ const VALID_SETTINGS = array( @@ -433,33 +436,35 @@ class WP_Theme_JSON { 'sticky' => null, ), 'spacing' => array( - 'customSpacingSize' => null, - 'spacingSizes' => null, - 'spacingScale' => null, - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, + 'customSpacingSize' => null, + 'defaultSpacingSizes' => null, + 'spacingSizes' => null, + 'spacingScale' => null, + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, ), 'shadow' => array( 'presets' => null, 'defaultPresets' => null, ), 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textAlign' => null, - 'textColumns' => null, - 'textDecoration' => null, - 'textTransform' => null, - 'writingMode' => null, + 'fluid' => null, + 'customFontSize' => null, + 'defaultFontSizes' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textAlign' => null, + 'textColumns' => null, + 'textDecoration' => null, + 'textTransform' => null, + 'writingMode' => null, ), ); @@ -728,20 +733,23 @@ public static function get_element_class_name( $element ) { * * @since 5.8.0 * @since 5.9.0 Changed value from 1 to 2. + * @since 6.6.0 Changed value from 2 to 3. * @var int */ - const LATEST_SCHEMA = 2; + const LATEST_SCHEMA = 3; /** * Constructor. * * @since 5.8.0 + * @since 6.6.0 Key spacingScale by origin, and Pre-generate the + * spacingSizes from spacingScale. * * @param array $theme_json A structure that follows the theme.json schema. * @param string $origin Optional. What source of data this object represents. * One of 'default', 'theme', or 'custom'. Default 'theme'. */ - public function __construct( $theme_json = array(), $origin = 'theme' ) { + public function __construct( $theme_json = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ), $origin = 'theme' ) { if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { $origin = 'theme'; } @@ -750,8 +758,8 @@ public function __construct( $theme_json = array(), $origin = 'theme' ) { $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); $valid_variations = static::get_valid_block_style_variations(); - $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations ); - $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); + $this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations ); + $this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json ); // Internally, presets are keyed by origin. $nodes = static::get_setting_nodes( $this->theme_json ); @@ -770,6 +778,27 @@ public function __construct( $theme_json = array(), $origin = 'theme' ) { } } } + + // In addition to presets, spacingScale (which generates presets) is also keyed by origin. + $scale_path = array( 'settings', 'spacing', 'spacingScale' ); + $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); + if ( null !== $spacing_scale ) { + // If the spacingScale is not already keyed by origin. + if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) { + _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) ); + } + } + + // Pre-generate the spacingSizes from spacingScale. + $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); + $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); + if ( isset( $spacing_scale ) ) { + $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin ); + $spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() ); + $spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale ); + $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes ); + _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes ); + } } /** @@ -2915,6 +2944,40 @@ public function merge( $incoming ) { $incoming_data = $incoming->get_raw_data(); $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); + /* + * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor + * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so + * we can allow partial spacingScale data to inherit missing data from earlier layers when + * computing the spacing sizes. + * + * This happens before the presets are merged to ensure that default spacing sizes can be + * removed from the theme origin if $prevent_override is true. + */ + $flattened_spacing_scale = array(); + foreach ( static::VALID_ORIGINS as $origin ) { + $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); + + // Apply the base spacing scale to the current layer. + $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() ); + $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale ); + + $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null ); + if ( ! isset( $spacing_scale ) ) { + continue; + } + + // Allow partial scale settings by merging with lower layers. + $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale ); + + // Generate and merge the scales for this layer. + $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin ); + $spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() ); + $spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale ); + $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes ); + + _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes ); + } + /* * The array_replace_recursive algorithm merges at the leaf level, * but we don't want leaf arrays to be merged, so we overwrite it. @@ -2951,12 +3014,15 @@ public function merge( $incoming ) { } // Replace the presets. - foreach ( static::PRESETS_METADATA as $preset ) { - $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $prevent_override = $preset_metadata['prevent_override']; + if ( is_array( $prevent_override ) ) { + $prevent_override = _wp_array_get( $this->theme_json['settings'], $preset_metadata['prevent_override'] ); + } foreach ( static::VALID_ORIGINS as $origin ) { $base_path = $node['path']; - foreach ( $preset['path'] as $leaf ) { + foreach ( $preset_metadata['path'] as $leaf ) { $base_path[] = $leaf; } @@ -2968,7 +3034,8 @@ public function merge( $incoming ) { continue; } - if ( 'theme' === $origin && $preset['use_default_names'] ) { + // Set names for theme presets based on the slug if they are not set and can use default names. + if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) { foreach ( $content as $key => $item ) { if ( ! isset( $item['name'] ) ) { $name = static::get_name_from_defaults( $item['slug'], $base_path ); @@ -2979,19 +3046,17 @@ public function merge( $incoming ) { } } - if ( - ( 'theme' !== $origin ) || - ( 'theme' === $origin && $override_preset ) - ) { - _wp_array_set( $this->theme_json, $path, $content ); - } else { - $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); - $slugs = array_merge_recursive( $slugs_global, $slugs_node ); + // Filter out default slugs from theme presets when defaults should not be overridden. + if ( 'theme' === $origin && $prevent_override ) { + $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); + $preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() ); + $preset_node = _wp_array_get( $slugs_node, $preset_metadata['path'], array() ); + $preset_slugs = array_merge_recursive( $preset_global, $preset_node ); - $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); - $content = static::filter_slugs( $content, $slugs_for_preset ); - _wp_array_set( $this->theme_json, $path, $content ); + $content = static::filter_slugs( $content, $preset_slugs ); } + + _wp_array_set( $this->theme_json, $path, $content ); } } } @@ -3530,6 +3595,13 @@ public static function get_from_editor_settings( $settings ) { $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; } + if ( isset( $settings['spacingSizes'] ) ) { + if ( ! isset( $theme_settings['settings']['spacing'] ) ) { + $theme_settings['settings']['spacing'] = array(); + } + $theme_settings['settings']['spacing']['spacingSizes'] = $settings['spacingSizes']; + } + return $theme_settings; } @@ -3703,10 +3775,16 @@ public function get_data() { * Sets the spacingSizes array based on the spacingScale values from theme.json. * * @since 6.1.0 + * @deprecated 6.6.0 + * + * @param string $origin Optional. What source of data to set the spacing sizes for. + * One of 'default', 'theme', or 'custom'. Default 'default'. * * @return null|void */ public function set_spacing_sizes() { + _deprecated_function( __METHOD__, '6.6.0' ); + $spacing_scale = isset( $this->theme_json['settings']['spacing']['spacingScale'] ) ? $this->theme_json['settings']['spacing']['spacingScale'] : array(); @@ -3740,6 +3818,99 @@ public function set_spacing_sizes() { return null; } + $spacing_sizes = static::compute_spacing_sizes( $spacing_scale ); + + // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes. + if ( $spacing_scale['steps'] <= 7 ) { + for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { + $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); + } + } + + _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); + } + + /** + * Merges two sets of spacing size presets. + * + * @since 6.6.0 + * + * @param array $base The base set of spacing sizes. + * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values. + * @return array The merged set of spacing sizes. + */ + private static function merge_spacing_sizes( $base, $incoming ) { + // Preserve the order if there are no base (spacingScale) values. + if ( empty( $base ) ) { + return $incoming; + } + $merged = array(); + foreach ( $base as $item ) { + $merged[ $item['slug'] ] = $item; + } + foreach ( $incoming as $item ) { + $merged[ $item['slug'] ] = $item; + } + ksort( $merged, SORT_NUMERIC ); + return array_values( $merged ); + } + + /** + * Generates a set of spacing sizes by starting with a medium size and + * applying an operator with an increment value to generate the rest of the + * sizes outward from the medium size. The medium slug is '50' with the rest + * of the slugs being 10 apart. The generated names use t-shirt sizing. + * + * Example: + * + * $spacing_scale = array( + * 'steps' => 4, + * 'mediumStep' => 16, + * 'unit' => 'px', + * 'operator' => '+', + * 'increment' => 2, + * ); + * $spacing_sizes = static::compute_spacing_sizes( $spacing_scale ); + * // -> array( + * // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ), + * // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ), + * // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ), + * // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ), + * // ) + * + * @since 6.6.0 + * + * @param array $spacing_scale { + * The spacing scale values. All are required. + * + * @type int $steps The number of steps in the scale. (up to 10 steps are supported.) + * @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.) + * @type string $unit The CSS unit to use for the sizes. + * @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'. + * @type float $increment The value used with the operator to generate the other sizes. + * } + * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid. + */ + private static function compute_spacing_sizes( $spacing_scale ) { + /* + * This condition is intentionally missing some checks on ranges for the values in order to + * keep backwards compatibility with the previous implementation. + */ + if ( + ! isset( $spacing_scale['steps'] ) || + ! is_numeric( $spacing_scale['steps'] ) || + 0 === $spacing_scale['steps'] || + ! isset( $spacing_scale['mediumStep'] ) || + ! is_numeric( $spacing_scale['mediumStep'] ) || + ! isset( $spacing_scale['unit'] ) || + ! isset( $spacing_scale['operator'] ) || + ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) || + ! isset( $spacing_scale['increment'] ) || + ! is_numeric( $spacing_scale['increment'] ) + ) { + return array(); + } + $unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] ); $current_step = $spacing_scale['mediumStep']; $steps_mid_point = round( $spacing_scale['steps'] / 2, 0 ); @@ -3822,14 +3993,7 @@ public function set_spacing_sizes() { $spacing_sizes[] = $above_sizes_item; } - // If there are 7 or fewer steps in the scale revert to numbers for labels instead of t-shirt sizes. - if ( $spacing_scale['steps'] <= 7 ) { - for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { - $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); - } - } - - _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); + return $spacing_sizes; } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php index c7f72d4ec1d9d..91b5f63e964ee 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php @@ -18,7 +18,7 @@ class WP_REST_Font_Faces_Controller extends WP_REST_Posts_Controller { * @since 6.5.0 * @var int */ - const LATEST_THEME_JSON_VERSION_SUPPORTED = 2; + const LATEST_THEME_JSON_VERSION_SUPPORTED = 3; /** * Whether the controller supports batching. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php index 184b42d141dd3..c2d70d0dd6916 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php @@ -20,7 +20,7 @@ class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller { * @since 6.5.0 * @var int */ - const LATEST_THEME_JSON_VERSION_SUPPORTED = 2; + const LATEST_THEME_JSON_VERSION_SUPPORTED = 3; /** * Whether the controller supports batching. diff --git a/src/wp-includes/theme.json b/src/wp-includes/theme.json index 485d01247d636..c2ea0e71938bb 100644 --- a/src/wp-includes/theme.json +++ b/src/wp-includes/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "settings": { "appearanceTools": false, "useRootPaddingAwareAlignments": false, @@ -265,6 +265,7 @@ "margin": false, "padding": false, "customSpacingSize": true, + "defaultSpacingSizes": true, "units": [ "px", "em", "rem", "vh", "vw", "%" ], "spacingScale": { "operator": "*", @@ -276,6 +277,7 @@ }, "typography": { "customFontSize": true, + "defaultFontSizes": true, "dropCap": true, "fontSizes": [ { diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 330c36d3f55ff..fd37543791912 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -2642,6 +2642,7 @@ function get_theme_starter_content() { * @since 6.3.0 The `border` feature allows themes without theme.json to add border styles to blocks. * @since 6.5.0 The `appearance-tools` feature enables a few design tools for blocks, * see `WP_Theme_JSON::APPEARANCE_TOOLS_OPT_INS` for a complete list. + * @since 6.6.0 The `editor-spacing-sizes` feature was added. * * @global array $_wp_theme_features * @@ -2669,6 +2670,7 @@ function get_theme_starter_content() { * - 'editor-color-palette' * - 'editor-gradient-presets' * - 'editor-font-sizes' + * - 'editor-spacing-sizes' * - 'editor-styles' * - 'featured-content' * - 'html5' @@ -4226,6 +4228,31 @@ function create_initial_theme_features() { ), ) ); + register_theme_feature( + 'editor-spacing-sizes', + array( + 'type' => 'array', + 'description' => __( 'Custom spacing sizes if defined by the theme.' ), + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + 'size' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); register_theme_feature( 'editor-styles', array( diff --git a/tests/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json b/tests/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json index 710ec336df70b..813024ba8abeb 100644 --- a/tests/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json +++ b/tests/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "settings": { "appearanceTools": true, "layout": { diff --git a/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json b/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json index dcd3745f1630c..3aa0560aaf062 100644 --- a/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json +++ b/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "settings": { "appearanceTools": true, "layout": { diff --git a/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json b/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json index 7b34524270295..b2624775a5003 100644 --- a/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json +++ b/tests/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "settings": { "appearanceTools": true, "typography": { diff --git a/tests/phpunit/data/themedir1/block-theme-child/styles/variation-a.json b/tests/phpunit/data/themedir1/block-theme-child/styles/variation-a.json index a9d5ade894692..53c3ef60619b5 100644 --- a/tests/phpunit/data/themedir1/block-theme-child/styles/variation-a.json +++ b/tests/phpunit/data/themedir1/block-theme-child/styles/variation-a.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "settings": { "blocks": { "core/paragraph": { diff --git a/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json b/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json index 0a8a4fcab99f6..4e949f24c7f40 100644 --- a/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json +++ b/tests/phpunit/data/themedir1/block-theme-child/styles/variation-b.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "settings": { "blocks": { "core/post-title": { diff --git a/tests/phpunit/data/themedir1/block-theme-child/theme.json b/tests/phpunit/data/themedir1/block-theme-child/theme.json index 1157fa9128030..185437e9b81c4 100644 --- a/tests/phpunit/data/themedir1/block-theme-child/theme.json +++ b/tests/phpunit/data/themedir1/block-theme-child/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "settings": { "color": { "palette": [ diff --git a/tests/phpunit/data/themedir1/block-theme-deprecated-path/styles/variation.json b/tests/phpunit/data/themedir1/block-theme-deprecated-path/styles/variation.json index ad3affb1152d6..06f672f6fd25d 100644 --- a/tests/phpunit/data/themedir1/block-theme-deprecated-path/styles/variation.json +++ b/tests/phpunit/data/themedir1/block-theme-deprecated-path/styles/variation.json @@ -1,23 +1,23 @@ { - "version": 2, - "settings": { - "color": { - "palette": [ - { - "slug": "foreground", - "color": "#3F67C6", - "name": "Foreground" - } - ] - } - }, - "styles": { - "blocks": { - "core/post-title": { - "typography": { - "fontWeight": "700" - } - } - } - } -} \ No newline at end of file + "version": 3, + "settings": { + "color": { + "palette": [ + { + "slug": "foreground", + "color": "#3F67C6", + "name": "Foreground" + } + ] + } + }, + "styles": { + "blocks": { + "core/post-title": { + "typography": { + "fontWeight": "700" + } + } + } + } +} diff --git a/tests/phpunit/data/themedir1/block-theme-with-hooked-blocks/theme.json b/tests/phpunit/data/themedir1/block-theme-with-hooked-blocks/theme.json index 28f3d7aa8443d..b4cfc5e80223a 100644 --- a/tests/phpunit/data/themedir1/block-theme-with-hooked-blocks/theme.json +++ b/tests/phpunit/data/themedir1/block-theme-with-hooked-blocks/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "templateParts": [ { "area": "header", diff --git a/tests/phpunit/data/themedir1/block-theme/styles/variation-a.json b/tests/phpunit/data/themedir1/block-theme/styles/variation-a.json index 42c20fc63b592..eb08d0090c177 100644 --- a/tests/phpunit/data/themedir1/block-theme/styles/variation-a.json +++ b/tests/phpunit/data/themedir1/block-theme/styles/variation-a.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "settings": { "blocks": { "core/paragraph": { diff --git a/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json b/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json index 23e9cb2d63391..a670b9dcb2fa1 100644 --- a/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json +++ b/tests/phpunit/data/themedir1/block-theme/styles/variation-b.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "settings": { "blocks": { "core/post-title": { diff --git a/tests/phpunit/data/themedir1/block-theme/styles/variation.json b/tests/phpunit/data/themedir1/block-theme/styles/variation.json index d0f316cb454dd..debb3666a767b 100644 --- a/tests/phpunit/data/themedir1/block-theme/styles/variation.json +++ b/tests/phpunit/data/themedir1/block-theme/styles/variation.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "title": "Block theme variation", "settings": { "color": { diff --git a/tests/phpunit/data/themedir1/block-theme/theme.json b/tests/phpunit/data/themedir1/block-theme/theme.json index fb24069fb6429..2c29884bf4dcb 100644 --- a/tests/phpunit/data/themedir1/block-theme/theme.json +++ b/tests/phpunit/data/themedir1/block-theme/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "title": "Block theme", "settings": { "color": { diff --git a/tests/phpunit/data/themedir1/empty-fontface-theme/theme.json b/tests/phpunit/data/themedir1/empty-fontface-theme/theme.json index 92b16631120f8..2b39f38dd66e4 100644 --- a/tests/phpunit/data/themedir1/empty-fontface-theme/theme.json +++ b/tests/phpunit/data/themedir1/empty-fontface-theme/theme.json @@ -1,89 +1,79 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "customTemplates": [ - { - "name": "blank", - "title": "Blank", - "postTypes": [ - "page", - "post" - ] - } - ], - "settings": { - "appearanceTools": true, - "color": { - "duotone": [], - "gradients": [], - "palette": [] - }, - "custom": {}, - "spacing": { - "units": [ - "%", - "px", - "em", - "rem", - "vh", - "vw" - ] - }, - "typography": { - "dropCap": false, - "fontFamilies": [ + "version": 3, + "customTemplates": [ { - "fontFamily": "Roboto", - "name": "Roboto", - "slug": "roboto", - "fontFace": [] + "name": "blank", + "title": "Blank", + "postTypes": [ "page", "post" ] } - ], - "fontSizes": [ - { - "size": "1rem", - "slug": "small" + ], + "settings": { + "appearanceTools": true, + "color": { + "duotone": [], + "gradients": [], + "palette": [] }, - { - "size": "1.125rem", - "slug": "medium" + "custom": {}, + "spacing": { + "units": [ "%", "px", "em", "rem", "vh", "vw" ] }, - { - "size": "1.75rem", - "slug": "large" + "typography": { + "dropCap": false, + "fontFamilies": [ + { + "fontFamily": "Roboto", + "name": "Roboto", + "slug": "roboto", + "fontFace": [] + } + ], + "fontSizes": [ + { + "size": "1rem", + "slug": "small" + }, + { + "size": "1.125rem", + "slug": "medium" + }, + { + "size": "1.75rem", + "slug": "large" + }, + { + "size": "clamp(1.75rem, 3vw, 2.25rem)", + "slug": "x-large" + } + ] }, - { - "size": "clamp(1.75rem, 3vw, 2.25rem)", - "slug": "x-large" + "layout": { + "contentSize": "650px", + "wideSize": "1000px" } - ] }, - "layout": { - "contentSize": "650px", - "wideSize": "1000px" - } - }, - "styles": { - "blocks": {}, - "color": { - "background": "var(--wp--preset--color--background)", - "text": "var(--wp--preset--color--foreground)" - }, - "elements": {}, - "spacing": { - "blockGap": "1.5rem" + "styles": { + "blocks": {}, + "color": { + "background": "var(--wp--preset--color--background)", + "text": "var(--wp--preset--color--foreground)" + }, + "elements": {}, + "spacing": { + "blockGap": "1.5rem" + }, + "typography": { + "fontFamily": "var(--wp--preset--font-family--system-font)", + "lineHeight": "var(--wp--custom--typography--line-height--normal)", + "fontSize": "var(--wp--preset--font-size--medium)" + } }, - "typography": { - "fontFamily": "var(--wp--preset--font-family--system-font)", - "lineHeight": "var(--wp--custom--typography--line-height--normal)", - "fontSize": "var(--wp--preset--font-size--medium)" - } - }, - "templateParts": [ - { - "name": "header", - "title": "Header", - "area": "header" - } - ] + "templateParts": [ + { + "name": "header", + "title": "Header", + "area": "header" + } + ] } diff --git a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-duplicate-fonts.json b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-duplicate-fonts.json index 040689043379d..c24a4a85a4f67 100644 --- a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-duplicate-fonts.json +++ b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-duplicate-fonts.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "title": "Variation: duplicate fonts", "settings": { "typography": { diff --git a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-family.json b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-family.json index 0af954cbecaa8..dfc83f3726d79 100644 --- a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-family.json +++ b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-family.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "title": "Variation: new font family", "settings": { "typography": { diff --git a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-variations.json b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-variations.json index 81268817ade70..95cb7b88d0fb7 100644 --- a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-variations.json +++ b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-new-font-variations.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "title": "Variation: new font variations", "settings": { "typography": { diff --git a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-no-fonts.json b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-no-fonts.json index 9c98c6893fa06..fcdd368ec691b 100644 --- a/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-no-fonts.json +++ b/tests/phpunit/data/themedir1/fonts-block-theme/styles/variation-no-fonts.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "title": "Variation - no fonts", "styles": { "typography": { diff --git a/tests/phpunit/data/themedir1/fonts-block-theme/theme.json b/tests/phpunit/data/themedir1/fonts-block-theme/theme.json index a5d40da2b5bb2..a7946a3f11023 100644 --- a/tests/phpunit/data/themedir1/fonts-block-theme/theme.json +++ b/tests/phpunit/data/themedir1/fonts-block-theme/theme.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, + "version": 3, "settings": { "appearanceTools": true, "color": { diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 3e74b23b7cb20..a4f6d8194902f 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -623,7 +623,7 @@ public function test_create_item_invalid_theme_json_version( $theme_json_version public function data_create_item_invalid_theme_json_version() { return array( array( 1 ), - array( 3 ), + array( 4 ), ); } diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index 78890139f304e..673d03606c490 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -431,7 +431,7 @@ public function test_create_item_invalid_theme_json_version( $theme_json_version public function data_create_item_invalid_theme_json_version() { return array( array( 1 ), - array( 3 ), + array( 4 ), ); } diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index 6f920c9152bb4..02d3ec8d5a72c 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -150,7 +150,7 @@ public function test_get_theme_items() { $data = $response->get_data(); $expected = array( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'blocks' => array( 'core/paragraph' => array( @@ -171,7 +171,7 @@ public function test_get_theme_items() { 'title' => 'variation-a', ), array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'blocks' => array( 'core/post-title' => array( @@ -216,7 +216,7 @@ public function test_get_theme_items() { ), ), array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'title' => 'Block theme variation', 'settings' => array( 'color' => array( diff --git a/tests/phpunit/tests/rest-api/rest-themes-controller.php b/tests/phpunit/tests/rest-api/rest-themes-controller.php index dfa243d5770c9..2c5660f8e0083 100644 --- a/tests/phpunit/tests/rest-api/rest-themes-controller.php +++ b/tests/phpunit/tests/rest-api/rest-themes-controller.php @@ -419,6 +419,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'editor-color-palette', $theme_supports ); $this->assertArrayHasKey( 'editor-font-sizes', $theme_supports ); $this->assertArrayHasKey( 'editor-gradient-presets', $theme_supports ); + $this->assertArrayHasKey( 'editor-spacing-sizes', $theme_supports ); $this->assertArrayHasKey( 'editor-styles', $theme_supports ); $this->assertArrayHasKey( 'formats', $theme_supports ); $this->assertArrayHasKey( 'html5', $theme_supports ); @@ -426,7 +427,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'responsive-embeds', $theme_supports ); $this->assertArrayHasKey( 'title-tag', $theme_supports ); $this->assertArrayHasKey( 'wp-block-styles', $theme_supports ); - $this->assertCount( 23, $theme_supports, 'There should be 23 theme supports' ); + $this->assertCount( 24, $theme_supports, 'There should be 23 theme supports' ); } /** diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 5707e6a804a00..8c6d6d1524a75 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -3300,7 +3300,7 @@ public function test_get_editor_settings_custom_units_can_be_filtered() { public function test_export_data() { $theme = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3321,7 +3321,7 @@ public function test_export_data() { ); $user = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3345,7 +3345,7 @@ public function test_export_data() { $theme->merge( $user ); $actual = $theme->get_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3378,7 +3378,7 @@ public function test_export_data() { public function test_export_data_deals_with_empty_user_data() { $theme = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3400,7 +3400,7 @@ public function test_export_data_deals_with_empty_user_data() { $actual = $theme->get_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3428,7 +3428,7 @@ public function test_export_data_deals_with_empty_user_data() { public function test_export_data_deals_with_empty_theme_data() { $user = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3451,7 +3451,7 @@ public function test_export_data_deals_with_empty_theme_data() { $actual = $user->get_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'color' => array( 'palette' => array( @@ -3477,25 +3477,15 @@ public function test_export_data_deals_with_empty_theme_data() { * @ticket 55505 */ public function test_export_data_deals_with_empty_data() { - $theme_v2 = new WP_Theme_JSON( - array( - 'version' => 2, - ), - 'theme' - ); - $actual_v2 = $theme_v2->get_data(); - $expected_v2 = array( 'version' => 2 ); - $this->assertEqualSetsWithIndex( $expected_v2, $actual_v2 ); - - $theme_v1 = new WP_Theme_JSON( + $theme = new WP_Theme_JSON( array( - 'version' => 1, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, ), 'theme' ); - $actual_v1 = $theme_v1->get_data(); - $expected_v1 = array( 'version' => 2 ); - $this->assertEqualSetsWithIndex( $expected_v1, $actual_v1 ); + $actual = $theme->get_data(); + $expected = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); + $this->assertEqualSetsWithIndex( $expected, $actual ); } /** @@ -3504,7 +3494,7 @@ public function test_export_data_deals_with_empty_data() { public function test_export_data_sets_appearance_tools() { $theme = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'appearanceTools' => true, 'blocks' => array( @@ -3518,7 +3508,7 @@ public function test_export_data_sets_appearance_tools() { $actual = $theme->get_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'appearanceTools' => true, 'blocks' => array( @@ -3538,7 +3528,7 @@ public function test_export_data_sets_appearance_tools() { public function test_export_data_sets_use_root_padding_aware_alignments() { $theme = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'useRootPaddingAwareAlignments' => true, 'blocks' => array( @@ -3552,7 +3542,7 @@ public function test_export_data_sets_use_root_padding_aware_alignments() { $actual = $theme->get_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'useRootPaddingAwareAlignments' => true, 'blocks' => array( @@ -3644,7 +3634,7 @@ public function test_get_element_class_name_invalid() { public function test_get_property_value_valid() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'color' => array( 'background' => '#ffffff', @@ -3729,7 +3719,7 @@ public function data_get_property_value_should_return_string_for_invalid_paths_o public function test_get_property_value_loop() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'color' => array( 'background' => '#ffffff', @@ -3767,7 +3757,7 @@ public function test_get_property_value_loop() { public function test_get_property_value_recursion() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'color' => array( 'background' => '#ffffff', @@ -3804,7 +3794,7 @@ public function test_get_property_value_recursion() { public function test_get_property_value_self() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'color' => array( 'background' => '#ffffff', @@ -3830,7 +3820,7 @@ public function test_get_property_value_self() { public function test_get_styles_for_block_with_padding_aware_alignments() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'spacing' => array( 'padding' => array( @@ -3867,7 +3857,7 @@ public function test_get_styles_for_block_with_padding_aware_alignments() { public function test_get_styles_for_block_without_padding_aware_alignments() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'spacing' => array( 'padding' => array( @@ -3900,7 +3890,7 @@ public function test_get_styles_for_block_without_padding_aware_alignments() { public function test_get_styles_for_block_with_content_width() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'layout' => array( 'contentSize' => '800px', @@ -3931,7 +3921,7 @@ public function test_get_styles_for_block_with_content_width() { public function test_get_styles_with_appearance_tools() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'appearanceTools' => true, ), @@ -3954,7 +3944,7 @@ public function test_get_styles_with_appearance_tools() { public function test_sanitization() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'spacing' => array( 'blockGap' => 'valid value', @@ -3973,7 +3963,7 @@ public function test_sanitization() { $actual = $theme_json->get_raw_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'spacing' => array( 'blockGap' => 'valid value', @@ -3997,7 +3987,7 @@ public function test_sanitization() { public function test_sanitize_for_unregistered_style_variations() { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'blocks' => array( 'core/quote' => array( @@ -4021,7 +4011,7 @@ public function test_sanitize_for_unregistered_style_variations() { $sanitized_theme_json = $theme_json->get_raw_data(); $expected = array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'blocks' => array( 'core/quote' => array( @@ -4050,7 +4040,7 @@ public function test_sanitize_for_unregistered_style_variations() { public function test_sanitize_for_block_with_style_variations( $theme_json_variations, $expected_sanitized ) { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'blocks' => array( 'core/quote' => $theme_json_variations, @@ -4134,7 +4124,7 @@ public function data_sanitize_for_block_with_style_variations() { public function test_sanitize_indexed_arrays() { $theme_json = new WP_Theme_JSON( array( - 'version' => '2', + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'badKey2' => 'I am Evil!', 'settings' => array( 'badKey3' => 'I am Evil!', @@ -4202,7 +4192,7 @@ public function test_sanitize_indexed_arrays() { ); $expected_sanitized = array( - 'version' => '2', + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'typography' => array( 'fontFamilies' => array( @@ -4271,7 +4261,7 @@ public function test_sanitize_indexed_arrays() { public function test_sanitize_with_invalid_style_variation( $theme_json_variations ) { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'blocks' => array( 'core/quote' => $theme_json_variations, @@ -4319,7 +4309,7 @@ public function data_sanitize_with_invalid_style_variation() { public function test_get_styles_for_block_with_style_variations( $theme_json_variations, $metadata_variations, $expected ) { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'blocks' => array( 'core/quote' => $theme_json_variations, @@ -4473,16 +4463,16 @@ public function test_block_style_variations_with_invalid_properties() { public function test_set_spacing_sizes( $spacing_scale, $expected_output ) { $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'spacing' => array( 'spacingScale' => $spacing_scale, ), ), - ) + ), + 'default' ); - $theme_json->set_spacing_sizes(); $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); } @@ -4505,7 +4495,7 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), @@ -4521,12 +4511,12 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), array( - 'name' => '2', + 'name' => 'Large', 'slug' => '60', 'size' => '5.5rem', ), @@ -4542,17 +4532,17 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Small', 'slug' => '40', 'size' => '2.5rem', ), array( - 'name' => '2', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), array( - 'name' => '3', + 'name' => 'Large', 'slug' => '60', 'size' => '5.5rem', ), @@ -4568,22 +4558,22 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Small', 'slug' => '40', 'size' => '2.5rem', ), array( - 'name' => '2', + 'name' => 'Medium', 'slug' => '50', 'size' => '4rem', ), array( - 'name' => '3', + 'name' => 'Large', 'slug' => '60', 'size' => '5.5rem', ), array( - 'name' => '4', + 'name' => 'X-Large', 'slug' => '70', 'size' => '7rem', ), @@ -4599,27 +4589,27 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'Small', 'slug' => '40', 'size' => '2.5rem', ), array( - 'name' => '2', + 'name' => 'Medium', 'slug' => '50', 'size' => '5rem', ), array( - 'name' => '3', + 'name' => 'Large', 'slug' => '60', 'size' => '7.5rem', ), array( - 'name' => '4', + 'name' => 'X-Large', 'slug' => '70', 'size' => '10rem', ), array( - 'name' => '5', + 'name' => '2X-Large', 'slug' => '80', 'size' => '12.5rem', ), @@ -4635,27 +4625,27 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'X-Small', 'slug' => '30', 'size' => '0.67rem', ), array( - 'name' => '2', + 'name' => 'Small', 'slug' => '40', 'size' => '1rem', ), array( - 'name' => '3', + 'name' => 'Medium', 'slug' => '50', 'size' => '1.5rem', ), array( - 'name' => '4', + 'name' => 'Large', 'slug' => '60', 'size' => '2.25rem', ), array( - 'name' => '5', + 'name' => 'X-Large', 'slug' => '70', 'size' => '3.38rem', ), @@ -4671,27 +4661,27 @@ public function data_set_spacing_sizes() { ), 'expected_output' => array( array( - 'name' => '1', + 'name' => 'X-Small', 'slug' => '30', 'size' => '0.09rem', ), array( - 'name' => '2', + 'name' => 'Small', 'slug' => '40', 'size' => '0.38rem', ), array( - 'name' => '3', + 'name' => 'Medium', 'slug' => '50', 'size' => '1.5rem', ), array( - 'name' => '4', + 'name' => 'Large', 'slug' => '60', 'size' => '6rem', ), array( - 'name' => '5', + 'name' => 'X-Large', 'slug' => '70', 'size' => '24rem', ), @@ -4762,33 +4752,18 @@ public function data_set_spacing_sizes() { * @param array $expected_output Expected output from data provider. */ public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_output ) { - $this->expectException( Exception::class ); - $this->expectExceptionMessage( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ); - $theme_json = new WP_Theme_JSON( array( - 'version' => 2, + 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( 'spacing' => array( 'spacingScale' => $spacing_scale, ), ), - ) - ); - - // Ensure PHPUnit 10 compatibility. - set_error_handler( - static function ( $errno, $errstr ) { - restore_error_handler(); - throw new Exception( $errstr, $errno ); - }, - E_ALL + ), + 'default' ); - $theme_json->set_spacing_sizes(); - - restore_error_handler(); - $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); } @@ -4809,7 +4784,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 4, 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'non numeric increment' => array( 'spacing_scale' => array( @@ -4819,7 +4794,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 4, 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'non numeric steps' => array( 'spacing_scale' => array( @@ -4829,7 +4804,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 4, 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'non numeric medium step' => array( 'spacing_scale' => array( @@ -4839,7 +4814,7 @@ public function data_set_spacing_sizes_when_invalid() { 'mediumStep' => 'That which is just right', 'unit' => 'rem', ), - 'expected_output' => null, + 'expected_output' => array(), ), 'missing unit value' => array( 'spacing_scale' => array( @@ -4848,7 +4823,7 @@ public function data_set_spacing_sizes_when_invalid() { 'steps' => 5, 'mediumStep' => 4, ), - 'expected_output' => null, + 'expected_output' => array(), ), ); } diff --git a/tests/phpunit/tests/theme/wpThemeJsonSchema.php b/tests/phpunit/tests/theme/wpThemeJsonSchema.php index cb5bb41780c68..aa6f78eb03439 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonSchema.php +++ b/tests/phpunit/tests/theme/wpThemeJsonSchema.php @@ -41,6 +41,18 @@ public function test_migrate_v1_to_latest() { 'width' => false, ), 'typography' => array( + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), 'fontStyle' => false, 'fontWeight' => false, 'letterSpacing' => false, @@ -126,11 +138,24 @@ public function test_migrate_v1_to_latest() { 'width' => false, ), 'typography' => array( - 'fontStyle' => false, - 'fontWeight' => false, - 'letterSpacing' => false, - 'textDecoration' => false, - 'textTransform' => false, + 'defaultFontSizes' => false, + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + 'fontStyle' => false, + 'fontWeight' => false, + 'letterSpacing' => false, + 'textDecoration' => false, + 'textTransform' => false, ), 'blocks' => array( 'core/group' => array( @@ -185,4 +210,80 @@ public function test_migrate_v1_to_latest() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + + public function test_migrate_v2_to_latest() { + $theme_json_v2 = array( + 'version' => 2, + 'settings' => array( + 'typography' => array( + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + ), + 'spacing' => array( + 'spacingSizes' => array( + array( + 'name' => 'Small', + 'slug' => 20, + 'size' => '20px', + ), + array( + 'name' => 'Large', + 'slug' => 80, + 'size' => '80px', + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Schema::migrate( $theme_json_v2 ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'typography' => array( + 'defaultFontSizes' => false, + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + ), + 'spacing' => array( + 'defaultSpacingSizes' => false, + 'spacingSizes' => array( + array( + 'name' => 'Small', + 'slug' => 20, + 'size' => '20px', + ), + array( + 'name' => 'Large', + 'slug' => 80, + 'size' => '80px', + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 250c64cff1e5d..5a472f00f8b29 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -7855,9 +7855,9 @@ mockedApiResponse.Schema = { "theme_json_version": { "description": "Version of the theme.json schema used for the typography settings.", "type": "integer", - "default": 2, + "default": 3, "minimum": 2, - "maximum": 2, + "maximum": 3, "required": false }, "font_family_settings": { @@ -7924,9 +7924,9 @@ mockedApiResponse.Schema = { "theme_json_version": { "description": "Version of the theme.json schema used for the typography settings.", "type": "integer", - "default": 2, + "default": 3, "minimum": 2, - "maximum": 2, + "maximum": 3, "required": false }, "font_family_settings": { @@ -8057,9 +8057,9 @@ mockedApiResponse.Schema = { "theme_json_version": { "description": "Version of the theme.json schema used for the typography settings.", "type": "integer", - "default": 2, + "default": 3, "minimum": 2, - "maximum": 2, + "maximum": 3, "required": false }, "font_face_settings": { From 8e2a83ce35f7b907a32cd9a3347d7fd8c4acf15b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Tue, 4 Jun 2024 12:44:59 +0000 Subject: [PATCH 16/21] Build/Test Tools: Add a PHP version input for E2E workflow. This allows a specific version of PHP to be used when calling the reusable end-to-end testing workflow, which is particularly useful in older branches. Follow up to [58165], [58269], [58270]. See #61213. git-svn-id: https://develop.svn.wordpress.org/trunk@58329 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/reusable-end-to-end-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml index 70609e8a65359..7aa23fbb1aa9e 100644 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -14,6 +14,11 @@ on: required: false type: 'boolean' default: false + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' install-gutenberg: description: 'Whether to install the Gutenberg plugin.' required: false @@ -22,6 +27,7 @@ on: env: LOCAL_DIR: build + LOCAL_PHP: ${{ inputs.php-version }}${{ 'latest' != inputs.php-version && '-fpm' || '' }} jobs: # Runs the end-to-end test suite. From 6765098b23cc3cbf3d472f24a15a432f5e847585 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Tue, 4 Jun 2024 13:49:31 +0000 Subject: [PATCH 17/21] Build/Test Tools: Add an input to allow errors for the PHPUnit workflow. This allows a calling workflow to configure the PHPUnit workflow to `continue-on-error` when one occurs. This is useful for older branches where support for a specific version of PHP was not at 100% within the test suite. Follow up to [58165], [58269], [58270], [58329]. See #61213. git-svn-id: https://develop.svn.wordpress.org/trunk@58331 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/reusable-phpunit-tests.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/reusable-phpunit-tests.yml b/.github/workflows/reusable-phpunit-tests.yml index 60949915e4d1c..d5302b83b2a07 100644 --- a/.github/workflows/reusable-phpunit-tests.yml +++ b/.github/workflows/reusable-phpunit-tests.yml @@ -50,6 +50,11 @@ on: required: false type: 'boolean' default: false + allow-errors: + description: 'Whether to continue when test errors occur.' + required: false + type: boolean + default: false env: LOCAL_PHP: ${{ inputs.php }}-fpm LOCAL_DB_TYPE: ${{ inputs.db-type }} @@ -156,22 +161,27 @@ jobs: run: npm run env:install - name: Run PHPUnit tests + continue-on-error: ${{ inputs.allow-errors }} run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} - name: Run AJAX tests + continue-on-error: ${{ inputs.allow-errors }} run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --group ajax - name: Run ms-files tests as a multisite install if: ${{ inputs.multisite }} + continue-on-error: ${{ inputs.allow-errors }} run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --group ms-files - name: Run external HTTP tests if: ${{ ! inputs.multisite }} + continue-on-error: ${{ inputs.allow-errors }} run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --group external-http # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. - name: Run (Xdebug) tests if: ${{ inputs.php != '8.3' }} + continue-on-error: ${{ inputs.allow-errors }} run: LOCAL_PHP_XDEBUG=true node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit -v --group xdebug --exclude-group __fakegroup__ - name: Ensure version-controlled files are not modified or deleted From 6087189f266a7701c7afb6e5a0dda624eda32bad Mon Sep 17 00:00:00 2001 From: Joe McGill Date: Tue, 4 Jun 2024 14:07:13 +0000 Subject: [PATCH 18/21] Site Health: Add test for large autoloaded options. This adds a new Site Health check that will alert site owners if they are autoloading a large amount of data from the options table, as it could result in poor performance. The issue will be shown if the size of autoloaded options is greater than 800 KB, which can be adjusted using the new `site_status_autoloaded_options_size_limit` filter. Props mukesh27, joemcgill, rajinsharwar, costdev, audrasjb, krupajnanda, pooja1210, Ankit K Gupta, johnbillion, oglekler. Fixes #61276. git-svn-id: https://develop.svn.wordpress.org/trunk@58332 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-site-health.php | 102 ++++++++++++++++++ tests/phpunit/tests/admin/wpSiteHealth.php | 76 +++++++++++++ 2 files changed, 178 insertions(+) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 40d09cf5590eb..4fec62defa4cd 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2586,6 +2586,104 @@ public function get_test_persistent_object_cache() { return $result; } + /** + * Calculates total amount of autoloaded data. + * + * @since 6.6.0 + * + * @return int Autoloaded data in bytes. + */ + public function get_autoloaded_options_size() { + $alloptions = wp_load_alloptions(); + + $total_length = 0; + + foreach ( $alloptions as $option_name => $option_value ) { + $total_length += strlen( $option_value ); + } + + return $total_length; + } + + /** + * Tests the number of autoloaded options. + * + * @since 6.6.0 + * + * @return array The test results. + */ + public function get_test_autoloaded_options() { + $autoloaded_options_size = $this->get_autoloaded_options_size(); + $autoloaded_options_count = count( wp_load_alloptions() ); + + $base_description = __( 'Autoloaded options are configuration settings for plugins and themes that are automatically loaded with every page load in WordPress. Having too many autoloaded options can slow down your site.' ); + + $result = array( + 'label' => __( 'Autoloaded options are acceptable' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Performance' ), + 'color' => 'blue', + ), + 'description' => sprintf( + /* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */ + '

' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which is acceptable.' ) . '

', + $autoloaded_options_count, + size_format( $autoloaded_options_size ) + ), + 'actions' => '', + 'test' => 'autoloaded_options', + ); + + /** + * Filters max bytes threshold to trigger warning in Site Health. + * + * @since 6.6.0 + * + * @param int $limit Autoloaded options threshold size. Default 800000. + */ + $limit = apply_filters( 'site_status_autoloaded_options_size_limit', 800000 ); + + if ( $autoloaded_options_size < $limit ) { + return $result; + } + + $result['status'] = 'critical'; + $result['label'] = __( 'Autoloaded options could affect performance' ); + $result['description'] = sprintf( + /* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */ + '

' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which could cause your site to be slow. You can review the options being autoloaded in your database and remove any options that are no longer needed by your site.' ) . '

', + $autoloaded_options_count, + size_format( $autoloaded_options_size ) + ); + + /** + * Filters description to be shown on Site Health warning when threshold is met. + * + * @since 6.6.0 + * + * @param string $description Description message when autoloaded options bigger than threshold. + */ + $result['description'] = apply_filters( 'site_status_autoloaded_options_limit_description', $result['description'] ); + + $result['actions'] = sprintf( + /* translators: 1: HelpHub URL, 2: Link description. */ + '

%2$s

', + esc_url( __( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#autoloaded-options' ) ), + __( 'More info about optimizing autoloaded options' ) + ); + + /** + * Filters actionable information to tackle the problem. It can be a link to an external guide. + * + * @since 6.6.0 + * + * @param string $actions Call to Action to be used to point to the right direction to solve the issue. + */ + $result['actions'] = apply_filters( 'site_status_autoloaded_options_action_to_perform', $result['actions'] ); + return $result; + } + /** * Returns a set of tests that belong to the site status page. * @@ -2670,6 +2768,10 @@ public static function get_tests() { 'label' => __( 'Available disk space' ), 'test' => 'available_updates_disk_space', ), + 'autoloaded_options' => array( + 'label' => __( 'Autoloaded options' ), + 'test' => 'autoloaded_options', + ), ), 'async' => array( 'dotorg_communication' => array( diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 07fe300060c34..52afdb49d38f6 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -499,4 +499,80 @@ public function data_object_cache_thresholds() { array( 'alloptions_bytes', 1000 ), ); } + + /** + * Tests get_test_autoloaded_options() when autoloaded options less than warning size. + * + * @ticket 61276 + * + * @covers ::get_test_autoloaded_options() + */ + public function test_wp_autoloaded_options_test_no_warning() { + $expected_label = esc_html__( 'Autoloaded options are acceptable' ); + $expected_status = 'good'; + + $result = $this->instance->get_test_autoloaded_options(); + $this->assertSame( $expected_label, $result['label'], 'The label should indicate that autoloaded options are acceptable.' ); + $this->assertSame( $expected_status, $result['status'], 'The status should be "good" when autoloaded options are acceptable.' ); + } + + /** + * Tests get_test_autoloaded_options() when autoloaded options more than warning size. + * + * @ticket 61276 + * + * @covers ::get_test_autoloaded_options() + */ + public function test_wp_autoloaded_options_test_warning() { + self::set_autoloaded_option( 800000 ); + + $expected_label = esc_html__( 'Autoloaded options could affect performance' ); + $expected_status = 'critical'; + + $result = $this->instance->get_test_autoloaded_options(); + $this->assertSame( $expected_label, $result['label'], 'The label should indicate that autoloaded options could affect performance.' ); + $this->assertSame( $expected_status, $result['status'], 'The status should be "critical" when autoloaded options could affect performance.' ); + } + + /** + * Tests get_autoloaded_options_size(). + * + * @ticket 61276 + * + * @covers ::get_autoloaded_options_size() + */ + public function test_get_autoloaded_options_size() { + global $wpdb; + + $autoload_values = wp_autoload_values_to_autoload(); + + $autoloaded_options_size = (int) $wpdb->get_var( + $wpdb->prepare( + sprintf( + "SELECT SUM(LENGTH(option_value)) FROM $wpdb->options WHERE autoload IN (%s)", + implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ) + ), + $autoload_values + ) + ); + $this->assertSame( $autoloaded_options_size, $this->instance->get_autoloaded_options_size(), 'The size of autoloaded options should match the calculated size from the database.' ); + + // Add autoload option. + $test_option_string = 'test'; + $test_option_string_bytes = mb_strlen( $test_option_string, '8bit' ); + self::set_autoloaded_option( $test_option_string_bytes ); + $this->assertSame( $autoloaded_options_size + $test_option_string_bytes, $this->instance->get_autoloaded_options_size(), 'The size of autoloaded options should increase by the size of the newly added option.' ); + } + + /** + * Sets a test autoloaded option. + * + * @param int $bytes bytes to load in options. + */ + public static function set_autoloaded_option( $bytes = 800000 ) { + $heavy_option_string = wp_generate_password( $bytes ); + + // Force autoloading so that WordPress core does not override it. See https://core.trac.wordpress.org/changeset/57920. + add_option( 'test_set_autoloaded_option', $heavy_option_string, '', true ); + } } From ce6d1d632dc1b54ca1a01728733217980878aec2 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 4 Jun 2024 14:42:29 +0000 Subject: [PATCH 19/21] Login and Registration: Flush `user_activation_key` after successfully login. This changeset ensures the `user_activation_key` is flushed after successful login, so reset password links can not be used anymore after the user successfully log into their dashboard. Props nsinelnikov, rajinsharwar, Rahmohn, oglekler, hellofromTonya. Fixes #58901. See #32429 git-svn-id: https://develop.svn.wordpress.org/trunk@58333 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/user.php | 22 ++++++++++++++++++++++ tests/phpunit/tests/auth.php | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 7a71b78af9e84..5a3e6ddc984f6 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -110,6 +110,28 @@ function wp_signon( $credentials = array(), $secure_cookie = '' ) { } wp_set_auth_cookie( $user->ID, $credentials['remember'], $secure_cookie ); + + /** + * @global wpdb $wpdb WordPress database abstraction object. + */ + global $wpdb; + + // Flush `user_activation_key` if exists after successful login. + if ( ! empty( $user->user_activation_key ) ) { + $wpdb->update( + $wpdb->users, + array( + 'user_activation_key' => '', + ), + array( 'ID' => $user->ID ), + array( '%s' ), + array( '%d' ) + ); + + // Empty user_activation_key object. + $user->user_activation_key = ''; + } + /** * Fires after the user has successfully logged in. * diff --git a/tests/phpunit/tests/auth.php b/tests/phpunit/tests/auth.php index 88faa6f710ab8..dfaca811513c7 100644 --- a/tests/phpunit/tests/auth.php +++ b/tests/phpunit/tests/auth.php @@ -423,6 +423,31 @@ public function test_plaintext_user_activation_key_is_rejected() { $this->assertInstanceOf( 'WP_Error', $check ); } + /** + * Ensure that the user_activation_key is cleared (if available) after a successful login. + * + * @ticket 58901 + */ + public function test_user_activation_key_after_successful_login() { + global $wpdb; + + $reset_key = get_password_reset_key( $this->user ); + $user = wp_signon( + array( + 'user_login' => self::USER_LOGIN, + 'user_password' => self::USER_PASS, + ) + ); + $activation_key_from_database = $wpdb->get_var( + $wpdb->prepare( "SELECT user_activation_key FROM $wpdb->users WHERE ID = %d", $this->user->ID ) + ); + + $this->assertNotWPError( $reset_key, 'The password reset key was not created.' ); + $this->assertNotWPError( $user, 'The user was not authenticated.' ); + $this->assertEmpty( $user->user_activation_key, 'The `user_activation_key` was not empty on the user object returned by `wp_signon` function.' ); + $this->assertEmpty( $activation_key_from_database, 'The `user_activation_key` was not empty in the database.' ); + } + /** * Ensure users can log in using both their username and their email address. * From 447f6d7bc9dddbb95f9ec20b79f3abcbfd980e8f Mon Sep 17 00:00:00 2001 From: Joe McGill Date: Tue, 4 Jun 2024 14:50:24 +0000 Subject: [PATCH 20/21] Editor: Cache global styles for blocks. This caches the generated CSS from block nodes in merged Theme JSON data to avoid repeated costly operations required to compute style properties for blocks. The generated CSS is saved to a transient that expires every hour. Props thekt12, spacedmonkey, pereirinha, mukesh27, isabel_brison, oandregal, andrewserong, ramonjd. Fixes #59595. git-svn-id: https://develop.svn.wordpress.org/trunk@58334 602fd350-edb4-49c9-b593-d223f7449a82 --- .../global-styles-and-settings.php | 42 +++++++- .../theme/wpAddGlobalStylesForBlocks.php | 102 ++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/global-styles-and-settings.php b/src/wp-includes/global-styles-and-settings.php index fbf4fe2c52c3e..b413273a64974 100644 --- a/src/wp-includes/global-styles-and-settings.php +++ b/src/wp-includes/global-styles-and-settings.php @@ -307,8 +307,44 @@ function wp_add_global_styles_for_blocks() { $tree = WP_Theme_JSON_Resolver::get_merged_data(); $block_nodes = $tree->get_styles_block_nodes(); + + $can_use_cached = ! wp_is_development_mode( 'theme' ); + if ( $can_use_cached ) { + // Hash global settings and block nodes together to optimize performance of key generation. + $hash = md5( + wp_json_encode( + array( + 'global_setting' => wp_get_global_settings(), + 'block_nodes' => $block_nodes, + ) + ) + ); + + $cache_key = "wp_styles_for_blocks:$hash"; + $cached = get_site_transient( $cache_key ); + if ( ! is_array( $cached ) ) { + $cached = array(); + } + } + + $update_cache = false; + foreach ( $block_nodes as $metadata ) { - $block_css = $tree->get_styles_for_block( $metadata ); + + if ( $can_use_cached ) { + // Use the block name as the key for cached CSS data. Otherwise, use a hash of the metadata. + $cache_node_key = isset( $metadata['name'] ) ? $metadata['name'] : md5( wp_json_encode( $metadata ) ); + + if ( isset( $cached[ $cache_node_key ] ) ) { + $block_css = $cached[ $cache_node_key ]; + } else { + $block_css = $tree->get_styles_for_block( $metadata ); + $cached[ $cache_node_key ] = $block_css; + $update_cache = true; + } + } else { + $block_css = $tree->get_styles_for_block( $metadata ); + } if ( ! wp_should_load_separate_core_block_assets() ) { wp_add_inline_style( 'global-styles', $block_css ); @@ -354,6 +390,10 @@ function wp_add_global_styles_for_blocks() { } } } + + if ( $update_cache ) { + set_site_transient( $cache_key, $cached, HOUR_IN_SECONDS ); + } } /** diff --git a/tests/phpunit/tests/theme/wpAddGlobalStylesForBlocks.php b/tests/phpunit/tests/theme/wpAddGlobalStylesForBlocks.php index 7d34e8d2a6b33..fb547db50b763 100644 --- a/tests/phpunit/tests/theme/wpAddGlobalStylesForBlocks.php +++ b/tests/phpunit/tests/theme/wpAddGlobalStylesForBlocks.php @@ -75,6 +75,87 @@ public function test_third_party_blocks_inline_styles_get_registered_to_global_s ); } + /** + * Ensure that the block cache is set for global styles. + * + * @ticket 59595 + */ + public function test_styles_for_blocks_cache_is_set() { + $this->set_up_third_party_block(); + + wp_register_style( 'global-styles', false, array(), true, true ); + + $cache_key = $this->get_wp_styles_for_blocks_cache_key(); + $styles_for_blocks_before = get_site_transient( $cache_key ); + $this->assertFalse( $styles_for_blocks_before ); + + wp_add_global_styles_for_blocks(); + + $styles_for_blocks_after = get_site_transient( $cache_key ); + $this->assertNotEmpty( $styles_for_blocks_after ); + } + + /** + * Confirm that the block cache is skipped when in dev mode for themes. + * + * @ticket 59595 + */ + public function test_styles_for_blocks_skips_cache_in_dev_mode() { + global $_wp_tests_development_mode; + + $orig_dev_mode = $_wp_tests_development_mode; + + // Setting development mode to theme should skip the cache. + $_wp_tests_development_mode = 'theme'; + + wp_register_style( 'global-styles', false, array(), true, true ); + + // Initial register of global styles. + wp_add_global_styles_for_blocks(); + + $cache_key = $this->get_wp_styles_for_blocks_cache_key(); + $styles_for_blocks_initial = get_site_transient( $cache_key ); + + // Cleanup. + $_wp_tests_development_mode = $orig_dev_mode; + + $this->assertFalse( $styles_for_blocks_initial ); + } + + /** + * Confirm that the block cache is updated if the block meta has changed. + * + * @ticket 59595 + */ + public function test_styles_for_blocks_cache_is_skipped() { + wp_register_style( 'global-styles', false, array(), true, true ); + + // Initial register of global styles. + wp_add_global_styles_for_blocks(); + + $cache_key = $this->get_wp_styles_for_blocks_cache_key(); + $styles_for_blocks_initial = get_site_transient( $cache_key ); + $this->assertNotEmpty( $styles_for_blocks_initial, 'Initial cache was not set.' ); + + $this->set_up_third_party_block(); + + /* + * Call register of global styles again to ensure the cache is updated. + * In normal conditions, this function is only called once per request. + */ + wp_add_global_styles_for_blocks(); + + $cache_key = $this->get_wp_styles_for_blocks_cache_key(); + $styles_for_blocks_updated = get_site_transient( $cache_key ); + $this->assertNotEmpty( $styles_for_blocks_updated, 'Updated cache was not set.' ); + + $this->assertNotEquals( + $styles_for_blocks_initial, + $styles_for_blocks_updated, + 'Block style cache was not updated.' + ); + } + /** * @ticket 56915 * @ticket 61165 @@ -253,4 +334,25 @@ private function get_global_styles() { $actual = wp_styles()->get_data( 'global-styles', 'after' ); return is_array( $actual ) ? $actual : array(); } + + /** + * Get cache key for `wp_styles_for_blocks`. + * + * @return string The cache key. + */ + private function get_wp_styles_for_blocks_cache_key() { + $tree = WP_Theme_JSON_Resolver::get_merged_data(); + $block_nodes = $tree->get_styles_block_nodes(); + // md5 is a costly operation, so we hashing global settings and block_node in a single call. + $hash = md5( + wp_json_encode( + array( + 'global_setting' => wp_get_global_settings(), + 'block_nodes' => $block_nodes, + ) + ) + ); + + return "wp_styles_for_blocks:$hash"; + } } From 1e208fc0530f8cfec21948cbadd7e4c97e6944f5 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Tue, 4 Jun 2024 15:27:57 +0000 Subject: [PATCH 21/21] Comments: Ensure the correct comment ID type is passed to `get_comment_author`. The `$comment_id` parameter of the `get_comment_author` filter is documented as a numeric string, however in case a non-existing comment ID is passed to the `get_comment_author()` function, it could be an integer instead. This commit resolves the issue and adds a PHPUnit test demonstrating the behavior. Includes updating `get_comment_author_url()` unit tests for consistency. Follow-up to [41127], [52818]. Props david.binda. Fixes #60475. git-svn-id: https://develop.svn.wordpress.org/trunk@58335 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/comment-template.php | 2 +- .../tests/comment/getCommentAuthor.php | 59 +++++++++++++++++++ .../comment/getCommentAuthorEmailLink.php | 1 + .../tests/comment/getCommentAuthorUrl.php | 29 +++++---- .../tests/comment/getCommentAuthorUrlLink.php | 1 + 5 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 tests/phpunit/tests/comment/getCommentAuthor.php diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index d125f0c2dda9f..8c560973339b8 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -24,7 +24,7 @@ function get_comment_author( $comment_id = 0 ) { $comment = get_comment( $comment_id ); - $comment_id = ! empty( $comment->comment_ID ) ? $comment->comment_ID : $comment_id; + $comment_id = ! empty( $comment->comment_ID ) ? $comment->comment_ID : (string) $comment_id; if ( empty( $comment->comment_author ) ) { $user = ! empty( $comment->user_id ) ? get_userdata( $comment->user_id ) : false; diff --git a/tests/phpunit/tests/comment/getCommentAuthor.php b/tests/phpunit/tests/comment/getCommentAuthor.php new file mode 100644 index 0000000000000..31d5a57bdbe7e --- /dev/null +++ b/tests/phpunit/tests/comment/getCommentAuthor.php @@ -0,0 +1,59 @@ +comment->create_and_get( + array( + 'comment_post_ID' => 0, + ) + ); + } + + public function get_comment_author_filter( $comment_author, $comment_id, $comment ) { + $this->assertSame( $comment_id, self::$comment->comment_ID, 'Comment IDs do not match.' ); + $this->assertTrue( is_string( $comment_id ), '$comment_id parameter is not a string.' ); + + return $comment_author; + } + + public function test_comment_author_passes_correct_comment_id_for_comment_object() { + add_filter( 'get_comment_author', array( $this, 'get_comment_author_filter' ), 99, 3 ); + + get_comment_author( self::$comment ); + } + + public function test_comment_author_passes_correct_comment_id_for_int() { + add_filter( 'get_comment_author', array( $this, 'get_comment_author_filter' ), 99, 3 ); + + get_comment_author( (int) self::$comment->comment_ID ); + } + + public function get_comment_author_filter_non_existent_id( $comment_author, $comment_id, $comment ) { + $this->assertSame( $comment_id, (string) self::$non_existent_comment_id, 'Comment IDs do not match.' ); + $this->assertTrue( is_string( $comment_id ), '$comment_id parameter is not a string.' ); + + return $comment_author; + } + + /** + * @ticket 60475 + */ + public function test_comment_author_passes_correct_comment_id_for_non_existent_comment() { + add_filter( 'get_comment_author', array( $this, 'get_comment_author_filter_non_existent_id' ), 99, 3 ); + + self::$non_existent_comment_id = self::$comment->comment_ID + 1; + + get_comment_author( self::$non_existent_comment_id ); // Non-existent comment ID. + } +} diff --git a/tests/phpunit/tests/comment/getCommentAuthorEmailLink.php b/tests/phpunit/tests/comment/getCommentAuthorEmailLink.php index a203d38122e30..c505c9750f48b 100644 --- a/tests/phpunit/tests/comment/getCommentAuthorEmailLink.php +++ b/tests/phpunit/tests/comment/getCommentAuthorEmailLink.php @@ -5,6 +5,7 @@ * @covers ::get_comment_author_email_link */ class Tests_Comment_GetCommentAuthorEmailLink extends WP_UnitTestCase { + public static $comment; public function set_up() { diff --git a/tests/phpunit/tests/comment/getCommentAuthorUrl.php b/tests/phpunit/tests/comment/getCommentAuthorUrl.php index bc008b19fcce2..e54e043b50c24 100644 --- a/tests/phpunit/tests/comment/getCommentAuthorUrl.php +++ b/tests/phpunit/tests/comment/getCommentAuthorUrl.php @@ -6,26 +6,31 @@ * @covers ::get_comment_author_url */ class Tests_Comment_GetCommentAuthorUrl extends WP_UnitTestCase { - public function get_comment_author_url_filter( $url, $id, $comment ) { - $this->assertSame( $id, $comment->comment_ID ); - return $url; - } + private static $comment; - /** - * @ticket 41334 - */ - public function test_comment_author_url_passes_correct_comment_id() { - $comment = self::factory()->comment->create_and_get( + public static function set_up_before_class() { + parent::set_up_before_class(); + + self::$comment = self::factory()->comment->create_and_get( array( 'comment_post_ID' => 0, ) ); + } - add_filter( 'get_comment_author_url', array( $this, 'get_comment_author_url_filter' ), 99, 3 ); + public function get_comment_author_url_filter( $comment_author_url, $comment_id, $comment ) { + $this->assertSame( $comment_id, $comment->comment_ID ); - get_comment_author_url( $comment ); + return $comment_author_url; + } + + /** + * @ticket 41334 + */ + public function test_comment_author_url_passes_correct_comment_id() { + add_filter( 'get_comment_author_url', array( $this, 'get_comment_author_url_filter' ), 99, 3 ); - remove_filter( 'get_comment_author_url', array( $this, 'get_comment_author_url_filter' ), 99 ); + get_comment_author_url( self::$comment ); } } diff --git a/tests/phpunit/tests/comment/getCommentAuthorUrlLink.php b/tests/phpunit/tests/comment/getCommentAuthorUrlLink.php index 7e60b9cc1b194..ee964d96d9de0 100644 --- a/tests/phpunit/tests/comment/getCommentAuthorUrlLink.php +++ b/tests/phpunit/tests/comment/getCommentAuthorUrlLink.php @@ -6,6 +6,7 @@ * @covers ::get_comment_author_url_link */ class Tests_Comment_GetCommentAuthorUrlLink extends WP_UnitTestCase { + protected static $comments = array(); public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {