Make WordPress Core

Changeset 48357


Ignore:
Timestamp:
07/07/2020 03:20:34 AM (5 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.