Make WordPress Core


Ignore:
Timestamp:
10/20/2020 06:22:39 PM (4 years ago)
Author:
TimothyBlynJacobs
Message:

REST API: Add support for the oneOf and anyOf keywords.

This allows for REST API routes to define more complex validation requirements as JSON Schema instead of procedural validation.

The error code returned from rest_validate_value_from_schema for invalid parameter types has been changed from the generic rest_invalid_param to the more specific rest_invalid_type.

Props yakimun, johnbillion, TimothyBlynJacobs.
Fixes #51025.

File:
1 edited

Legend:

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

    r49225 r49246  
    16661666
    16671667/**
     1668 * Formats a combining operation error into a WP_Error object.
     1669 *
     1670 * @since 5.6.0
     1671 *
     1672 * @param string $param The parameter name.
     1673 * @param array $error  The error details.
     1674 * @return WP_Error
     1675 */
     1676function rest_format_combining_operation_error( $param, $error ) {
     1677    $position = $error['index'];
     1678    $reason   = $error['error_object']->get_error_message();
     1679
     1680    if ( isset( $error['schema']['title'] ) ) {
     1681        $title = $error['schema']['title'];
     1682
     1683        return new WP_Error(
     1684            'rest_invalid_param',
     1685            /* translators: 1: Parameter, 2: Schema title, 3: Reason. */
     1686            sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ),
     1687            array( 'position' => $position )
     1688        );
     1689    }
     1690
     1691    return new WP_Error(
     1692        'rest_invalid_param',
     1693        /* translators: 1: Parameter, 2: Reason. */
     1694        sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ),
     1695        array( 'position' => $position )
     1696    );
     1697}
     1698
     1699/**
     1700 * Gets the error of combining operation.
     1701 *
     1702 * @since 5.6.0
     1703 *
     1704 * @param array  $value  The value to validate.
     1705 * @param string $param  The parameter name, used in error messages.
     1706 * @param array  $errors The errors array, to search for possible error.
     1707 * @return WP_Error      The combining operation error.
     1708 */
     1709function rest_get_combining_operation_error( $value, $param, $errors ) {
     1710    // If there is only one error, simply return it.
     1711    if ( 1 === count( $errors ) ) {
     1712        return rest_format_combining_operation_error( $param, $errors[0] );
     1713    }
     1714
     1715    // Filter out all errors related to type validation.
     1716    $filtered_errors = array();
     1717    foreach ( $errors as $error ) {
     1718        $error_code = $error['error_object']->get_error_code();
     1719        $error_data = $error['error_object']->get_error_data();
     1720
     1721        if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) {
     1722            $filtered_errors[] = $error;
     1723        }
     1724    }
     1725
     1726    // If there is only one error left, simply return it.
     1727    if ( 1 === count( $filtered_errors ) ) {
     1728        return rest_format_combining_operation_error( $param, $filtered_errors[0] );
     1729    }
     1730
     1731    // If there are only errors related to object validation, try choosing the most appropriate one.
     1732    if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) {
     1733        $result = null;
     1734        $number = 0;
     1735
     1736        foreach ( $filtered_errors as $error ) {
     1737            if ( isset( $error['schema']['properties'] ) ) {
     1738                $n = count( array_intersect_key( $error['schema']['properties'], $value ) );
     1739                if ( $n > $number ) {
     1740                    $result = $error;
     1741                    $number = $n;
     1742                }
     1743            }
     1744        }
     1745
     1746        if ( null !== $result ) {
     1747            return rest_format_combining_operation_error( $param, $result );
     1748        }
     1749    }
     1750
     1751    // If each schema has a title, include those titles in the error message.
     1752    $schema_titles = array();
     1753    foreach ( $errors as $error ) {
     1754        if ( isset( $error['schema']['title'] ) ) {
     1755            $schema_titles[] = $error['schema']['title'];
     1756        }
     1757    }
     1758
     1759    if ( count( $schema_titles ) === count( $errors ) ) {
     1760        /* translators: 1: Parameter, 2: Schema titles. */
     1761        return new WP_Error( 'rest_invalid_param', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) );
     1762    }
     1763
     1764    /* translators: 1: Parameter. */
     1765    return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s does not match any of the expected formats.' ), $param ) );
     1766}
     1767
     1768/**
     1769 * Finds the matching schema among the "anyOf" schemas.
     1770 *
     1771 * @since 5.6.0
     1772 *
     1773 * @param mixed  $value   The value to validate.
     1774 * @param array  $args    The schema array to use.
     1775 * @param string $param   The parameter name, used in error messages.
     1776 * @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match.
     1777 */
     1778function rest_find_any_matching_schema( $value, $args, $param ) {
     1779    $errors = array();
     1780
     1781    foreach ( $args['anyOf'] as $index => $schema ) {
     1782        if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
     1783            $schema['type'] = $args['type'];
     1784        }
     1785
     1786        $is_valid = rest_validate_value_from_schema( $value, $schema, $param );
     1787        if ( ! is_wp_error( $is_valid ) ) {
     1788            return $schema;
     1789        }
     1790
     1791        $errors[] = array(
     1792            'error_object' => $is_valid,
     1793            'schema'       => $schema,
     1794            'index'        => $index,
     1795        );
     1796    }
     1797
     1798    return rest_get_combining_operation_error( $value, $param, $errors );
     1799}
     1800
     1801/**
     1802 * Finds the matching schema among the "oneOf" schemas.
     1803 *
     1804 * @since 5.6.0
     1805 *
     1806 * @param mixed  $value                  The value to validate.
     1807 * @param array  $args                   The schema array to use.
     1808 * @param string $param                  The parameter name, used in error messages.
     1809 * @param bool   $stop_after_first_match Optional. Whether the process should stop after the first successful match.
     1810 * @return array|WP_Error                The matching schema or WP_Error instance if the number of matching schemas is not equal to one.
     1811 */
     1812function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) {
     1813    $matching_schemas = array();
     1814    $errors           = array();
     1815
     1816    foreach ( $args['oneOf'] as $index => $schema ) {
     1817        if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
     1818            $schema['type'] = $args['type'];
     1819        }
     1820
     1821        $is_valid = rest_validate_value_from_schema( $value, $schema, $param );
     1822        if ( ! is_wp_error( $is_valid ) ) {
     1823            if ( $stop_after_first_match ) {
     1824                return $schema;
     1825            }
     1826
     1827            $matching_schemas[] = array(
     1828                'schema_object' => $schema,
     1829                'index'         => $index,
     1830            );
     1831        } else {
     1832            $errors[] = array(
     1833                'error_object' => $is_valid,
     1834                'schema'       => $schema,
     1835                'index'        => $index,
     1836            );
     1837        }
     1838    }
     1839
     1840    if ( ! $matching_schemas ) {
     1841        return rest_get_combining_operation_error( $value, $param, $errors );
     1842    }
     1843
     1844    if ( count( $matching_schemas ) > 1 ) {
     1845        $schema_positions = array();
     1846        $schema_titles    = array();
     1847
     1848        foreach ( $matching_schemas as $schema ) {
     1849            $schema_positions[] = $schema['index'];
     1850
     1851            if ( isset( $schema['schema_object']['title'] ) ) {
     1852                $schema_titles[] = $schema['schema_object']['title'];
     1853            }
     1854        }
     1855
     1856        // If each schema has a title, include those titles in the error message.
     1857        if ( count( $schema_titles ) === count( $matching_schemas ) ) {
     1858            return new WP_Error(
     1859                'rest_invalid_param',
     1860                /* translators: 1: Parameter, 2: Schema titles. */
     1861                wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ),
     1862                array( 'positions' => $schema_positions )
     1863            );
     1864        }
     1865
     1866        return new WP_Error(
     1867            'rest_invalid_param',
     1868            /* translators: 1: Parameter. */
     1869            sprintf( __( '%1$s matches more than one of the expected formats.' ), $param ),
     1870            array( 'positions' => $schema_positions )
     1871        );
     1872    }
     1873
     1874    return $matching_schemas[0]['schema_object'];
     1875}
     1876
     1877/**
    16681878 * Validate a value based on a schema.
    16691879 *
     
    16801890 *              Support the "multipleOf" keyword for numbers and integers.
    16811891 *              Support the "patternProperties" keyword for objects.
     1892 *              Support the "anyOf" and "oneOf" keywords.
    16821893 *
    16831894 * @param mixed  $value The value to validate.
     
    16871898 */
    16881899function rest_validate_value_from_schema( $value, $args, $param = '' ) {
     1900    if ( isset( $args['anyOf'] ) ) {
     1901        $matching_schema = rest_find_any_matching_schema( $value, $args, $param );
     1902        if ( is_wp_error( $matching_schema ) ) {
     1903            return $matching_schema;
     1904        }
     1905
     1906        if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
     1907            $args['type'] = $matching_schema['type'];
     1908        }
     1909    }
     1910
     1911    if ( isset( $args['oneOf'] ) ) {
     1912        $matching_schema = rest_find_one_matching_schema( $value, $args, $param );
     1913        if ( is_wp_error( $matching_schema ) ) {
     1914            return $matching_schema;
     1915        }
     1916
     1917        if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
     1918            $args['type'] = $matching_schema['type'];
     1919        }
     1920    }
     1921
    16891922    $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
    16901923
     
    16981931
    16991932        if ( ! $best_type ) {
    1700             /* translators: 1: Parameter, 2: List of types. */
    1701             return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) );
     1933            return new WP_Error(
     1934                'rest_invalid_type',
     1935                /* translators: 1: Parameter, 2: List of types. */
     1936                sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ),
     1937                array( 'param' => $param )
     1938            );
    17021939        }
    17031940
     
    17161953    if ( 'array' === $args['type'] ) {
    17171954        if ( ! rest_is_array( $value ) ) {
    1718             /* translators: 1: Parameter, 2: Type name. */
    1719             return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
     1955            return new WP_Error(
     1956                'rest_invalid_type',
     1957                /* translators: 1: Parameter, 2: Type name. */
     1958                sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ),
     1959                array( 'param' => $param )
     1960            );
    17201961        }
    17211962
     
    17491990    if ( 'object' === $args['type'] ) {
    17501991        if ( ! rest_is_object( $value ) ) {
    1751             /* translators: 1: Parameter, 2: Type name. */
    1752             return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ) );
     1992            return new WP_Error(
     1993                'rest_invalid_type',
     1994                /* translators: 1: Parameter, 2: Type name. */
     1995                sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ),
     1996                array( 'param' => $param )
     1997            );
    17531998        }
    17541999
     
    18172062    if ( 'null' === $args['type'] ) {
    18182063        if ( null !== $value ) {
    1819             /* translators: 1: Parameter, 2: Type name. */
    1820             return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ) );
     2064            return new WP_Error(
     2065                'rest_invalid_type',
     2066                /* translators: 1: Parameter, 2: Type name. */
     2067                sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ),
     2068                array( 'param' => $param )
     2069            );
    18212070        }
    18222071
     
    18332082    if ( in_array( $args['type'], array( 'integer', 'number' ), true ) ) {
    18342083        if ( ! is_numeric( $value ) ) {
    1835             /* translators: 1: Parameter, 2: Type name. */
    1836             return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
     2084            return new WP_Error(
     2085                'rest_invalid_type',
     2086                /* translators: 1: Parameter, 2: Type name. */
     2087                sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ),
     2088                array( 'param' => $param )
     2089            );
    18372090        }
    18382091
     
    18442097
    18452098    if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) {
    1846         /* translators: 1: Parameter, 2: Type name. */
    1847         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
     2099        return new WP_Error(
     2100            'rest_invalid_type',
     2101            /* translators: 1: Parameter, 2: Type name. */
     2102            sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ),
     2103            array( 'param' => $param )
     2104        );
    18482105    }
    18492106
    18502107    if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
    1851         /* translators: 1: Parameter, 2: Type name. */
    1852         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ) );
     2108        return new WP_Error(
     2109            'rest_invalid_type',
     2110            /* translators: 1: Parameter, 2: Type name. */
     2111            sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ),
     2112            array( 'param' => $param )
     2113        );
    18532114    }
    18542115
    18552116    if ( 'string' === $args['type'] ) {
    18562117        if ( ! is_string( $value ) ) {
    1857             /* translators: 1: Parameter, 2: Type name. */
    1858             return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
     2118            return new WP_Error(
     2119                'rest_invalid_type',
     2120                /* translators: 1: Parameter, 2: Type name. */
     2121                sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ),
     2122                array( 'param' => $param )
     2123            );
    18592124        }
    18602125
     
    19772242 * @since 4.7.0
    19782243 * @since 5.5.0 Added the `$param` parameter.
     2244 * @since 5.6.0 Support the "anyOf" and "oneOf" keywords.
    19792245 *
    19802246 * @param mixed  $value The value to sanitize.
     
    19842250 */
    19852251function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
     2252    if ( isset( $args['anyOf'] ) ) {
     2253        $matching_schema = rest_find_any_matching_schema( $value, $args, $param );
     2254        if ( is_wp_error( $matching_schema ) ) {
     2255            return $matching_schema;
     2256        }
     2257
     2258        if ( ! isset( $args['type'] ) ) {
     2259            $args['type'] = $matching_schema['type'];
     2260        }
     2261
     2262        $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
     2263    }
     2264
     2265    if ( isset( $args['oneOf'] ) ) {
     2266        $matching_schema = rest_find_one_matching_schema( $value, $args, $param );
     2267        if ( is_wp_error( $matching_schema ) ) {
     2268            return $matching_schema;
     2269        }
     2270
     2271        if ( ! isset( $args['type'] ) ) {
     2272            $args['type'] = $matching_schema['type'];
     2273        }
     2274
     2275        $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
     2276    }
     2277
    19862278    $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
    19872279
     
    21992491 * @since 5.5.0
    22002492 * @since 5.6.0 Support the "patternProperties" keyword for objects.
     2493 *              Support the "anyOf" and "oneOf" keywords.
    22012494 *
    22022495 * @param array|object $data    The response data to modify.
     
    22062499 */
    22072500function rest_filter_response_by_context( $data, $schema, $context ) {
     2501    if ( isset( $schema['anyOf'] ) ) {
     2502        $matching_schema = rest_find_any_matching_schema( $data, $schema, '' );
     2503        if ( ! is_wp_error( $matching_schema ) ) {
     2504            if ( ! isset( $schema['type'] ) ) {
     2505                $schema['type'] = $matching_schema['type'];
     2506            }
     2507
     2508            $data = rest_filter_response_by_context( $data, $matching_schema, $context );
     2509        }
     2510    }
     2511
     2512    if ( isset( $schema['oneOf'] ) ) {
     2513        $matching_schema = rest_find_one_matching_schema( $data, $schema, '', true );
     2514        if ( ! is_wp_error( $matching_schema ) ) {
     2515            if ( ! isset( $schema['type'] ) ) {
     2516                $schema['type'] = $matching_schema['type'];
     2517            }
     2518
     2519            $data = rest_filter_response_by_context( $data, $matching_schema, $context );
     2520        }
     2521    }
     2522
    22082523    if ( ! is_array( $data ) && ! is_object( $data ) ) {
    22092524        return $data;
     
    24722787        'maxItems',
    24732788        'uniqueItems',
     2789        'anyOf',
     2790        'oneOf',
    24742791    );
    24752792
Note: See TracChangeset for help on using the changeset viewer.