Make WordPress Core

Changeset 48357


Ignore:
Timestamp:
07/07/2020 03:20:34 AM (6 years ago)
Author:
TimothyBlynJacobs
Message:

REST API: Add support for the uniqueItems keyword.

Props sorenbronsted.
Fixes #48821.

Location:
trunk
Files:
2 added
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api.php

    r48307 r48357  
    14401440
    14411441/**
     1442 * Checks if an array is made up of unique items.
     1443 *
     1444 * @since 5.5.0
     1445 *
     1446 * @param array $array The array to check.
     1447 * @return bool True if the array contains unique items, false otherwise.
     1448 */
     1449function rest_validate_array_contains_unique_items( $array ) {
     1450        $seen = array();
     1451
     1452        foreach ( $array as $item ) {
     1453                $stabilized = rest_stabilize_value( $item );
     1454                $key        = serialize( $stabilized );
     1455
     1456                if ( ! isset( $seen[ $key ] ) ) {
     1457                        $seen[ $key ] = true;
     1458
     1459                        continue;
     1460                }
     1461
     1462                return false;
     1463        }
     1464
     1465        return true;
     1466}
     1467
     1468/**
     1469 * Stabilizes a value following JSON Schema semantics.
     1470 *
     1471 * For lists, order is preserved. For objects, properties are reordered alphabetically.
     1472 *
     1473 * @since 5.5.0
     1474 *
     1475 * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays.
     1476 * @return mixed The stabilized value.
     1477 */
     1478function rest_stabilize_value( $value ) {
     1479        if ( is_scalar( $value ) || is_null( $value ) ) {
     1480                return $value;
     1481        }
     1482
     1483        if ( is_object( $value ) ) {
     1484                _doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' );
     1485
     1486                return $value;
     1487        }
     1488
     1489        ksort( $value );
     1490
     1491        foreach ( $value as $k => $v ) {
     1492                $value[ $k ] = rest_stabilize_value( $v );
     1493        }
     1494
     1495        return $value;
     1496}
     1497
     1498/**
    14421499 * Validate a value based on a schema.
    14431500 *
    … …  
    14491506 * @since 5.5.0 Add the "uuid" and "hex-color" formats.
    14501507 *              Support the "minLength", "maxLength" and "pattern" keywords for strings.
     1508 *              Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays.
    14511509 *              Validate required properties.
    1452  *              Support the "minItems" and "maxItems" keywords for arrays.
    14531510 *
    14541511 * @param mixed  $value The value to validate.
    … …  
    14931550                $value = rest_sanitize_array( $value );
    14941551
    1495                 foreach ( $value as $index => $v ) {
    1496                         $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
    1497                         if ( is_wp_error( $is_valid ) ) {
    1498                                 return $is_valid;
     1552                if ( isset( $args['items'] ) ) {
     1553                        foreach ( $value as $index => $v ) {
     1554                                $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
     1555                                if ( is_wp_error( $is_valid ) ) {
     1556                                        return $is_valid;
     1557                                }
    14991558                        }
    15001559                }
    … …  
    15081567                        /* translators: 1: Parameter, 2: Number. */
    15091568                        return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at most %2$s items.' ), $param, number_format_i18n( $args['maxItems'] ) ) );
     1569                }
     1570
     1571                if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
     1572                        /* translators: 1: Parameter */
     1573                        return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) );
    15101574                }
    15111575        }
    … …  
    17191783 * @param array  $args  Schema array to use for sanitization.
    17201784 * @param string $param The parameter name, used in error messages.
    1721  * @return mixed The sanitized value.
     1785 * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
    17221786 */
    17231787function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
    … …  
    17511815                $value = rest_sanitize_array( $value );
    17521816
    1753                 if ( empty( $args['items'] ) ) {
    1754                         return $value;
    1755                 }
    1756 
    1757                 foreach ( $value as $index => $v ) {
    1758                         $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
     1817                if ( ! empty( $args['items'] ) ) {
     1818                        foreach ( $value as $index => $v ) {
     1819                                $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
     1820                        }
     1821                }
     1822
     1823                if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
     1824                        /* translators: 1: Parameter */
     1825                        return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) );
    17591826                }
    17601827
  • trunk/tests/phpunit/tests/rest-api/rest-schema-sanitization.php

    r48306 r48357  
    465465                $this->assertNull( rest_sanitize_value_from_schema( array( 'Hello!' ), $schema ) );
    466466        }
     467
     468        /**
     469         * @ticket 48821
     470         */
     471        public function test_unique_items_after_sanitization() {
     472                $schema = array(
     473                        'type'        => 'array',
     474                        'uniqueItems' => true,
     475                        'items'       => array(
     476                                'type'   => 'string',
     477                                'format' => 'uri',
     478                        ),
     479                );
     480
     481                $data = array(
     482                        'https://example.org/hello%20world',
     483                        'https://example.org/hello world',
     484                );
     485
     486                $this->assertTrue( rest_validate_value_from_schema( $data, $schema ) );
     487                $this->assertWPError( rest_sanitize_value_from_schema( $data, $schema ) );
     488        }
    467489}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php

    r48306 r48357  
    508508
    509509        /**
    510          * @ticket 48818
     510         * @ticket       48818
    511511         *
    512512         * @dataProvider data_required_property
    … …  
    536536
    537537        /**
    538          * @ticket 48818
     538         * @ticket       48818
    539539         *
    540540         * @dataProvider data_required_property
    … …  
    578578
    579579        /**
    580          * @ticket 48818
     580         * @ticket       48818
    581581         *
    582582         * @dataProvider data_required_nested_property
    … …  
    611611
    612612        /**
    613          * @ticket 48818
     613         * @ticket       48818
    614614         *
    615615         * @dataProvider data_required_nested_property
    … …  
    670670
    671671        /**
    672          * @ticket 48818
     672         * @ticket       48818
    673673         *
    674674         * @dataProvider data_required_deeply_nested_property
    … …  
    710710
    711711        /**
    712          * @ticket 48818
     712         * @ticket       48818
    713713         *
    714714         * @dataProvider data_required_deeply_nested_property
    … …  
    750750
    751751        /**
    752          * @ticket 48818
     752         * @ticket       48818
    753753         *
    754754         * @dataProvider data_required_deeply_nested_property
    … …  
    906906                $this->assertWPError( rest_validate_value_from_schema( 'foobar', $schema ) );
    907907        }
     908
     909        /**
     910         * @ticket       48821
     911         *
     912         * @dataProvider data_unique_items
     913         */
     914        public function test_unique_items( $test, $suite ) {
     915                $test_description = $suite['description'] . ': ' . $test['description'];
     916                $message          = $test_description . ': ' . var_export( $test['data'], true );
     917
     918                $valid = rest_validate_value_from_schema( $test['data'], $suite['schema'] );
     919
     920                if ( $test['valid'] ) {
     921                        $this->assertTrue( $valid, $message );
     922                } else {
     923                        $this->assertWPError( $valid, $message );
     924                }
     925        }
     926
     927        public function data_unique_items() {
     928                $all_types = array( 'object', 'array', 'null', 'number', 'integer', 'boolean', 'string' );
     929
     930                // the following test suites is not supported at the moment
     931                $skip   = array(
     932                        'uniqueItems with an array of items',
     933                        'uniqueItems with an array of items and additionalItems=false',
     934                        'uniqueItems=false with an array of items',
     935                        'uniqueItems=false with an array of items and additionalItems=false',
     936                );
     937                $suites = json_decode( file_get_contents( __DIR__ . '/json_schema_test_suite/uniqueitems.json' ), true );
     938
     939                $tests = array();
     940
     941                foreach ( $suites as $suite ) {
     942                        if ( in_array( $suite['description'], $skip, true ) ) {
     943                                continue;
     944                        }
     945                        // type is required for our implementation
     946                        if ( ! isset( $suite['schema']['type'] ) ) {
     947                                $suite['schema']['type'] = 'array';
     948                        }
     949                        // items is required for our implementation
     950                        if ( ! isset( $suite['schema']['items'] ) ) {
     951                                $suite['schema']['items'] = array(
     952                                        'type'  => $all_types,
     953                                        'items' => array(
     954                                                'type' => $all_types,
     955                                        ),
     956                                );
     957                        }
     958                        foreach ( $suite['tests'] as $test ) {
     959                                $tests[] = array( $test, $suite );
     960                        }
     961                }
     962
     963                return $tests;
     964        }
     965
     966        /**
     967         * @ticket 48821
     968         */
     969        public function test_unique_items_deep_objects() {
     970                $schema = array(
     971                        'type'        => 'array',
     972                        'uniqueItems' => true,
     973                        'items'       => array(
     974                                'type'       => 'object',
     975                                'properties' => array(
     976                                        'release' => array(
     977                                                'type'       => 'object',
     978                                                'properties' => array(
     979                                                        'name'    => array(
     980                                                                'type' => 'string',
     981                                                        ),
     982                                                        'version' => array(
     983                                                                'type' => 'string',
     984                                                        ),
     985                                                ),
     986                                        ),
     987                                ),
     988                        ),
     989                );
     990
     991                $data = array(
     992                        array(
     993                                'release' => array(
     994                                        'name'    => 'Kirk',
     995                                        'version' => '5.3',
     996                                ),
     997                        ),
     998                        array(
     999                                'release' => array(
     1000                                        'version' => '5.3',
     1001                                        'name'    => 'Kirk',
     1002                                ),
     1003                        ),
     1004                );
     1005
     1006                $this->assertWPError( rest_validate_value_from_schema( $data, $schema ) );
     1007
     1008                $data[0]['release']['version'] = '5.3.0';
     1009                $this->assertTrue( rest_validate_value_from_schema( $data, $schema ) );
     1010        }
     1011
     1012        /**
     1013         * @ticket 48821
     1014         */
     1015        public function test_unique_items_deep_arrays() {
     1016                $schema = array(
     1017                        'type'        => 'array',
     1018                        'uniqueItems' => true,
     1019                        'items'       => array(
     1020                                'type'  => 'array',
     1021                                'items' => array(
     1022                                        'type' => 'string',
     1023                                ),
     1024                        ),
     1025                );
     1026
     1027                $data = array(
     1028                        array(
     1029                                'Kirk',
     1030                                'Jaco',
     1031                        ),
     1032                        array(
     1033                                'Kirk',
     1034                                'Jaco',
     1035                        ),
     1036                );
     1037
     1038                $this->assertWPError( rest_validate_value_from_schema( $data, $schema ) );
     1039
     1040                $data[1] = array_reverse( $data[1] );
     1041                $this->assertTrue( rest_validate_value_from_schema( $data, $schema ) );
     1042        }
    9081043}
Note: See TracChangeset for help on using the changeset viewer.