Make WordPress Core

Changeset 62068


Ignore:
Timestamp:
03/19/2026 02:39:42 PM (6 months ago)
Author:
wildworks
Message:

Blocks: Fix wrapper attribute merging in get_block_wrapper_attributes().

Replace the previous generic concatenation logic in get_block_wrapper_attributes() with attribute-specific merge behavior:

  • Add explicit merge callbacks for each attribute.
  • Merge style values safely, normalize trailing semicolons, and sanitize the result.
  • Merge class values with deduplication .
  • Treat id and aria-label as override attributes, giving precedence to explicitly passed extra attributes.

Props adrock42, mamaduka, r1k0, westonruter, wildworks.

Fixes #64603.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block-supports.php

    r59925 r62068  
    180180        }
    181181
    182         // This is hardcoded on purpose.
    183         // We only support a fixed list of attributes.
    184         $attributes_to_merge = array( 'style', 'class', 'id', 'aria-label' );
    185         $attributes          = array();
    186         foreach ( $attributes_to_merge as $attribute_name ) {
    187                 if ( empty( $new_attributes[ $attribute_name ] ) && empty( $extra_attributes[ $attribute_name ] ) ) {
     182        // Attribute values are concatenated or overridden depending on the attribute type.
     183        // This is hardcoded on purpose, as we only support a fixed list of attributes.
     184        $attribute_merge_callbacks = array(
     185                'style'      => static function ( $new_attribute, $extra_attribute ) {
     186                        $styles = array_filter(
     187                                array(
     188                                        rtrim( trim( $new_attribute ), ';' ),
     189                                        rtrim( trim( $extra_attribute ), ';' ),
     190                                )
     191                        );
     192                        return safecss_filter_attr( implode( ';', array_filter( $styles ) ) );
     193                },
     194                'class'      => static function ( $new_attribute, $extra_attribute ) {
     195                        $classes = array_merge(
     196                                (array) preg_split( '/\s+/', $extra_attribute, -1, PREG_SPLIT_NO_EMPTY ),
     197                                (array) preg_split( '/\s+/', $new_attribute, -1, PREG_SPLIT_NO_EMPTY )
     198                        );
     199                        $classes = array_unique( array_filter( $classes ) );
     200                        return implode( ' ', $classes );
     201                },
     202                'id'         => static function ( $new_attribute, $extra_attribute ) {
     203                        return '' !== $extra_attribute ? $extra_attribute : $new_attribute;
     204                },
     205                'aria-label' => static function ( $new_attribute, $extra_attribute ) {
     206                        return '' !== $extra_attribute ? $extra_attribute : $new_attribute;
     207                },
     208        );
     209
     210        $attributes = array();
     211        foreach ( $attribute_merge_callbacks as $attribute_name => $merge_callback ) {
     212                $new_attribute   = $new_attributes[ $attribute_name ] ?? '';
     213                $extra_attribute = $extra_attributes[ $attribute_name ] ?? '';
     214                $new_attribute   = is_string( $new_attribute ) ? $new_attribute : '';
     215                $extra_attribute = is_string( $extra_attribute ) ? $extra_attribute : '';
     216
     217                if ( '' === $new_attribute && '' === $extra_attribute ) {
    188218                        continue;
    189219                }
    190220
    191                 if ( empty( $new_attributes[ $attribute_name ] ) ) {
    192                         $attributes[ $attribute_name ] = $extra_attributes[ $attribute_name ];
    193                         continue;
    194                 }
    195 
    196                 if ( empty( $extra_attributes[ $attribute_name ] ) ) {
    197                         $attributes[ $attribute_name ] = $new_attributes[ $attribute_name ];
    198                         continue;
    199                 }
    200 
    201                 $attributes[ $attribute_name ] = $extra_attributes[ $attribute_name ] . ' ' . $new_attributes[ $attribute_name ];
     221                $attributes[ $attribute_name ] = $merge_callback( $new_attribute, $extra_attribute );
    202222        }
    203223
    204224        foreach ( $extra_attributes as $attribute_name => $value ) {
    205                 if ( ! in_array( $attribute_name, $attributes_to_merge, true ) ) {
     225                if ( ! isset( $attribute_merge_callbacks[ $attribute_name ] ) ) {
    206226                        $attributes[ $attribute_name ] = $value;
    207227                }
  • trunk/tests/phpunit/tests/blocks/supportedStyles.php

    r59925 r62068  
    100100                        array(
    101101                                'class' => 'foo-bar-class',
    102                                 'style' => 'test: style;',
     102                                'style' => 'margin-top: 2px;',
    103103                        )
    104104                );
    105105                return '<div ' . $wrapper_attributes . '>' . self::BLOCK_CONTENT . '</div>';
    106         }
    107 
    108         /**
    109          * Runs assertions that the rendered output has expected class/style attrs.
    110          *
    111          * @param array  $block            Block to render.
    112          * @param string $expected_classes Expected output class attr string.
    113          * @param string $expected_styles  Expected output styles attr string.
    114          */
    115         private function assert_styles_and_classes_match( $block, $expected_classes, $expected_styles ) {
    116                 $styled_block = $this->render_example_block( $block );
    117                 $class_list   = $this->get_attribute_from_block( 'class', $styled_block );
    118                 $style_list   = $this->get_attribute_from_block( 'style', $styled_block );
    119 
    120                 $this->assertSame( $expected_classes, $class_list, 'Class list does not match expected classes' );
    121                 $this->assertSame( $expected_styles, $style_list, 'Style list does not match expected styles' );
    122106        }
    123107
     
    197181
    198182                $expected_classes = 'foo-bar-class wp-block-example has-text-color has-red-color has-background has-black-background-color';
    199                 $expected_styles  = 'test: style;';
     183                $expected_styles  = 'margin-top: 2px';
    200184
    201185                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    233217                );
    234218
    235                 $expected_styles  = 'test: style;color:#000;background-color:#fff;';
     219                $expected_styles  = 'color:#000;background-color:#fff;margin-top: 2px';
    236220                $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background';
    237221
     
    265249
    266250                $expected_classes = 'foo-bar-class wp-block-example has-background has-red-gradient-background';
    267                 $expected_styles  = 'test: style;';
     251                $expected_styles  = 'margin-top: 2px';
    268252
    269253                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    296280
    297281                $expected_classes = 'foo-bar-class wp-block-example has-background';
    298                 $expected_styles  = 'test: style; background:some-gradient-style;';
     282                $expected_styles  = 'background:some-gradient-style;margin-top: 2px';
    299283
    300284                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    332316
    333317                $expected_classes = 'foo-bar-class wp-block-example';
    334                 $expected_styles  = 'test: style;';
     318                $expected_styles  = 'margin-top: 2px';
    335319
    336320                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    362346
    363347                $expected_classes = 'foo-bar-class wp-block-example has-large-font-size';
    364                 $expected_styles  = 'test: style;';
     348                $expected_styles  = 'margin-top: 2px';
    365349
    366350                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    392376
    393377                $expected_classes = 'foo-bar-class wp-block-example';
    394                 $expected_styles  = 'test: style; font-size:10px;';
     378                $expected_styles  = 'font-size:10px;margin-top: 2px';
    395379
    396380                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    419403
    420404                $expected_classes = 'foo-bar-class wp-block-example';
    421                 $expected_styles  = 'test: style;';
     405                $expected_styles  = 'margin-top: 2px';
    422406
    423407                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    449433
    450434                $expected_classes = 'foo-bar-class wp-block-example';
    451                 $expected_styles  = 'test: style; line-height:10;';
     435                $expected_styles  = 'line-height:10;margin-top: 2px';
    452436
    453437                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    475459
    476460                $expected_classes = 'foo-bar-class wp-block-example';
    477                 $expected_styles  = 'test: style;';
     461                $expected_styles  = 'margin-top: 2px';
    478462
    479463                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    503487
    504488                $expected_classes = 'foo-bar-class wp-block-example alignwide';
    505                 $expected_styles  = 'test: style;';
     489                $expected_styles  = 'margin-top: 2px';
    506490
    507491                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    529513
    530514                $expected_classes = 'foo-bar-class wp-block-example';
    531                 $expected_styles  = 'test: style;';
     515                $expected_styles  = 'margin-top: 2px';
    532516
    533517                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    576560
    577561                $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background alignwide';
    578                 $expected_styles  = 'test: style; color:#000; background-color:#fff; font-size:10px; line-height:20;';
     562                $expected_styles  = 'color:#000;background-color:#fff;font-size:10px;line-height:20;margin-top: 2px';
    579563
    580564                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    619603
    620604                $expected_classes = 'foo-bar-class wp-block-example';
    621                 $expected_styles  = 'test: style; font-size:10px;';
     605                $expected_styles  = 'font-size:10px;margin-top: 2px';
    622606
    623607                $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    644628                );
    645629
    646                 $expected_styles  = 'test: style;';
     630                $expected_styles  = 'margin-top: 2px';
    647631                $expected_classes = 'foo-bar-class wp-block-example my-custom-classname';
    648632
     
    672656                );
    673657
    674                 $expected_styles  = 'test: style;';
     658                $expected_styles  = 'margin-top: 2px';
    675659                $expected_classes = 'foo-bar-class wp-block-example';
    676660
     
    698682                );
    699683
    700                 $expected_styles  = 'test: style;';
     684                $expected_styles  = 'margin-top: 2px';
    701685                $expected_classes = 'foo-bar-class';
    702686
     
    765749                $this->assertEmpty( $errors, 'Libxml errors should be dropped.' );
    766750        }
     751
     752        /**
     753         * Ensures that style, class, id, and aria-label attributes are correctly merged or overridden
     754         * in get_block_wrapper_attributes().
     755         *
     756         * @ticket 64603
     757         * @covers ::get_block_wrapper_attributes
     758         *
     759         * @dataProvider data_get_block_wrapper_attributes_merge_or_override
     760         *
     761         * @param array<string, mixed>  $block_type_settings
     762         * @param array<string, mixed>  $block_attrs
     763         * @param array<string, string> $extra_attributes
     764         * @param string                $expected_attribute
     765         */
     766        public function test_get_block_wrapper_attributes_merge_and_override( array $block_type_settings, array $block_attrs, array $extra_attributes, string $expected_attribute ): void {
     767                $block_name          = 'core/example';
     768                $block_type_settings = array_merge(
     769                        array(
     770                                'attributes'      => array(),
     771                                'render_callback' => true,
     772                        ),
     773                        $block_type_settings
     774                );
     775                $this->register_block_type( $block_name, $block_type_settings );
     776
     777                $block = array(
     778                        'blockName'    => $block_name,
     779                        'attrs'        => $block_attrs,
     780                        'innerBlock'   => array(),
     781                        'innerContent' => array(),
     782                        'innerHTML'    => array(),
     783                );
     784
     785                WP_Block_Supports::init();
     786                WP_Block_Supports::$block_to_render = $block;
     787
     788                $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes );
     789
     790                $this->assertSame( $expected_attribute, $wrapper_attributes );
     791        }
     792
     793        /**
     794         * Data provider for test_get_block_wrapper_attributes_merge_and_override.
     795         *
     796         * @return array<string, array{
     797         *      block_type_settings: array<string, mixed>,
     798         *      block_attrs: array<string, mixed>,
     799         *      extra_attributes: array<string, string>,
     800         *      expected_attribute: string
     801         *  }> Array of test cases.
     802         */
     803        public function data_get_block_wrapper_attributes_merge_or_override(): array {
     804                return array(
     805                        'extra style attributes are merged with block values' => array(
     806                                'block_type_settings' => array(
     807                                        'supports' => array(
     808                                                'color' => true,
     809                                        ),
     810                                ),
     811                                'block_attrs'         => array(
     812                                        'style' => array(
     813                                                'color' => array(
     814                                                        'text' => '#000',
     815                                                ),
     816                                        ),
     817                                ),
     818                                'extra_attributes'    => array(
     819                                        // Redundant trailing semicolons should be stripped
     820                                        'style' => 'margin-top: 2px;;;',
     821                                ),
     822                                'expected_attribute'  => 'style="color:#000;margin-top: 2px" class="wp-block-example has-text-color"',
     823                        ),
     824                        'extra class attributes are merged with block values' => array(
     825                                'block_type_settings' => array(
     826                                        'supports' => array(
     827                                                'color' => true,
     828                                        ),
     829                                ),
     830                                'block_attrs'         => array(
     831                                        'style' => array(
     832                                                'color' => array(
     833                                                        'text' => '#000',
     834                                                ),
     835                                        ),
     836                                ),
     837                                'extra_attributes'    => array(
     838                                        // Duplicate class names should be merged, and commas should be preserved.
     839                                        'class' => 'extra-class extra,class has-text-color',
     840                                ),
     841                                'expected_attribute'  => 'style="color:#000" class="extra-class extra,class has-text-color wp-block-example"',
     842                        ),
     843                        'extra attributes override block-generated id' => array(
     844                                'block_type_settings' => array(
     845                                        'supports' => array(
     846                                                'anchor' => true,
     847                                        ),
     848                                ),
     849                                'block_attrs'         => array(
     850                                        'anchor' => 'block-id',
     851                                ),
     852                                'extra_attributes'    => array(
     853                                        'id' => 'user-id',
     854                                ),
     855                                'expected_attribute'  => 'class="wp-block-example" id="user-id"',
     856                        ),
     857                        'block-generated id is used when no extra provided' => array(
     858                                'block_type_settings' => array(
     859                                        'supports' => array(
     860                                                'anchor' => true,
     861                                        ),
     862                                ),
     863                                'block_attrs'         => array(
     864                                        'anchor' => 'block-id',
     865                                ),
     866                                'extra_attributes'    => array(),
     867                                'expected_attribute'  => 'class="wp-block-example" id="block-id"',
     868                        ),
     869                        'extra attributes override block-generated aria-label' => array(
     870                                'block_type_settings' => array(
     871                                        'supports' => array(
     872                                                'ariaLabel' => true,
     873                                        ),
     874                                ),
     875                                'block_attrs'         => array(
     876                                        'ariaLabel' => 'Block aria-label',
     877                                ),
     878                                'extra_attributes'    => array(
     879                                        'aria-label' => 'User aria-label',
     880                                ),
     881                                'expected_attribute'  => 'class="wp-block-example" aria-label="User aria-label"',
     882                        ),
     883                        'block-generated aria-label is used when no extra provided' => array(
     884                                'block_type_settings' => array(
     885                                        'supports' => array(
     886                                                'ariaLabel' => true,
     887                                        ),
     888                                ),
     889                                'block_attrs'         => array(
     890                                        'ariaLabel' => 'Block aria-label',
     891                                ),
     892                                'extra_attributes'    => array(),
     893                                'expected_attribute'  => 'class="wp-block-example" aria-label="Block aria-label"',
     894                        ),
     895                );
     896        }
    767897}
Note: See TracChangeset for help on using the changeset viewer.