From a133b6d2447f74251a748ac330d44e2114c22c6f Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 9 Sep 2022 12:37:47 +0000 Subject: [PATCH 01/66] KSES: Allow assigning values to CSS variables. The `safecss_filter_attr()` function allows using custom CSS variables like `color: var(--color)`. However, it did not allow assigning values to CSS variables like `--color: #F00`, which is common in Global Styles and Gutenberg. This commit adds support for assigning values to CSS variables, so that the function can be used consistently in Global Styles and the future Style Engine in Gutenberg. Follow-up to [50923], [54100]. Props aristath, ramonopoly, SergeyBiryukov. Fixes #56353. git-svn-id: https://develop.svn.wordpress.org/trunk@54117 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/kses.php | 18 +++++++++++++++++- tests/phpunit/tests/kses.php | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 2be0b5e14c3cf..7776de59d55d6 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2229,7 +2229,7 @@ function kses_init() { * @since 5.7.1 Added support for `object-position`. * @since 5.8.0 Added support for `calc()` and `var()` values. * @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`, - * and nested `var()` values. + * nested `var()` values, and assigning values to CSS variables. * Added support for `gap`, `column-gap`, `row-gap`, and `flex-wrap`. * Extended `margin-*` and `padding-*` support for logical properties. * @@ -2391,6 +2391,9 @@ function safecss_filter_attr( $css, $deprecated = '' ) { 'object-position', 'overflow', 'vertical-align', + + // Custom CSS properties. + '--*', ) ); @@ -2436,6 +2439,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { $found = false; $url_attr = false; $gradient_attr = false; + $is_custom_var = false; if ( strpos( $css_item, ':' ) === false ) { $found = true; @@ -2443,11 +2447,23 @@ function safecss_filter_attr( $css, $deprecated = '' ) { $parts = explode( ':', $css_item, 2 ); $css_selector = trim( $parts[0] ); + // Allow assigning values to CSS variables. + if ( in_array( '--*', $allowed_attr, true ) && preg_match( '/^--[a-zA-Z0-9-_]+$/', $css_selector ) ) { + $allowed_attr[] = $css_selector; + $is_custom_var = true; + } + if ( in_array( $css_selector, $allowed_attr, true ) ) { $found = true; $url_attr = in_array( $css_selector, $css_url_data_types, true ); $gradient_attr = in_array( $css_selector, $css_gradient_data_types, true ); } + + if ( $is_custom_var ) { + $css_value = trim( $parts[1] ); + $url_attr = str_starts_with( $css_value, 'url(' ); + $gradient_attr = str_contains( $css_value, '-gradient(' ); + } } if ( $found && $url_attr ) { diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 274c5aaf7be06..5871f0fe3397f 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1247,6 +1247,23 @@ public function data_test_safecss_filter_attr() { 'css' => 'margin-block-start: 1px;margin-block-end: 2px;margin-inline-start: 3px;margin-inline-end: 4px;padding-block-start: 1px;padding-block-end: 2px;padding-inline-start: 3px;padding-inline-end: 4px', 'expected' => 'margin-block-start: 1px;margin-block-end: 2px;margin-inline-start: 3px;margin-inline-end: 4px;padding-block-start: 1px;padding-block-end: 2px;padding-inline-start: 3px;padding-inline-end: 4px', ), + // Assigning values to CSS variables introduced in 6.1. + array( + 'css' => '--wp--medium-width: 100px; --var_with_underscores: #cccccc;', + 'expected' => '--wp--medium-width: 100px;--var_with_underscores: #cccccc', + ), + array( + 'css' => '--miXeD-CAse: red; --with-numbers-3_56: red; --with-url-value: url("foo.jpg");', + 'expected' => '--miXeD-CAse: red;--with-numbers-3_56: red;--with-url-value: url("foo.jpg")', + ), + array( + 'css' => '--with-gradient: repeating-linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);', + 'expected' => '--with-gradient: repeating-linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)', + ), + array( + 'css' => '--?><.%-not-allowed: red;', + 'expected' => '', + ), ); } From 438822aa5b8eab673c4b372474d6cb69bd9799da Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 10 Sep 2022 12:37:00 +0000 Subject: [PATCH 02/66] Editor: Backport Elements API updates. This commit backports the original PRs from Gutenberg repository: * [https://github.com/WordPress/gutenberg/pull/40260 #40260 Add support for button elements to theme.json] * [https://github.com/WordPress/gutenberg/pull/40889 #40889 Theme Json: Don't output double selectors for elements inside blocks] * [https://github.com/WordPress/gutenberg/pull/41140 #41140 Global Styles: Add support for caption elements] * [https://github.com/WordPress/gutenberg/pull/41160 #41160 Global Styles: Load block CSS conditionally] * [https://github.com/WordPress/gutenberg/pull/41240 #41240 Global Styles: Button Element: update button element selector] * [https://github.com/WordPress/gutenberg/pull/41335 #41335 Duotone: Fix CSS Selectors rendered by theme.json duotone/filter settings for blocks on public pages] * [https://github.com/WordPress/gutenberg/pull/41446 #41446 Block styles: Account for style block nodes that have no name] * [https://github.com/WordPress/gutenberg/pull/41696 #41696 Global Styles: Allow references to values in other locations in the tree] * [https://github.com/WordPress/gutenberg/pull/41753 #41753 Elements: Add an API make it easier to get class names] * [https://github.com/WordPress/gutenberg/pull/41786 #41786 Support pseudo selectors on elements in theme json] * [https://github.com/WordPress/gutenberg/pull/41822 #41822 Elements: Button - Fix element selectors] * [https://github.com/WordPress/gutenberg/pull/41981 #41981 Global Styles: Add support for heading elements] * [https://github.com/WordPress/gutenberg/pull/42072 #42072 Fix link element hover bleeding into button element default styles] * [https://github.com/WordPress/gutenberg/pull/42096 #42096 Add visited to link element allowed pseudo selector list] * [https://github.com/WordPress/gutenberg/pull/42669 #42669 Link elements: Add a :where selector to the :not to lower specificity] * [https://github.com/WordPress/gutenberg/pull/42776 #42776 Theme JSON: Add a static $blocks_metadata data definition to the Gutenberg instance of WP_Theme_JSON] * [https://github.com/WordPress/gutenberg/pull/43088 #43088 Pseudo elements supports on button elements] * [https://github.com/WordPress/gutenberg/pull/43167 #43167 Theme_JSON: Use existing append_to_selector for pseudo elements] * [https://github.com/WordPress/gutenberg/pull/43988 #43988 Styles API: Fixed selectors for nested elements] Props onemaggie, bernhard-reiter, cbravobernal, mmaattiiaass, scruffian, andraganescu, dpcalhoun, get_dave, Mamaduka, SergeyBiryukov. See #56467. git-svn-id: https://develop.svn.wordpress.org/trunk@54118 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-theme-json.php | 416 ++++++++++++--- .../global-styles-and-settings.php | 42 ++ src/wp-includes/script-loader.php | 34 ++ tests/phpunit/tests/theme/wpThemeJson.php | 504 +++++++++++++++++- 4 files changed, 894 insertions(+), 102 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 2833e8be9c94d..69a349f970cc7 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -343,15 +343,27 @@ class WP_Theme_JSON { ), ); + /** + * Defines which pseudo selectors are enabled for which elements. + * + * Note: this will affect both top-level and block-level elements. + * + * @since 6.1.0 + */ + const VALID_ELEMENT_PSEUDO_SELECTORS = array( + 'link' => array( ':hover', ':focus', ':active', ':visited' ), + 'button' => array( ':hover', ':focus', ':active', ':visited' ), + ); + /** * The valid elements that can be found under styles. * * @since 5.8.0 - * @since 6.1.0 Added `heading`, `button`, and `caption` to the elements. + * @since 6.1.0 Added `heading`, `button`. and `caption` elements. * @var string[] */ const ELEMENTS = array( - 'link' => 'a', + 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity. 'heading' => 'h1, h2, h3, h4, h5, h6', 'h1' => 'h1', 'h2' => 'h2', @@ -365,6 +377,29 @@ class WP_Theme_JSON { 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption', ); + const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( + 'button' => 'wp-element-button', + 'caption' => 'wp-element-caption', + ); + + /** + * Returns a class name by an element name. + * + * @since 6.1.0 + * + * @param string $element The name of the element. + * @return string The name of the class. + */ + public static function get_element_class_name( $element ) { + $class_name = ''; + + if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) { + $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ]; + } + + return $class_name; + } + /** * Options that settings.appearanceTools enables. * @@ -488,16 +523,21 @@ protected static function do_opt_in_into_settings( &$context ) { * @return array The sanitized output. */ protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { + $output = array(); if ( ! is_array( $input ) ) { return $output; } + // Preserve only the top most level keys. $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); - // Some styles are only meant to be available at the top-level (e.g.: blockGap), - // hence, the schema for blocks & elements should not have them. + /* + * Remove any rules that are annotated as "top" in VALID_STYLES constant. + * Some styles are only meant to be available at the top-level (e.g.: blockGap), + * hence, the schema for blocks & elements should not have them. + */ $styles_non_top_level = static::VALID_STYLES; foreach ( array_keys( $styles_non_top_level ) as $section ) { foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { @@ -510,9 +550,24 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n // Build the schema based on valid block & element names. $schema = array(); $schema_styles_elements = array(); + + /* + * Set allowed element pseudo selectors based on per element allow list. + * Target data structure in schema: + * e.g. + * - top level elements: `$schema['styles']['elements']['link'][':hover']`. + * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. + */ foreach ( $valid_element_names as $element ) { $schema_styles_elements[ $element ] = $styles_non_top_level; + + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; + } + } } + $schema_styles_blocks = array(); $schema_settings_blocks = array(); foreach ( $valid_block_names as $block ) { @@ -520,6 +575,7 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks[ $block ] = $styles_non_top_level; $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; } + $schema['styles'] = static::VALID_STYLES; $schema['styles']['blocks'] = $schema_styles_blocks; $schema['styles']['elements'] = $schema_styles_elements; @@ -549,6 +605,30 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n return $output; } + /** + * Appends a sub-selector to an existing one. + * + * Given the compounded $selector "h1, h2, h3" + * and the $to_append selector ".some-class" the result will be + * "h1.some-class, h2.some-class, h3.some-class". + * + * @since 5.8.0 + * @since 6.1.0 Added append position. + * + * @param string $selector Original selector. + * @param string $to_append Selector to append. + * @param string $position A position sub-selector should be appended. Default 'right'. + * @return string + */ + protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { + $new_selectors = array(); + $selectors = explode( ',', $selector ); + foreach ( $selectors as $sel ) { + $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel; + } + return implode( ',', $new_selectors ); + } + /** * Returns the metadata for each block. * @@ -611,7 +691,11 @@ protected static function get_blocks_metadata() { foreach ( static::ELEMENTS as $el_name => $el_selector ) { $element_selector = array(); foreach ( $block_selectors as $selector ) { - $element_selector[] = $selector . ' ' . $el_selector; + if ( $selector === $el_selector ) { + $element_selector = array( $el_selector ); + break; + } + $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' ); } static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); } @@ -810,54 +894,7 @@ protected function get_block_classes( $style_nodes ) { if ( null === $metadata['selector'] ) { continue; } - - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $selector = $metadata['selector']; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - $declarations = static::compute_style_properties( $node, $settings ); - - // 1. Separate the ones who use the general selector - // and the ones who use the duotone selector. - $declarations_duotone = array(); - foreach ( $declarations as $index => $declaration ) { - if ( 'filter' === $declaration['name'] ) { - unset( $declarations[ $index ] ); - $declarations_duotone[] = $declaration; - } - } - - /* - * Reset default browser margin on the root body element. - * This is set on the root selector **before** generating the ruleset - * from the `theme.json`. This is to ensure that if the `theme.json` declares - * `margin` in its `spacing` declaration for the `body` element then these - * user-generated values take precedence in the CSS cascade. - * @link https://github.com/WordPress/gutenberg/issues/36147. - */ - if ( static::ROOT_BLOCK_SELECTOR === $selector ) { - $block_rules .= 'body { margin: 0; }'; - } - - // 2. Generate the rules that use the general selector. - $block_rules .= static::to_ruleset( $selector, $declarations ); - - // 3. Generate the rules that use the duotone selector. - if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { - $selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] ); - $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); - } - - if ( static::ROOT_BLOCK_SELECTOR === $selector ) { - $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; - $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; - $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; - if ( $has_block_gap_support ) { - $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; - $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }'; - } - } + $block_rules .= static::get_styles_for_block( $metadata ); } return $block_rules; @@ -972,29 +1009,6 @@ static function ( $carry, $element ) { return $selector . '{' . $declaration_block . '}'; } - /** - * Function that appends a sub-selector to a existing one. - * - * Given the compounded $selector "h1, h2, h3" - * and the $to_append selector ".some-class" the result will be - * "h1.some-class, h2.some-class, h3.some-class". - * - * @since 5.8.0 - * - * @param string $selector Original selector. - * @param string $to_append Selector to append. - * @return string - */ - protected static function append_to_selector( $selector, $to_append ) { - $new_selectors = array(); - $selectors = explode( ',', $selector ); - foreach ( $selectors as $sel ) { - $new_selectors[] = $sel . $to_append; - } - - return implode( ',', $new_selectors ); - } - /** * Given a settings array, it returns the generated rulesets * for the preset classes. @@ -1312,13 +1326,15 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { * * @since 5.8.0 * @since 5.9.0 Added the `$settings` and `$properties` parameters. + * @since 6.1.0 Added the `$theme_json` parameter. * * @param array $styles Styles to process. * @param array $settings Theme settings. * @param array $properties Properties metadata. + * @param array $theme_json Theme JSON array. * @return array Returns the modified $declarations. */ - protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) { + protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null ) { if ( null === $properties ) { $properties = static::PROPERTIES_METADATA; } @@ -1329,7 +1345,7 @@ protected static function compute_style_properties( $styles, $settings = array() } foreach ( $properties as $css_property => $value_path ) { - $value = static::get_property_value( $styles, $value_path ); + $value = static::get_property_value( $styles, $value_path, $theme_json ); // Look up protected properties, keyed by value path. // Skip protected properties that are explicitly set to `null`. @@ -1365,20 +1381,58 @@ protected static function compute_style_properties( $styles, $settings = array() * "var:preset|color|secondary" to the form * "--wp--preset--color--secondary". * + * It also converts references to a path to the value + * stored at that location, e.g. + * { "ref": "style.color.background" } => "#fff". + * * @since 5.8.0 * @since 5.9.0 Added support for values of array type, which are returned as is. + * @since 6.1.0 Added the `$theme_json` parameter. * * @param array $styles Styles subtree. * @param array $path Which property to process. + * @param array $theme_json Theme JSON array. * @return string|array Style property value. */ - protected static function get_property_value( $styles, $path ) { + protected static function get_property_value( $styles, $path, $theme_json = null ) { $value = _wp_array_get( $styles, $path, '' ); + /* + * This converts references to a path to the value at that path + * where the values is an array with a "ref" key, pointing to a path. + * For example: { "ref": "style.color.background" } => "#fff". + */ + if ( is_array( $value ) && array_key_exists( 'ref', $value ) ) { + $value_path = explode( '.', $value['ref'] ); + $ref_value = _wp_array_get( $theme_json, $value_path ); + // Only use the ref value if we find anything. + if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { + $value = $ref_value; + } + + if ( is_array( $ref_value ) && array_key_exists( 'ref', $ref_value ) ) { + $path_string = json_encode( $path ); + $ref_value_string = json_encode( $ref_value ); + _doing_it_wrong( + 'get_property_value', + sprintf( + /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */ + __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ), + 'theme.json', + $ref_value_string, + $path_string, + $ref_value['ref'] + ), + '6.1.0' + ); + } + } + if ( '' === $value || is_array( $value ) ) { return $value; } + // Convert custom CSS properties. $prefix = 'var:'; $prefix_len = strlen( $prefix ); $token_in = '|'; @@ -1490,6 +1544,19 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { 'path' => array( 'styles', 'elements', $element ), 'selector' => static::ELEMENTS[ $element ], ); + + // Handle any pseudo selectors for the element. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + + if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'elements', $element ), + 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), + ); + } + } + } } } @@ -1498,6 +1565,51 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { return $nodes; } + $nodes = array_merge( $nodes, static::get_block_nodes( $theme_json ) ); + + /** + * Filters the list of style nodes with metadata. + * + * This allows for things like loading block CSS independently. + * + * @since 6.1.0 + * + * @param array $nodes Style nodes with metadata. + */ + return apply_filters( 'get_style_nodes', $nodes ); + } + + /** + * A public helper to get the block nodes from a theme.json file. + * + * @since 6.1.0 + * + * @return array The block nodes in theme.json. + */ + public function get_styles_block_nodes() { + return static::get_block_nodes( $this->theme_json ); + } + + /** + * An internal method to get the block nodes from a theme.json file. + * + * @since 6.1.0 + * + * @param array $theme_json The theme.json converted to an array. + * @return array The block nodes in theme.json. + */ + private static function get_block_nodes( $theme_json ) { + $selectors = static::get_blocks_metadata(); + $nodes = array(); + if ( ! isset( $theme_json['styles'] ) ) { + return $nodes; + } + + // Blocks. + if ( ! isset( $theme_json['styles']['blocks'] ) ) { + return $nodes; + } + foreach ( $theme_json['styles']['blocks'] as $name => $node ) { $selector = null; if ( isset( $selectors[ $name ]['selector'] ) ) { @@ -1510,6 +1622,7 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { } $nodes[] = array( + 'name' => $name, 'path' => array( 'styles', 'blocks', $name ), 'selector' => $selector, 'duotone' => $duotone_selector, @@ -1521,6 +1634,18 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 'selector' => $selectors[ $name ]['elements'][ $element ], ); + + // Handle any pseudo selectors for the element. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), + 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), + ); + } + } + } } } } @@ -1528,6 +1653,116 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { return $nodes; } + /** + * Gets the CSS rules for a particular block from theme.json. + * + * @since 6.1.0 + * + * @param array $block_metadata Meta data about the block to get styles for. + * @return array Styles for the block. + */ + public function get_styles_for_block( $block_metadata ) { + + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + + $selector = $block_metadata['selector']; + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + + /* + * Get a reference to element name from path. + * $block_metadata['path'] = array( 'styles','elements','link' ); + * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ]. + * Skip non-element paths like just ['styles']. + */ + $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); + + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; + + $element_pseudo_allowed = array(); + + if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; + } + + /* + * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). + * This also resets the array keys. + */ + $pseudo_matches = array_values( + array_filter( + $element_pseudo_allowed, + function( $pseudo_selector ) use ( $selector ) { + return str_contains( $selector, $pseudo_selector ); + } + ) + ); + + $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; + + /* + * If the current selector is a pseudo selector that's defined in the allow list for the current + * element then compute the style properties for it. + * Otherwise just compute the styles for the default selector as normal. + */ + if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && + array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) + && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) + ) { + $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json ); + } else { + $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json ); + } + + $block_rules = ''; + + /* + * 1. Separate the declarations that use the general selector + * from the ones using the duotone selector. + */ + $declarations_duotone = array(); + foreach ( $declarations as $index => $declaration ) { + if ( 'filter' === $declaration['name'] ) { + unset( $declarations[ $index ] ); + $declarations_duotone[] = $declaration; + } + } + + /* + * Reset default browser margin on the root body element. + * This is set on the root selector **before** generating the ruleset + * from the `theme.json`. This is to ensure that if the `theme.json` declares + * `margin` in its `spacing` declaration for the `body` element then these + * user-generated values take precedence in the CSS cascade. + * @link https://github.com/WordPress/gutenberg/issues/36147. + */ + if ( static::ROOT_BLOCK_SELECTOR === $selector ) { + $block_rules .= 'body { margin: 0; }'; + } + + // 2. Generate and append the rules that use the general selector. + $block_rules .= static::to_ruleset( $selector, $declarations ); + + // 3. Generate and append the rules that use the duotone selector. + if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { + $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); + $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); + } + + if ( static::ROOT_BLOCK_SELECTOR === $selector ) { + $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; + $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; + $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + + $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; + if ( $has_block_gap_support ) { + $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; + $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }'; + } + } + + return $block_rules; + } + /** * For metadata values that can either be booleans or paths to booleans, gets the value. * @@ -1837,10 +2072,12 @@ public static function remove_insecure_properties( $theme_json ) { $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); - $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); + + $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); $blocks_metadata = static::get_blocks_metadata(); $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); + foreach ( $style_nodes as $metadata ) { $input = _wp_array_get( $theme_json, $metadata['path'], array() ); if ( empty( $input ) ) { @@ -1848,6 +2085,25 @@ public static function remove_insecure_properties( $theme_json ) { } $output = static::remove_insecure_styles( $input ); + + /* + * Get a reference to element name from path. + * $metadata['path'] = array( 'styles', 'elements', 'link' ); + */ + $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; + + /* + * $output is stripped of pseudo selectors. Re-add and process them + * or insecure styles here. + */ + if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { + if ( isset( $input[ $pseudo_selector ] ) ) { + $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); + } + } + } + if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } diff --git a/src/wp-includes/global-styles-and-settings.php b/src/wp-includes/global-styles-and-settings.php index efc2edcaaa354..2e642cc2b1e05 100644 --- a/src/wp-includes/global-styles-and-settings.php +++ b/src/wp-includes/global-styles-and-settings.php @@ -192,3 +192,45 @@ function wp_get_global_styles_svg_filters() { return $svgs; } + +/** + * Adds global style rules to the inline style for each block. + * + * @since 6.1.0 + */ +function wp_add_global_styles_for_blocks() { + $tree = WP_Theme_JSON_Resolver::get_merged_data(); + $block_nodes = $tree->get_styles_block_nodes(); + foreach ( $block_nodes as $metadata ) { + $block_css = $tree->get_styles_for_block( $metadata ); + + if ( isset( $metadata['name'] ) ) { + $block_name = str_replace( 'core/', '', $metadata['name'] ); + /* + * These block styles are added on block_render. + * This hooks inline CSS to them so that they are loaded conditionally + * based on whether or not the block is used on the page. + */ + wp_add_inline_style( 'wp-block-' . $block_name, $block_css ); + } + + // The likes of block element styles from theme.json do not have $metadata['name'] set. + if ( ! isset( $metadata['name'] ) && ! empty( $metadata['path'] ) ) { + $result = array_values( + array_filter( + $metadata['path'], + function ( $item ) { + if ( strpos( $item, 'core/' ) !== false ) { + return true; + } + return false; + } + ) + ); + if ( isset( $result[0] ) ) { + $block_name = str_replace( 'core/', '', $result[0] ); + wp_add_inline_style( 'wp-block-' . $block_name, $block_css ); + } + } + } +} diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 11858ec9bfe10..354845fb9d670 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2353,6 +2353,30 @@ function wp_common_block_scripts_and_styles() { do_action( 'enqueue_block_assets' ); } +/** + * Applies a filter to the list of style nodes that comes from WP_Theme_JSON::get_style_nodes(). + * + * This particular filter removes all of the blocks from the array. + * + * We want WP_Theme_JSON to be ignorant of the implementation details of how the CSS is being used. + * This filter allows us to modify the output of WP_Theme_JSON depending on whether or not we are + * loading separate assets, without making the class aware of that detail. + * + * @since 6.1.0 + * + * @param array $nodes The nodes to filter. + * @return array A filtered array of style nodes. + */ +function wp_filter_out_block_nodes( $nodes ) { + return array_filter( + $nodes, + function( $node ) { + return ! in_array( 'blocks', $node['path'], true ); + }, + ARRAY_FILTER_USE_BOTH + ); +} + /** * Enqueues the global styles defined via theme.json. * @@ -2377,6 +2401,16 @@ function wp_enqueue_global_styles() { return; } + /* + * If we are loading CSS for each block separately, then we can load the theme.json CSS conditionally. + * This removes the CSS from the global-styles stylesheet and adds it to the inline CSS for each block. + */ + if ( $separate_assets ) { + add_filter( 'get_style_nodes', 'wp_filter_out_block_nodes' ); + // Add each block as an inline css. + wp_add_global_styles_for_blocks(); + } + $stylesheet = wp_get_global_stylesheet(); if ( empty( $stylesheet ) ) { diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 8a3649dbcbb90..740ea4a6d7b9c 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -367,8 +367,8 @@ public function test_get_stylesheet_support_for_shorthand_and_longhand_values() ); $styles = '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; }.wp-block-group{border-radius: 10px;margin: 1em;padding: 24px;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;padding-top: 15px;}'; - $this->assertEquals( $styles, $theme_json->get_stylesheet() ); - $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertSame( $styles, $theme_json->get_stylesheet() ); + $this->assertSame( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); } /** @@ -399,8 +399,8 @@ public function test_get_stylesheet_skips_disabled_protected_properties() { ); $expected = '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; }'; - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } /** @@ -424,8 +424,8 @@ public function test_get_stylesheet_renders_enabled_protected_properties() { ); $expected = 'body { margin: 0; }body{--wp--style--block-gap: 1em;}.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; }.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }'; - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } /** @@ -458,9 +458,6 @@ public function test_get_stylesheet() { ), ), ), - 'spacing' => array( - 'blockGap' => false, - ), 'misc' => 'value', 'blocks' => array( 'core/group' => array( @@ -553,13 +550,13 @@ public function test_get_stylesheet() { ); $variables = 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.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 = 'body { margin: 0; }body{color: var(--wp--preset--color--grey);}.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; }.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}'; + $styles = 'body { margin: 0; }body{color: var(--wp--preset--color--grey);}.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; }a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;padding: 24px;}.wp-block-group a:where(:not(.wp-element-button)){color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a:where(:not(.wp-element-button)),h2 a:where(:not(.wp-element-button)),h3 a:where(:not(.wp-element-button)),h4 a:where(:not(.wp-element-button)),h5 a:where(:not(.wp-element-button)),h6 a:where(:not(.wp-element-button)){background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a:where(:not(.wp-element-button)){background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}'; $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-small-font-family{font-family: var(--wp--preset--font-family--small) !important;}.has-big-font-family{font-family: var(--wp--preset--font-family--big) !important;}'; $all = $variables . $styles . $presets; - $this->assertEquals( $all, $theme_json->get_stylesheet() ); - $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); - $this->assertEquals( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) ); - $this->assertEquals( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) ); + $this->assertSame( $all, $theme_json->get_stylesheet() ); + $this->assertSame( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertSame( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) ); + $this->assertSame( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) ); } /** @@ -587,7 +584,7 @@ public function test_get_stylesheet_preset_classes_work_with_compounded_selector ) ); - $this->assertEquals( + $this->assertSame( 'h1.has-white-color,h2.has-white-color,h3.has-white-color,h4.has-white-color,h5.has-white-color,h6.has-white-color{color: var(--wp--preset--color--white) !important;}h1.has-white-background-color,h2.has-white-background-color,h3.has-white-background-color,h4.has-white-background-color,h5.has-white-background-color,h6.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}h1.has-white-border-color,h2.has-white-border-color,h3.has-white-border-color,h4.has-white-border-color,h5.has-white-border-color,h6.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}', $theme_json->get_stylesheet( array( 'presets' ) ) ); @@ -631,10 +628,10 @@ public function test_get_stylesheet_preset_rules_come_after_block_rules() { $presets = '.wp-block-group.has-grey-color{color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}'; $variables = '.wp-block-group{--wp--preset--color--grey: grey;}'; $all = $variables . $styles . $presets; - $this->assertEquals( $all, $theme_json->get_stylesheet() ); - $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); - $this->assertEquals( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) ); - $this->assertEquals( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) ); + $this->assertSame( $all, $theme_json->get_stylesheet() ); + $this->assertSame( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertSame( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) ); + $this->assertSame( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) ); } /** @@ -672,11 +669,11 @@ public function test_get_stylesheet_generates_proper_classes_and_css_vars_from_s ) ); - $this->assertEquals( + $this->assertSame( '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-dark-grey-color{color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-color{color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-color{color: var(--wp--preset--color--white-2-black) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-dark-grey-background-color{background-color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-background-color{background-color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-background-color{background-color: var(--wp--preset--color--white-2-black) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-dark-grey-border-color{border-color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-border-color{border-color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-border-color{border-color: var(--wp--preset--color--white-2-black) !important;}', $theme_json->get_stylesheet( array( 'presets' ) ) ); - $this->assertEquals( + $this->assertSame( 'body{--wp--preset--color--grey: grey;--wp--preset--color--dark-grey: grey;--wp--preset--color--light-grey: grey;--wp--preset--color--white-2-black: grey;--wp--custom--white-2-black: value;}', $theme_json->get_stylesheet( array( 'variables' ) ) ); @@ -719,12 +716,285 @@ public function test_get_stylesheet_preset_values_are_marked_as_important() { 'default' ); - $this->assertEquals( + $this->assertSame( 'body{--wp--preset--color--grey: grey;}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; }p{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.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;}', $theme_json->get_stylesheet() ); } + /** + * @ticket 56467 + */ + public function test_get_stylesheet_handles_whitelisted_element_pseudo_selectors() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a:where(:not(.wp-element-button)){background-color: red;color: green;}a:where(:not(.wp-element-button)):hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:where(:not(.wp-element-button)):focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * @ticket 56467 + */ + public function test_get_stylesheet_handles_only_pseudo_selector_rules_for_given_property() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a:where(:not(.wp-element-button)):hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:where(:not(.wp-element-button)):focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * @ticket 56467 + */ + public function test_get_stylesheet_ignores_pseudo_selectors_on_non_whitelisted_elements() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'h4' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'h4{background-color: red;color: green;}'; + + $expected = $base_styles . $element_styles; + + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * @ticket 56467 + */ + public function test_get_stylesheet_ignores_non_whitelisted_pseudo_selectors() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + ), + ':levitate' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a:where(:not(.wp-element-button)){background-color: red;color: green;}a:where(:not(.wp-element-button)):hover{background-color: green;color: red;}'; + + $expected = $base_styles . $element_styles; + + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertStringNotContainsString( 'a:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * @ticket 56467 + */ + public function test_get_stylesheet_handles_priority_of_elements_vs_block_elements_pseudo_selectors() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = '.wp-block-group a:where(:not(.wp-element-button)){background-color: red;color: green;}.wp-block-group a:where(:not(.wp-element-button)):hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}.wp-block-group a:where(:not(.wp-element-button)):focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * @ticket 56467 + */ + public function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_selectors() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + ), + ), + ), + 'blocks' => array( + 'core/group' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a:where(:not(.wp-element-button)){background-color: red;color: green;}a:where(:not(.wp-element-button)):hover{background-color: green;color: red;}.wp-block-group a:where(:not(.wp-element-button)):hover{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + /** * @ticket 52991 * @ticket 54336 @@ -2086,6 +2356,56 @@ public function test_remove_insecure_properties_applies_safe_styles() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + /** + * @ticket 56467 + */ + public function test_remove_invalid_element_pseudo_selectors() { + $actual = WP_Theme_JSON::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'hotpink', + 'background' => 'yellow', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'blue', + ), + ), + ), + ), + ), + ), + true + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'hotpink', + 'background' => 'yellow', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'blue', + ), + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + /** * @ticket 54336 */ @@ -2621,4 +2941,144 @@ function test_export_data_sets_appearance_tools() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + + /** + * @ticket 56467 + */ + public function test_get_element_class_name_button() { + $expected = 'wp-element-button'; + $actual = WP_Theme_JSON::get_element_class_name( 'button' ); + + $this->assertSame( $expected, $actual ); + } + + /** + * @ticket 56467 + */ + public function test_get_element_class_name_invalid() { + $expected = ''; + $actual = WP_Theme_JSON::get_element_class_name( 'unknown-element' ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Testing that dynamic properties in theme.json return the value they refrence, + * e.g. array( 'ref' => 'styles.color.background' ) => "#ffffff". + * + * @ticket 56467 + */ + public function test_get_property_value_valid() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => array( 'ref' => 'styles.color.text' ), + 'text' => array( 'ref' => 'styles.color.background' ), + ), + ), + ), + ), + ) + ); + + $expected = 'body { margin: 0; }body{background-color: #ffffff;color: #000000;}.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; }.wp-element-button, .wp-block-button__link{background-color: #000000;color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + } + + /** + * Testing that dynamic properties in theme.json that refer to other dynamic properties in a loop + * should be left untouched. + * + * @ticket 56467 + * @expectedIncorrectUsage get_property_value + */ + public function test_get_property_value_loop() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.elements.button.color.background' ), + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => array( 'ref' => 'styles.color.text' ), + 'text' => array( 'ref' => 'styles.color.background' ), + ), + ), + ), + ), + ) + ); + + $expected = 'body { margin: 0; }body{background-color: #ffffff;}.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; }.wp-element-button, .wp-block-button__link{color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + } + + /** + * Testing that dynamic properties in theme.json that refer to other dynamic properties + * should be left unprocessed. + * + * @ticket 56467 + * @expectedIncorrectUsage get_property_value + */ + public function test_get_property_value_recursion() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.color.background' ), + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => array( 'ref' => 'styles.color.text' ), + 'text' => array( 'ref' => 'styles.color.background' ), + ), + ), + ), + ), + ) + ); + + $expected = 'body { margin: 0; }body{background-color: #ffffff;color: #ffffff;}.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; }.wp-element-button, .wp-block-button__link{color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + } + + /** + * Testing that dynamic properties in theme.json that refer to themselves + * should be left unprocessed. + * + * @ticket 56467 + * @expectedIncorrectUsage get_property_value + */ + public function test_get_property_value_self() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => 2, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.color.text' ), + ), + ), + ) + ); + + $expected = 'body { margin: 0; }body{background-color: #ffffff;}.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; }'; + $this->assertSame( $expected, $theme_json->get_stylesheet() ); + } + } From 640cca7a7059b48f246d03f2b26b00fd5fe6d660 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 11 Sep 2022 13:11:33 +0000 Subject: [PATCH 03/66] Docs: Correct `@global` tags in `WP_User_Query::prepare_query()`. * `$blog_id` global was replaced with `get_current_blog_id()` and is no longer used directly. * `$wp_roles` global usage was previously undocumented. Follow-up to [32637], [38457], [51943]. Props shoaibkarimali. Fixes #56543. git-svn-id: https://develop.svn.wordpress.org/trunk@54119 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-user-query.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index 9b9087dcebde8..f894027e67a8c 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -140,8 +140,8 @@ public static function fill_query_vars( $args ) { * @since 5.3.0 Introduced the 'meta_type_key' parameter. * @since 5.9.0 Added 'capability', 'capability__in', and 'capability__not_in' parameters. * - * @global wpdb $wpdb WordPress database abstraction object. - * @global int $blog_id + * @global wpdb $wpdb WordPress database abstraction object. + * @global WP_Roles $wp_roles WordPress role management object. * * @param string|array $query { * Optional. Array or string of Query parameters. @@ -256,7 +256,7 @@ public static function fill_query_vars( $args ) { * } */ public function prepare_query( $query = array() ) { - global $wpdb; + global $wpdb, $wp_roles; if ( empty( $this->query_vars ) || ! empty( $query ) ) { $this->query_limit = null; @@ -448,8 +448,6 @@ public function prepare_query( $query = array() ) { $available_roles = array(); if ( ! empty( $qv['capability'] ) || ! empty( $qv['capability__in'] ) || ! empty( $qv['capability__not_in'] ) ) { - global $wp_roles; - $wp_roles->for_site( $blog_id ); $available_roles = $wp_roles->roles; } From 8f533d77d27fc60b4914755ef1c126677e9d38af Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Sun, 11 Sep 2022 18:16:27 +0000 Subject: [PATCH 04/66] Customize: Use Semantically correct function Functionally, `add_action` and `add_filter` are essentially the same, but semantically they are not. Props Drivingralle. Fixes #56285. git-svn-id: https://develop.svn.wordpress.org/trunk@54120 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-customize-nav-menus.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index ae0925fe02be3..341a7f572a30a 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -1322,7 +1322,7 @@ public function customize_preview_init() { add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) ); add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 ); add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 ); - add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 ); + add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1 ); add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) ); } From da03cf1c6b3ab71ea7e56f4a050d87c41150726e Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Sun, 11 Sep 2022 18:53:28 +0000 Subject: [PATCH 05/66] REST API: Use helper functions for building routes in more places. Props get_dave, spacedmonkey. Fixes #56472. git-svn-id: https://develop.svn.wordpress.org/trunk@54121 602fd350-edb4-49c9-b593-d223f7449a82 --- .../endpoints/class-wp-rest-posts-controller.php | 16 ++++++++-------- .../class-wp-rest-revisions-controller.php | 5 +++-- .../class-wp-rest-templates-controller.php | 6 ++---- .../endpoints/class-wp-rest-terms-controller.php | 12 +++++++----- 4 files changed, 20 insertions(+), 19 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 2c9bf40f191b0..2b543be296a0e 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 @@ -418,7 +418,8 @@ public function get_items( $request ) { $response->header( 'X-WP-TotalPages', (int) $max_pages ); $request_params = $request->get_query_params(); - $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + $collection_url = rest_url( rest_get_route_for_post_type_items( $this->post_type ) ); + $base = add_query_arg( urlencode_deep( $request_params ), $collection_url ); if ( $page > 1 ) { $prev_page = $page - 1; @@ -777,7 +778,7 @@ public function create_item( $request ) { $response = rest_ensure_response( $response ); $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); + $response->header( 'Location', rest_url( rest_get_route_for_post( $post ) ) ); return $response; } @@ -2030,15 +2031,13 @@ public function protected_title_format() { * @return array Links for the given post. */ protected function prepare_links( $post ) { - $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); - // Entity meta. $links = array( 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $post->ID ), + 'href' => rest_url( rest_get_route_for_post( $post->ID ) ), ), 'collection' => array( - 'href' => rest_url( $base ), + 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ), ), 'about' => array( 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), @@ -2066,15 +2065,16 @@ protected function prepare_links( $post ) { if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) { $revisions = wp_get_latest_revision_id_and_total_count( $post->ID ); $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; + $revisions_base = sprintf( '/%s/%s/%d/revisions', $this->namespace, $this->rest_base, $post->ID ); $links['version-history'] = array( - 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ), + 'href' => rest_url( $revisions_base ), 'count' => $revisions_count, ); if ( $revisions_count > 0 ) { $links['predecessor-version'] = array( - 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions/' . $revisions['latest_id'] ), + 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), 'id' => $revisions['latest_id'], ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php index bea3ddac789c6..23f9ada215104 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php @@ -334,7 +334,8 @@ public function get_items( $request ) { $response->header( 'X-WP-TotalPages', (int) $max_pages ); $request_params = $request->get_query_params(); - $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) ) ); + $base_path = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) ); + $base = add_query_arg( urlencode_deep( $request_params ), $base_path ); if ( $page > 1 ) { $prev_page = $page - 1; @@ -620,7 +621,7 @@ public function prepare_item_for_response( $item, $request ) { $response = rest_ensure_response( $data ); if ( ! empty( $data['parent'] ) ) { - $response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) ); + $response->add_link( 'parent', rest_url( rest_get_route_for_post( $data['parent'] ) ) ); } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 6d70372cbcac3..7e60674890646 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -687,14 +687,12 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V * @return array Links for the given post. */ protected function prepare_links( $id ) { - $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); - $links = array( 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $id ), + 'href' => rest_url( rest_get_route_for_post( $id ) ), ), 'collection' => array( - 'href' => rest_url( $base ), + 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ), ), 'about' => array( 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index 3a0baeac71e74..52ad18d1848ec 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -303,7 +303,10 @@ public function get_items( $request ) { $response->header( 'X-WP-TotalPages', (int) $max_pages ); - $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( $this->namespace . '/' . $this->rest_base ) ); + $request_params = $request->get_query_params(); + $collection_url = rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) ); + $base = add_query_arg( urlencode_deep( $request_params ), $collection_url ); + if ( $page > 1 ) { $prev_page = $page - 1; @@ -893,13 +896,12 @@ public function prepare_item_for_response( $item, $request ) { * @return array Links for the given term. */ protected function prepare_links( $term ) { - $base = $this->namespace . '/' . $this->rest_base; $links = array( 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), + 'href' => rest_url( rest_get_route_for_term( $term ) ), ), 'collection' => array( - 'href' => rest_url( $base ), + 'href' => rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) ), ), 'about' => array( 'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ), @@ -911,7 +913,7 @@ protected function prepare_links( $term ) { if ( $parent_term ) { $links['up'] = array( - 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), + 'href' => rest_url( rest_get_route_for_term( $parent_term ) ), 'embeddable' => true, ); } From 3fbc7e5aea966a75c6e41c6b65cb365c9748a0df Mon Sep 17 00:00:00 2001 From: Joe McGill Date: Sun, 11 Sep 2022 20:44:50 +0000 Subject: [PATCH 06/66] Editor: Refresh nones for metaboxes after reauthentication. This fixes an issue where metaboxes fail to save after a session expires and a user logs in again via the heartbeat API. Props LinSoftware. Fixes #52584. git-svn-id: https://develop.svn.wordpress.org/trunk@54122 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/admin-filters.php | 1 + src/wp-admin/includes/misc.php | 35 +++++++++++++++++ src/wp-admin/includes/post.php | 44 ++++++++++++++++++++++ tests/phpunit/tests/admin/includesPost.php | 38 +++++++++++++++++++ 4 files changed, 118 insertions(+) diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index e80c111dde33f..33354cb073e32 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -80,6 +80,7 @@ add_filter( 'heartbeat_received', 'heartbeat_autosave', 500, 2 ); add_filter( 'wp_refresh_nonces', 'wp_refresh_post_nonces', 10, 3 ); +add_filter( 'wp_refresh_nonces', 'wp_refresh_metabox_loader_nonces', 10, 2 ); add_filter( 'wp_refresh_nonces', 'wp_refresh_heartbeat_nonces' ); add_filter( 'heartbeat_settings', 'wp_heartbeat_set_suspension' ); diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index 94337fee7d3ff..65cf69ab31a04 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -1255,6 +1255,41 @@ function wp_refresh_post_nonces( $response, $data, $screen_id ) { return $response; } +/** + * Refresh nonces used with meta boxes in the block editor. + * + * @since 6.1.0 + * + * @param array $response The Heartbeat response. + * @param array $data The $_POST data sent. + * @return array The Heartbeat response. + */ +function wp_refresh_metabox_loader_nonces( $response, $data ) { + if ( empty( $data['wp-refresh-metabox-loader-nonces'] ) ) { + return $response; + } + + $received = $data['wp-refresh-metabox-loader-nonces']; + $post_id = (int) $received['post_id']; + + if ( ! $post_id ) { + return $response; + } + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return $response; + } + + $response['wp-refresh-metabox-loader-nonces'] = array( + 'replace' => array( + 'metabox_loader_nonce' => wp_create_nonce( 'meta-box-loader' ), + '_wpnonce' => wp_create_nonce( 'update-post_' . $post_id ), + ), + ); + + return $response; +} + /** * Adds the latest Heartbeat and REST-API nonce to the Heartbeat response. * diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 8eb0368220e83..1abe5423166b5 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -2334,6 +2334,50 @@ function the_block_editor_meta_boxes() { wp_add_inline_script( 'wp-lists', $script ); } + /* + * Refresh nonces used by the meta box loader. + * + * The logic is very similar to that provided by post.js for the classic editor. + */ + $script = "( function( $ ) { + var check, timeout; + + function schedule() { + check = false; + window.clearTimeout( timeout ); + timeout = window.setTimeout( function() { check = true; }, 300000 ); + } + + $( document ).on( 'heartbeat-send.wp-refresh-nonces', function( e, data ) { + var post_id, \$authCheck = $( '#wp-auth-check-wrap' ); + + if ( check || ( \$authCheck.length && ! \$authCheck.hasClass( 'hidden' ) ) ) { + if ( ( post_id = $( '#post_ID' ).val() ) && $( '#_wpnonce' ).val() ) { + data['wp-refresh-metabox-loader-nonces'] = { + post_id: post_id + }; + } + } + }).on( 'heartbeat-tick.wp-refresh-nonces', function( e, data ) { + var nonces = data['wp-refresh-metabox-loader-nonces']; + + if ( nonces ) { + if ( nonces.replace ) { + if ( nonces.replace.metabox_loader_nonce && window._wpMetaBoxUrl && wp.url ) { + window._wpMetaBoxUrl= wp.url.addQueryArgs( window._wpMetaBoxUrl, { 'meta-box-loader-nonce': nonces.replace.metabox_loader_nonce } ); + } + + if ( nonces.replace._wpnonce ) { + $( '#_wpnonce' ).val( nonces.replace._wpnonce ); + } + } + } + }).ready( function() { + schedule(); + }); + } )( jQuery );"; + wp_add_inline_script( 'heartbeat', $script ); + // Reset meta box data. $wp_meta_boxes = $_original_meta_boxes; } diff --git a/tests/phpunit/tests/admin/includesPost.php b/tests/phpunit/tests/admin/includesPost.php index eaf20d987ebfa..2a41131a923ff 100644 --- a/tests/phpunit/tests/admin/includesPost.php +++ b/tests/phpunit/tests/admin/includesPost.php @@ -1016,4 +1016,42 @@ public function test_post_exists_should_not_match_invalid_post_type_and_status_c $this->assertSame( 0, post_exists( $title, null, null, $post_type, 'draft' ) ); $this->assertSame( 0, post_exists( $title, null, null, 'wp_tests', $post_status ) ); } + + /** + * Test refreshed nonce for metabox loader. + * + * @return void + */ + public function test_user_get_refreshed_metabox_nonce() { + + // Create a post by the current user. + wp_set_current_user( self::$editor_id ); + + $post_data = array( + 'post_content' => 'Test post content', + 'post_title' => 'Test post title', + 'post_excerpt' => 'Test post excerpt', + 'post_author' => self::$editor_id, + 'post_status' => 'draft', + ); + $post_id = wp_insert_post( $post_data ); + + // Simulate the $_POST data from the heartbeat. + $data = array( + 'wp-refresh-metabox-loader-nonces' => array( + 'post_id' => (string) $post_id, + ), + 'wp-refresh-post-lock' => array( + 'lock' => '1658203298:1', + 'post_id' => (string) $post_id, + ), + ); + + // Call the function we're testing. + $response = wp_refresh_metabox_loader_nonces( array(), $data ); + + // Ensure that both nonces were created. + $this->assertNotEmpty( $response['wp-refresh-metabox-loader-nonces']['replace']['_wpnonce'] ); + $this->assertNotEmpty( $response['wp-refresh-metabox-loader-nonces']['replace']['metabox_loader_nonce'] ); + } } From a400e99225abe191fd02f42ed28704441d6c91ea Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Sun, 11 Sep 2022 21:10:31 +0000 Subject: [PATCH 07/66] REST API: Add support for searching resources by id. This brings support for the `include` and `exclude` collection parameters to the Search Controller. This can be used to find an item by id when it's subtype is unknown. Props kadamwhite. Fixes #56546. git-svn-id: https://develop.svn.wordpress.org/trunk@54123 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-search-controller.php | 18 +++++ .../class-wp-rest-post-search-handler.php | 8 ++ .../class-wp-rest-term-search-handler.php | 8 ++ .../tests/rest-api/rest-search-controller.php | 74 +++++++++++++++++++ tests/qunit/fixtures/wp-api-generated.js | 18 +++++ 5 files changed, 126 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php index 76021ff0b002c..93502d60f41fb 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php @@ -331,6 +331,24 @@ public function get_collection_params() { 'sanitize_callback' => array( $this, 'sanitize_subtypes' ), ); + $query_params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + + $query_params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + return $query_params; } diff --git a/src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php b/src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php index 5d20661b05cdc..5809245acf00e 100644 --- a/src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php +++ b/src/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php @@ -69,6 +69,14 @@ public function search_items( WP_REST_Request $request ) { $query_args['s'] = $request['search']; } + if ( ! empty( $request['exclude'] ) ) { + $query_args['post__not_in'] = $request['exclude']; + } + + if ( ! empty( $request['include'] ) ) { + $query_args['post__in'] = $request['include']; + } + /** * Filters the query arguments for a REST API search request. * diff --git a/src/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php b/src/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php index af9b2e5663ef7..eed35e7470618 100644 --- a/src/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php +++ b/src/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php @@ -70,6 +70,14 @@ public function search_items( WP_REST_Request $request ) { $query_args['search'] = $request['search']; } + if ( ! empty( $request['exclude'] ) ) { + $query_args['exclude'] = $request['exclude']; + } + + if ( ! empty( $request['include'] ) ) { + $query_args['include'] = $request['include']; + } + /** * Filters the query arguments for a REST API search request. * diff --git a/tests/phpunit/tests/rest-api/rest-search-controller.php b/tests/phpunit/tests/rest-api/rest-search-controller.php index 1e59b263966c1..2b2e87adebbeb 100644 --- a/tests/phpunit/tests/rest-api/rest-search-controller.php +++ b/tests/phpunit/tests/rest-api/rest-search-controller.php @@ -819,4 +819,78 @@ private function get_request( $params = array(), $method = 'GET' ) { return $request; } + /** + * @ticket 56546 + */ + public function test_get_items_search_posts_include_ids() { + $response = $this->do_request_with_params( + array( + 'include' => array_slice( self::$my_title_post_ids, 1, 2 ), + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array( self::$my_title_post_ids[1], self::$my_title_post_ids[2] ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @ticket 56546 + */ + public function test_get_items_search_posts_exclude_ids() { + $response = $this->do_request_with_params( + array( + 'exclude' => self::$my_title_page_ids, + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array_merge( + self::$my_title_post_ids, + self::$my_content_post_ids + ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @ticket 56546 + */ + public function test_get_items_search_terms_include_ids() { + $response = $this->do_request_with_params( + array( + 'include' => self::$my_tag_id, + 'type' => 'term', + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array( self::$my_tag_id ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @ticket 56546 + */ + public function test_get_items_search_terms_exclude_ids() { + $response = $this->do_request_with_params( + array( + // "1" is the default category. + 'exclude' => array( 1, self::$my_tag_id ), + 'type' => 'term', + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array( self::$my_category_id ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 00b6a2f8340de..4020b25da844d 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -9269,6 +9269,24 @@ mockedApiResponse.Schema = { "type": "string" }, "required": false + }, + "exclude": { + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "include": { + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } } From db0290b04264de1fa791b6973ce3122ccc160a90 Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Sun, 11 Sep 2022 21:52:10 +0000 Subject: [PATCH 08/66] Build/Test: Prevent using unsupported NPM versions. Using NPM 7+ currently causes a number of issues. In order to improve developer experience, we should prevent a developer from going down the road of using incompatible engines. See also: https://github.com/WordPress/gutenberg/pull/29204 and https://github.com/WordPress/gutenberg/pull/23600. Props rcorrales. Fixes #56547. git-svn-id: https://develop.svn.wordpress.org/trunk@54124 602fd350-edb4-49c9-b593-d223f7449a82 --- .npmrc | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 1dab4ed4c3020..aafab1669bf72 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ save-exact = true +engine-strict = true diff --git a/package.json b/package.json index 825a5714bfe1e..6429aea0b1c93 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "engines": { "node": ">=14.15.0", - "npm": ">=6.14.8" + "npm": ">=6.14.8 <7" }, "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 4b3476530d577b71f2187f85e0508e69f48beb56 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 11 Sep 2022 21:55:28 +0000 Subject: [PATCH 09/66] Customize: Prevent JS error in Links widget when selective refresh is enabled This prevents erroneously replacing the `data-customize-partial-id` when only the `id` attribute should be replaced. Props dlh, costdev, nikeo, greenshady. Fixes #39451. git-svn-id: https://develop.svn.wordpress.org/trunk@54125 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/widgets/class-wp-widget-links.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/widgets/class-wp-widget-links.php b/src/wp-includes/widgets/class-wp-widget-links.php index e90b558ade4ee..58ae2a7919b54 100644 --- a/src/wp-includes/widgets/class-wp-widget-links.php +++ b/src/wp-includes/widgets/class-wp-widget-links.php @@ -48,7 +48,7 @@ public function widget( $args, $instance ) { $order = 'rating' === $orderby ? 'DESC' : 'ASC'; $limit = isset( $instance['limit'] ) ? $instance['limit'] : -1; - $before_widget = preg_replace( '/id="[^"]*"/', 'id="%id"', $args['before_widget'] ); + $before_widget = preg_replace( '/ id="[^"]*"/', ' id="%id"', $args['before_widget'] ); $widget_links_args = array( 'title_before' => $args['before_title'], From 27aa1ec4c6e8b3552787b4f8e91f3e83a7def272 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 11 Sep 2022 21:56:42 +0000 Subject: [PATCH 10/66] Date/Time: Cast extracted strings to integers in `wp_resolve_post_date()`. `wp_resolve_post_date()` extracts year/month/day from a post date (which is a string) and passes it to `wp_checkdate` (and from there to `checkdate()`), which requires `int`s. Casting the strings to integers avoids PHP notices due to incorrect argument types. Props hilayt24. Fixes #54186 git-svn-id: https://develop.svn.wordpress.org/trunk@54126 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/post.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 6a0af16a8b771..ad271c2ab8737 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -4945,9 +4945,9 @@ function wp_resolve_post_date( $post_date = '', $post_date_gmt = '' ) { } // Validate the date. - $month = substr( $post_date, 5, 2 ); - $day = substr( $post_date, 8, 2 ); - $year = substr( $post_date, 0, 4 ); + $month = (int) substr( $post_date, 5, 2 ); + $day = (int) substr( $post_date, 8, 2 ); + $year = (int) substr( $post_date, 0, 4 ); $valid_date = wp_checkdate( $month, $day, $year, $post_date ); From bb20a18040c7dddbf224bfef741592b709d64de8 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Sun, 11 Sep 2022 22:00:16 +0000 Subject: [PATCH 11/66] REST API: Introduce _pretty query parameter to opt in to JSON_PRETTY_PRINT. Add support for a "_pretty" meta-parameter on all REST controllers which instructs WordPress to return pretty-printed JSON, for better readability when inspecting endpoint responses in curl output or certain developer tools. Introduce the "rest_json_encode_options" filter to permit site owners to control this behavior globally. Props Viper007Bond, TimothyBlynJacobs, chrisguitarguy, johnbillion, swissspidy, adamsilverstein, danielbachhuber, rmccue. Fixes #41998. git-svn-id: https://develop.svn.wordpress.org/trunk@54127 602fd350-edb4-49c9-b593-d223f7449a82 --- .../rest-api/class-wp-rest-server.php | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 4e26611bb459b..ede83bdb191f3 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -230,6 +230,33 @@ protected function json_error( $code, $message, $status = null ) { return wp_json_encode( $error ); } + /** + * Gets the encoding options passed to {@see wp_json_encode}. + * + * @since 6.1.0 + * + * @param \WP_REST_Request $request The current request object. + * + * @return int The JSON encode options. + */ + protected function get_json_encode_options( WP_REST_Request $request ) { + $options = 0; + + if ( $request->has_param( '_pretty' ) ) { + $options |= JSON_PRETTY_PRINT; + } + + /** + * Filters the JSON encoding options used to send the REST API response. + * + * @since 6.1.0 + * + * @param int $options JSON encoding options {@see json_encode()}. + * @param WP_REST_Request $request Current request object. + */ + return apply_filters( 'rest_json_encode_options', $options, $request ); + } + /** * Handles serving a REST API request. * @@ -493,7 +520,7 @@ public function serve_request( $path = null ) { return null; } - $result = wp_json_encode( $result ); + $result = wp_json_encode( $result, $this->get_json_encode_options( $request ) ); $json_error_message = $this->get_json_last_error(); @@ -506,7 +533,7 @@ public function serve_request( $path = null ) { ); $result = $this->error_to_response( $json_error_obj ); - $result = wp_json_encode( $result->data ); + $result = wp_json_encode( $result->data, $this->get_json_encode_options( $request ) ); } if ( $jsonp_callback ) { From 1c6951324501ef1dc888e8284e1b3ab4f6461b81 Mon Sep 17 00:00:00 2001 From: Anthony Burchell Date: Sun, 11 Sep 2022 22:17:04 +0000 Subject: [PATCH 12/66] Media: Add muted property for video elements. This change allows for the muted property to be used in video elements which solves for content that wishes to `autoPlay` when a page is viewed. Adding `muted` to video elements adhears to the requirements browsers have to honor `autoPlay` functionality. Props prokium, peterwilsoncc, costdev, johnbillion, Benouare. Fixes #54788. git-svn-id: https://develop.svn.wordpress.org/trunk@54128 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/media.php | 5 ++++- tests/phpunit/tests/media.php | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index caf8d247f5f11..61c93c95289f4 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -3219,6 +3219,7 @@ function wp_get_video_extensions() { * @type string $poster The 'poster' attribute for the `