Making WordPress.org

Changeset 11410


Ignore:
Timestamp:
01/04/2022 11:40:07 PM (3 weeks ago)
Author:
ryelle
Message:

Plugin Directory: Update block.json validation and discovery in block plugins.

Start using https://schemas.wp.org/trunk/block.json to validate the block.json. This fixes a few mismatched field types between our validator and the official expected format. This also updates the block discovery code to merge found blocks (if a block is registered in both PHP and JS, for example).

Props gziolo, welcher, jeffpaul.
See #5303, #5971.

Location:
sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/block-json/class-validator.php

    r11096 r11410  
    2828     * The schema for the block.json file.
    2929     *
    30      * This attempts to follow the schema for the schema.
    31      * See https://json-schema.org/understanding-json-schema/reference/index.html
     30     * Fetch the schema from `schemas.wp.org`, which redirects to the latest version
     31     * of this file in GitHub.
     32     * See https://github.com/WordPress/gutenberg/blob/trunk/schemas/json/block.json.
    3233     *
    3334     * @return array
    3435     */
    3536    public static function schema() {
    36         return array(
    37             'type'                 => 'object',
    38             'properties'           => array(
    39                 'apiVersion'   => array(
    40                     'type' => 'string',
    41                 ),
    42                 'attributes'   => array(
    43                     'type'                 => 'object',
    44                     'additionalProperties' => array(
    45                         'type'       => 'object',
    46                         'properties' => array(
    47                             'attribute' => array(
    48                                 'type' => 'string',
    49                             ),
    50                             'meta'      => array(
    51                                 'type' => 'string',
    52                             ),
    53                             'multiline' => array(
    54                                 'type' => 'string',
    55                             ),
    56                             'query'     => array(
    57                                 'type' => 'object',
    58                             ),
    59                             'selector'  => array(
    60                                 'type' => 'string',
    61                             ),
    62                             'source'    => array(
    63                                 'type' => 'string',
    64                                 'enum' => array( 'attribute', 'text', 'html', 'query' ),
    65                             ),
    66                             'type'      => array(
    67                                 'type' => 'string',
    68                                 'enum' => array( 'null', 'boolean', 'object', 'array', 'number', 'string', 'integer' ),
    69                             ),
    70                         ),
    71                         'required'   => array( 'type' ),
    72                     ),
    73                 ),
    74                 'category'     => array(
    75                     'type' => 'string',
    76                 ),
    77                 'comment'      => array(
    78                     'type' => 'string',
    79                 ),
    80                 'description'  => array(
    81                     'type' => 'string',
    82                 ),
    83                 'editorScript' => array(
    84                     'type'    => 'string',
    85                     'pattern' => '\.js$',
    86                 ),
    87                 'editorStyle'  => array(
    88                     'type'    => 'string',
    89                     'pattern' => '\.css$',
    90                 ),
    91                 'example'      => array(
    92                     'type'                 => 'object',
    93                     'additionalProperties' => array(
    94                         'type' => 'object',
    95                     ),
    96                 ),
    97                 'icon'         => array(
    98                     'type' => 'string',
    99                 ),
    100                 'keywords'     => array(
    101                     'type'  => 'array',
    102                     'items' => array(
    103                         'type' => 'string',
    104                     ),
    105                 ),
    106                 'name'         => array(
    107                     'type' => 'string',
    108                 ),
    109                 'parent'       => array(
    110                     'type'  => 'array',
    111                     'items' => array(
    112                         'type' => 'string',
    113                     ),
    114                 ),
    115                 'script'       => array(
    116                     'type'    => 'string',
    117                     'pattern' => '\.js$',
    118                 ),
    119                 'style'        => array(
    120                     'type'    => 'string',
    121                     'pattern' => '\.css$',
    122                 ),
    123                 'styles'       => array(
    124                     'type'  => 'array',
    125                     'items' => array(
    126                         'type'       => 'object',
    127                         'properties' => array(
    128                             'isDefault' => array(
    129                                 'type' => 'boolean',
    130                             ),
    131                             'label'     => array(
    132                                 'type' => 'string',
    133                             ),
    134                             'name'      => array(
    135                                 'type' => 'string',
    136                             ),
    137                         ),
    138                     ),
    139                 ),
    140                 'supports'     => array(
    141                     'type'       => 'object',
    142                     'properties' => array(
    143                         'align'           => array(
    144                             'type'  => array( 'boolean', 'array' ),
    145                             'items' => array(
    146                                 'type' => 'string',
    147                                 'enum' => array( 'left', 'center', 'right', 'wide', 'full' ),
    148                             ),
    149                         ),
    150                         'alignWide'       => array(
    151                             'type' => 'boolean',
    152                         ),
    153                         'anchor'          => array(
    154                             'type' => 'boolean',
    155                         ),
    156                         'className'       => array(
    157                             'type' => 'boolean',
    158                         ),
    159                         'customClassName' => array(
    160                             'type' => 'boolean',
    161                         ),
    162                         'html'            => array(
    163                             'type' => 'boolean',
    164                         ),
    165                         'inserter'        => array(
    166                             'type' => 'boolean',
    167                         ),
    168                         'multiple'        => array(
    169                             'type' => 'boolean',
    170                         ),
    171                         'reusable'        => array(
    172                             'type' => 'boolean',
    173                         ),
    174                     ),
    175                 ),
    176                 'textdomain'   => array(
    177                     'type' => 'string',
    178                 ),
    179                 'title'        => array(
    180                     'type' => 'string',
    181                 ),
    182             ),
    183             'required'             => array( 'name', 'title' ),
    184             'additionalProperties' => false,
    185         );
     37        $schema_url = 'https://schemas.wp.org/trunk/block.json';
     38        $response = wp_remote_get( $schema_url );
     39        if ( is_wp_error( $response ) ) {
     40            return $response;
     41        }
     42
     43        $body = wp_remote_retrieve_body( $response );
     44        $schema = json_decode( $body, true );
     45        return $schema;
    18646    }
    18747
     
    20060
    20161        $schema = self::schema();
     62        if ( is_wp_error( $schema ) ) {
     63            return $schema;
     64        }
    20265
    203         $this->validate_object( $block_json, 'block.json', $schema );
     66        $result = rest_validate_value_from_schema( $block_json, $schema, 'block.json' );
     67
     68        // Workaround for a bug in validation, `oneOf` incorrectly flags that the value matches multiple options.
     69        // Getting this message means it passed the "string" condition, so this is not an error.
     70        // See https://core.trac.wordpress.org/ticket/54740.
     71        if ( is_wp_error( $result ) && 'rest_one_of_multiple_matches' !== $result->get_error_code() ) {
     72            $this->messages = $result;
     73        }
     74
    20475        $this->check_conditional_properties( $block_json );
    20576
     
    22394        }
    22495
     96        // phpcs:ignore WordPress.NamingConventions.ValidVariableName
    22597        if ( ! isset( $block_json->script ) && ! isset( $block_json->editorScript ) ) {
    22698            $this->messages->add(
    22799                'error',
    228100                sprintf(
    229                     __( 'At least one of the following properties must be present: %s', 'wporg-plugins' ),
     101                    __( 'block.json[script] At least one of the following properties must be present: %s', 'wporg-plugins' ),
    230102                    // translators: used between list items, there is a space after the comma.
    231103                    '<code>script</code>' . __( ', ', 'wporg-plugins' ) . '<code>editorScript</code>'
    232104                )
    233105            );
    234             $this->append_error_data( 'block.json:script', 'error' );
    235             $this->append_error_data( 'block.json:editorScript', 'error' );
    236106        }
    237     }
    238 
    239     /**
    240      * Validate an object and its properties.
    241      *
    242      * @param object $object The value to validate as an object.
    243      * @param string $prop   The name of the property, used in error reporting.
    244      * @param array  $schema The schema for the property, used for validation.
    245      *
    246      * @return bool
    247      */
    248     protected function validate_object( $object, $prop, $schema ) {
    249         if ( ! is_object( $object ) ) {
    250             $this->messages->add(
    251                 'error',
    252                 sprintf(
    253                     __( 'The %s property must contain an object value.', 'wporg-plugins' ),
    254                     '<code>' . $prop . '</code>'
    255                 )
    256             );
    257             $this->append_error_data( $prop, 'error' );
    258 
    259             return false;
    260         }
    261 
    262         $results = array();
    263 
    264         if ( isset( $schema['required'] ) ) {
    265             foreach ( $schema['required'] as $required_prop ) {
    266                 if ( ! property_exists( $object, $required_prop ) ) {
    267                     $this->messages->add(
    268                         'error',
    269                         sprintf(
    270                             __( 'The %1$s property is required in the %2$s object.', 'wporg-plugins' ),
    271                             '<code>' . $required_prop . '</code>',
    272                             '<code>' . $prop . '</code>'
    273                         )
    274                     );
    275                     $this->append_error_data( "$prop:$required_prop", 'error' );
    276                     $results[] = false;
    277                 }
    278             }
    279         }
    280 
    281         if ( isset( $schema['properties'] ) ) {
    282             foreach ( $schema['properties'] as $subprop => $subschema ) {
    283                 if ( ! isset( $object->$subprop ) ) {
    284                     continue;
    285                 }
    286 
    287                 if ( isset( $subschema['type'] ) ) {
    288                     $results[] = $this->route_validation_for_type(
    289                         $subschema['type'],
    290                         $object->$subprop,
    291                         "$prop:$subprop",
    292                         $subschema
    293                     );
    294                 }
    295             }
    296         }
    297 
    298         if ( isset( $schema['additionalProperties'] ) ) {
    299             if ( false === $schema['additionalProperties'] ) {
    300                 foreach ( array_keys( get_object_vars( $object ) ) as $key ) {
    301                     if ( ! isset( $schema['properties'][ $key ] ) ) {
    302                         $this->messages->add(
    303                             'warning',
    304                             sprintf(
    305                                 __( '%1$s is not a valid property in the %2$s object.', 'wporg-plugins' ),
    306                                 '<code>' . $key . '</code>',
    307                                 '<code>' . $prop . '</code>'
    308                             )
    309                         );
    310                         $this->append_error_data( "$prop:$key", 'warning' );
    311                         $results[] = false;
    312                         continue;
    313                     }
    314                 }
    315             } elseif ( isset( $schema['additionalProperties']['type'] ) ) {
    316                 foreach ( $object as $subprop => $subvalue ) {
    317                     $results[] = $this->route_validation_for_type(
    318                         $schema['additionalProperties']['type'],
    319                         $subvalue,
    320                         "$prop:$subprop",
    321                         $schema['additionalProperties']
    322                     );
    323                 }
    324             }
    325         }
    326 
    327         return ! in_array( false, $results, true );
    328     }
    329 
    330     /**
    331      * Validate an array and its items.
    332      *
    333      * @param array  $array  The value to validate as an array.
    334      * @param string $prop   The name of the property, used in error reporting.
    335      * @param array  $schema The schema for the property, used for validation.
    336      *
    337      * @return bool
    338      */
    339     protected function validate_array( $array, $prop, $schema ) {
    340         if ( ! is_array( $array ) ) {
    341             $this->messages->add(
    342                 'error',
    343                 sprintf(
    344                     __( 'The %s property must contain an array value.', 'wporg-plugins' ),
    345                     '<code>' . $prop . '</code>'
    346                 )
    347             );
    348             $this->append_error_data( $prop, 'error' );
    349 
    350             return false;
    351         }
    352 
    353         if ( isset( $schema['items']['type'] ) ) {
    354             $results = array();
    355             $index   = 0;
    356 
    357             foreach ( $array as $item ) {
    358                 $results[] = $this->route_validation_for_type(
    359                     $schema['items']['type'],
    360                     $item,
    361                     $prop . "[$index]",
    362                     $schema['items']
    363                 );
    364                 $index ++;
    365             }
    366 
    367             return ! in_array( false, $results, true );
    368         }
    369 
    370         return true;
    371     }
    372 
    373     /**
    374      * Validate a string.
    375      *
    376      * @param string $string The value to validate as a string.
    377      * @param string $prop   The name of the property, used in error reporting.
    378      * @param array  $schema The schema for the property, used for validation.
    379      *
    380      * @return bool
    381      */
    382     protected function validate_string( $string, $prop, $schema ) {
    383         if ( ! is_string( $string ) ) {
    384             $this->messages->add(
    385                 'error',
    386                 sprintf(
    387                     __( 'The %s property must contain a string value.', 'wporg-plugins' ),
    388                     '<code>' . $prop . '</code>'
    389                 )
    390             );
    391             $this->append_error_data( $prop, 'error' );
    392 
    393             return false;
    394         }
    395 
    396         if ( isset( $schema['enum'] ) ) {
    397             if ( ! in_array( $string, $schema['enum'], true ) ) {
    398                 $this->messages->add(
    399                     'warning',
    400                     sprintf(
    401                         __( '"%1$s" is not a valid value for the %2$s property.', 'wporg-plugins' ),
    402                         esc_html( $string ),
    403                         '<code>' . $prop . '</code>'
    404                     )
    405                 );
    406                 $this->append_error_data( $prop, 'warning' );
    407             }
    408         }
    409 
    410         if ( isset( $schema['pattern'] ) ) {
    411             if ( ! preg_match( '#' . $schema['pattern'] . '#', $string ) ) {
    412                 $pattern_description = $this->get_human_readable_pattern_description( $schema['pattern'] );
    413                 if ( $pattern_description ) {
    414                     $message = sprintf(
    415                         $pattern_description,
    416                         '<code>' . $prop . '</code>'
    417                     );
    418                 } else {
    419                     $message = sprintf(
    420                         __( 'The value of %s does not match the required pattern.', 'wporg-plugins' ),
    421                         '<code>' . $prop . '</code>'
    422                     );
    423                 }
    424 
    425                 $this->messages->add( 'warning', $message );
    426                 $this->append_error_data( $prop, 'warning' );
    427             }
    428         }
    429 
    430         return true;
    431     }
    432 
    433     /**
    434      * Validate a boolean.
    435      *
    436      * @param bool   $boolean The value to validate as a boolean.
    437      * @param string $prop    The name of the property, used in error reporting.
    438      *
    439      * @return bool
    440      */
    441     protected function validate_boolean( $boolean, $prop ) {
    442         if ( ! is_bool( $boolean ) ) {
    443             $this->messages->add(
    444                 'error',
    445                 sprintf(
    446                     __( 'The %s property must contain a boolean value.', 'wporg-plugins' ),
    447                     '<code>' . $prop . '</code>'
    448                 )
    449             );
    450             $this->append_error_data( $prop, 'error' );
    451 
    452             return false;
    453         }
    454 
    455         return true;
    456     }
    457 
    458     /**
    459      * Send a property value to the correct validator depending on which type(s) it can be.
    460      *
    461      * @param string|array $valid_types
    462      * @param mixed        $value
    463      * @param string       $prop
    464      * @param array        $schema
    465      *
    466      * @return bool
    467      */
    468     protected function route_validation_for_type( $valid_types, $value, $prop, $schema ) {
    469         // There is a single valid type.
    470         if ( is_string( $valid_types ) ) {
    471             $method = "validate_$valid_types";
    472             return $this->$method( $value, $prop, $schema );
    473         }
    474 
    475         // There are multiple valid types in an array.
    476         foreach ( $valid_types as $type ) {
    477             switch ( $type ) {
    478                 case 'boolean':
    479                     $check = 'is_bool';
    480                     break;
    481                 default:
    482                     $check = "is_$type";
    483                     break;
    484             }
    485 
    486             if ( $check( $value ) ) {
    487                 $method = "validate_$type";
    488                 return $this->$method( $value, $prop, $schema );
    489             }
    490         }
    491 
    492         // Made it this far, it's none of the valid types.
    493         $this->messages->add(
    494             'error',
    495             sprintf(
    496                 __( 'The %1$s property must contain a value that is one of these types: %2$s', 'wporg-plugins' ),
    497                 '<code>' . $prop . '</code>',
    498                 // translators: used between list items, there is a space after the comma.
    499                 '<code>' . implode( '</code>' . __( ', ', 'wporg-plugins' ) . '<code>', $valid_types ) . '</code>'
    500             )
    501         );
    502         $this->append_error_data( $prop, 'error' );
    503 
    504         return false;
    505107    }
    506108
     
    520122        $this->messages->add_data( $data, $error_code );
    521123    }
    522 
    523     /**
    524      * Get a description of a regex pattern that can be understood by humans.
    525      *
    526      * @param string $pattern A regex pattern.
    527      *
    528      * @return string
    529      */
    530     protected function get_human_readable_pattern_description( $pattern ) {
    531         $description = '';
    532 
    533         switch ( $pattern ) {
    534             case '\.css$':
    535                 $description = __( 'The value of %s must end in ".css".', 'wporg-plugins' );
    536                 break;
    537             case '\.js$':
    538                 $description = __( 'The value of %s must end in ".js".', 'wporg-plugins' );
    539                 break;
    540         }
    541 
    542         return $description;
    543     }
    544124}
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php

    r11291 r11410  
    626626                    $potential_block_directories[] = dirname( $relative_filename );
    627627                    foreach ( $blocks_in_file as $block ) {
    628                         $blocks[ $block->name ] = $block;
     628                        if ( isset( $blocks[ $block->name ] ) ) {
     629                            $blocks[ $block->name ] = (object) array_merge( (array) $blocks[ $block->name ], array_filter( (array) $block ) );
     630                        } else {
     631                            $blocks[ $block->name ] = $block;
     632                        }
    629633                    }
    630634                }
     
    792796                // Only certain properties must be valid for our purposes here.
    793797                $required_valid_props = array(
    794                     'block.json',
    795                     'block.json:editorScript',
    796                     'block.json:editorStyle',
    797                     'block.json:name',
    798                     'block.json:script',
    799                     'block.json:style',
     798                    'block.json[editorScript]',
     799                    'block.json[editorStyle]',
     800                    'block.json[name]',
     801                    'block.json[script]',
     802                    'block.json[style]',
    800803                );
    801                 $invalid_props = array_intersect( $required_valid_props, $result->get_error_data( 'error' ) ?: [] );
    802                 if ( empty( $invalid_props ) ) {
     804                $error = $result->get_error_message();
     805                $is_json_valid = array_reduce(
     806                    $required_valid_props,
     807                    function( $is_valid, $prop ) use ( $error ) {
     808                        return $is_valid && ( false === strpos( $error, $prop ) );
     809                    },
     810                    true
     811                );
     812                if ( $is_json_valid ) {
    803813                    $blocks[] = $block;
    804814                }
Note: See TracChangeset for help on using the changeset viewer.