Make WordPress Core

Changeset 39046


Ignore:
Timestamp:
10/31/2016 01:47:36 AM (7 years ago)
Author:
pento
Message:

REST API: Add support for arrays in schema validation and sanitization.

By allowing more fine-grained validation and sanitisation of endpoint args, we can ensure the correct data is being passed to endpoints.

This can easily be extended to support new data types, such as CSV fields or objects.

Props joehoyle, rachelbaker, pento.
Fixes #38531.

Location:
trunk
Files:
1 added
5 edited

Legend:

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

    r39042 r39046  
    821821    $args = $attributes['args'][ $param ];
    822822
     823    return rest_validate_value_from_schema( $value, $args, $param );
     824}
     825
     826/**
     827 * Sanitize a request argument based on details registered to the route.
     828 *
     829 * @since 4.7.0
     830 *
     831 * @param  mixed            $value
     832 * @param  WP_REST_Request  $request
     833 * @param  string           $param
     834 * @return mixed
     835 */
     836function rest_sanitize_request_arg( $value, $request, $param ) {
     837    $attributes = $request->get_attributes();
     838    if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
     839        return $value;
     840    }
     841    $args = $attributes['args'][ $param ];
     842
     843    return rest_sanitize_value_from_schema( $value, $args, $param );
     844}
     845
     846/**
     847 * Parse a request argument based on details registered to the route.
     848 *
     849 * Runs a validation check and sanitizes the value, primarily to be used via
     850 * the `sanitize_callback` arguments in the endpoint args registration.
     851 *
     852 * @since 4.7.0
     853 *
     854 * @param  mixed            $value
     855 * @param  WP_REST_Request  $request
     856 * @param  string           $param
     857 * @return mixed
     858 */
     859function rest_parse_request_arg( $value, $request, $param ) {
     860    $is_valid = rest_validate_request_arg( $value, $request, $param );
     861
     862    if ( is_wp_error( $is_valid ) ) {
     863        return $is_valid;
     864    }
     865
     866    $value = rest_sanitize_request_arg( $value, $request, $param );
     867
     868    return $value;
     869}
     870
     871/**
     872 * Determines if a IPv4 address is valid.
     873 *
     874 * Does not handle IPv6 addresses.
     875 *
     876 * @since 4.7.0
     877 *
     878 * @param  string $ipv4 IP 32-bit address.
     879 * @return string|false The valid IPv4 address, otherwise false.
     880 */
     881function rest_is_ip_address( $ipv4 ) {
     882    $pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
     883
     884    if ( ! preg_match( $pattern, $ipv4 ) ) {
     885        return false;
     886    }
     887
     888    return $ipv4;
     889}
     890
     891/**
     892 * Changes a boolean-like value into the proper boolean value.
     893 *
     894 * @since 4.7.0
     895 *
     896 * @param bool|string|int $value The value being evaluated.
     897 * @return boolean Returns the proper associated boolean value.
     898 */
     899function rest_sanitize_boolean( $value ) {
     900    // String values are translated to `true`; make sure 'false' is false.
     901    if ( is_string( $value )  ) {
     902        $value = strtolower( $value );
     903        if ( in_array( $value, array( 'false', '0' ), true ) ) {
     904            $value = false;
     905        }
     906    }
     907
     908    // Everything else will map nicely to boolean.
     909    return (boolean) $value;
     910}
     911
     912/**
     913 * Determines if a given value is boolean-like.
     914 *
     915 * @since 4.7.0
     916 *
     917 * @param bool|string $maybe_bool The value being evaluated.
     918 * @return boolean True if a boolean, otherwise false.
     919 */
     920function rest_is_boolean( $maybe_bool ) {
     921    if ( is_bool( $maybe_bool ) ) {
     922        return true;
     923    }
     924
     925    if ( is_string( $maybe_bool ) ) {
     926        $maybe_bool = strtolower( $maybe_bool );
     927
     928        $valid_boolean_values = array(
     929            'false',
     930            'true',
     931            '0',
     932            '1',
     933        );
     934
     935        return in_array( $maybe_bool, $valid_boolean_values, true );
     936    }
     937
     938    if ( is_int( $maybe_bool ) ) {
     939        return in_array( $maybe_bool, array( 0, 1 ), true );
     940    }
     941
     942    return false;
     943}
     944
     945/**
     946 * Retrieves the avatar urls in various sizes based on a given email address.
     947 *
     948 * @since 4.7.0
     949 *
     950 * @see get_avatar_url()
     951 *
     952 * @param string $email Email address.
     953 * @return array $urls Gravatar url for each size.
     954 */
     955function rest_get_avatar_urls( $email ) {
     956    $avatar_sizes = rest_get_avatar_sizes();
     957
     958    $urls = array();
     959    foreach ( $avatar_sizes as $size ) {
     960        $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
     961    }
     962
     963    return $urls;
     964}
     965
     966/**
     967 * Retrieves the pixel sizes for avatars.
     968 *
     969 * @since 4.7.0
     970 *
     971 * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
     972 */
     973function rest_get_avatar_sizes() {
     974    /**
     975     * Filter the REST avatar sizes.
     976     *
     977     * Use this filter to adjust the array of sizes returned by the
     978     * `rest_get_avatar_sizes` function.
     979     *
     980     * @since 4.4.0
     981     *
     982     * @param array $sizes An array of int values that are the pixel sizes for avatars.
     983     *                     Default `[ 24, 48, 96 ]`.
     984     */
     985    return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
     986}
     987
     988/**
     989 * Validate a value based on a schema.
     990 *
     991 * @param mixed  $value The value to validate.
     992 * @param array  $args  Schema array to use for validation.
     993 * @param string $param The parameter name, used in error messages.
     994 * @return true|WP_Error
     995 */
     996function rest_validate_value_from_schema( $value, $args, $param = '' ) {
     997    if ( 'array' === $args['type'] ) {
     998        if ( ! is_array( $value ) ) {
     999            return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
     1000        }
     1001        foreach ( $value as $index => $v ) {
     1002            $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
     1003            if ( is_wp_error( $is_valid ) ) {
     1004                return $is_valid;
     1005            }
     1006        }
     1007    }
    8231008    if ( ! empty( $args['enum'] ) ) {
    8241009        if ( ! in_array( $value, $args['enum'], true ) ) {
     
    8271012    }
    8281013
    829     if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) {
     1014    if ( in_array( $args['type'], array( 'integer', 'number' ) ) && ! is_numeric( $value ) ) {
     1015        return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
     1016    }
     1017
     1018    if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) {
    8301019        return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
    8311020    }
     
    8601049    }
    8611050
    862     if ( in_array( $args['type'], array( 'numeric', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
     1051    if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
    8631052        if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
    8641053            if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
     
    8981087
    8991088/**
    900  * Sanitize a request argument based on details registered to the route.
    901  *
    902  * @since 4.7.0
    903  *
    904  * @param  mixed            $value
    905  * @param  WP_REST_Request  $request
    906  * @param  string           $param
    907  * @return mixed
    908  */
    909 function rest_sanitize_request_arg( $value, $request, $param ) {
    910     $attributes = $request->get_attributes();
    911     if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
     1089 * Sanitize a value based on a schema.
     1090 *
     1091 * @param mixed $value The value to sanitize.
     1092 * @param array $args  Schema array to use for sanitization.
     1093 * @return true|WP_Error
     1094 */
     1095function rest_sanitize_value_from_schema( $value, $args ) {
     1096    if ( 'array' === $args['type'] ) {
     1097        if ( empty( $args['items'] ) ) {
     1098            return (array) $value;
     1099        }
     1100        foreach ( $value as $index => $v ) {
     1101            $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] );
     1102        }
    9121103        return $value;
    9131104    }
    914     $args = $attributes['args'][ $param ];
    915 
    9161105    if ( 'integer' === $args['type'] ) {
    9171106        return (int) $value;
     1107    }
     1108
     1109    if ( 'number' === $args['type'] ) {
     1110        return (float) $value;
    9181111    }
    9191112
     
    9291122            case 'email' :
    9301123                /*
    931                  * sanitize_email() validates, which would be unexpected
     1124                 * sanitize_email() validates, which would be unexpected.
    9321125                 */
    9331126                return sanitize_text_field( $value );
     
    9431136    return $value;
    9441137}
    945 
    946 /**
    947  * Parse a request argument based on details registered to the route.
    948  *
    949  * Runs a validation check and sanitizes the value, primarily to be used via
    950  * the `sanitize_callback` arguments in the endpoint args registration.
    951  *
    952  * @since 4.7.0
    953  *
    954  * @param  mixed            $value
    955  * @param  WP_REST_Request  $request
    956  * @param  string           $param
    957  * @return mixed
    958  */
    959 function rest_parse_request_arg( $value, $request, $param ) {
    960     $is_valid = rest_validate_request_arg( $value, $request, $param );
    961 
    962     if ( is_wp_error( $is_valid ) ) {
    963         return $is_valid;
    964     }
    965 
    966     $value = rest_sanitize_request_arg( $value, $request, $param );
    967 
    968     return $value;
    969 }
    970 
    971 /**
    972  * Determines if a IPv4 address is valid.
    973  *
    974  * Does not handle IPv6 addresses.
    975  *
    976  * @since 4.7.0
    977  *
    978  * @param  string $ipv4 IP 32-bit address.
    979  * @return string|false The valid IPv4 address, otherwise false.
    980  */
    981 function rest_is_ip_address( $ipv4 ) {
    982     $pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
    983 
    984     if ( ! preg_match( $pattern, $ipv4 ) ) {
    985         return false;
    986     }
    987 
    988     return $ipv4;
    989 }
    990 
    991 /**
    992  * Changes a boolean-like value into the proper boolean value.
    993  *
    994  * @since 4.7.0
    995  *
    996  * @param bool|string|int $value The value being evaluated.
    997  * @return boolean Returns the proper associated boolean value.
    998  */
    999 function rest_sanitize_boolean( $value ) {
    1000     // String values are translated to `true`; make sure 'false' is false.
    1001     if ( is_string( $value )  ) {
    1002         $value = strtolower( $value );
    1003         if ( in_array( $value, array( 'false', '0' ), true ) ) {
    1004             $value = false;
    1005         }
    1006     }
    1007 
    1008     // Everything else will map nicely to boolean.
    1009     return (boolean) $value;
    1010 }
    1011 
    1012 /**
    1013  * Determines if a given value is boolean-like.
    1014  *
    1015  * @since 4.7.0
    1016  *
    1017  * @param bool|string $maybe_bool The value being evaluated.
    1018  * @return boolean True if a boolean, otherwise false.
    1019  */
    1020 function rest_is_boolean( $maybe_bool ) {
    1021     if ( is_bool( $maybe_bool ) ) {
    1022         return true;
    1023     }
    1024 
    1025     if ( is_string( $maybe_bool ) ) {
    1026         $maybe_bool = strtolower( $maybe_bool );
    1027 
    1028         $valid_boolean_values = array(
    1029             'false',
    1030             'true',
    1031             '0',
    1032             '1',
    1033         );
    1034 
    1035         return in_array( $maybe_bool, $valid_boolean_values, true );
    1036     }
    1037 
    1038     if ( is_int( $maybe_bool ) ) {
    1039         return in_array( $maybe_bool, array( 0, 1 ), true );
    1040     }
    1041 
    1042     return false;
    1043 }
    1044 
    1045 /**
    1046  * Retrieves the avatar urls in various sizes based on a given email address.
    1047  *
    1048  * @since 4.7.0
    1049  *
    1050  * @see get_avatar_url()
    1051  *
    1052  * @param string $email Email address.
    1053  * @return array $urls Gravatar url for each size.
    1054  */
    1055 function rest_get_avatar_urls( $email ) {
    1056     $avatar_sizes = rest_get_avatar_sizes();
    1057 
    1058     $urls = array();
    1059     foreach ( $avatar_sizes as $size ) {
    1060         $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
    1061     }
    1062 
    1063     return $urls;
    1064 }
    1065 
    1066 /**
    1067  * Retrieves the pixel sizes for avatars.
    1068  *
    1069  * @since 4.7.0
    1070  *
    1071  * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
    1072  */
    1073 function rest_get_avatar_sizes() {
    1074     /**
    1075      * Filter the REST avatar sizes.
    1076      *
    1077      * Use this filter to adjust the array of sizes returned by the
    1078      * `rest_get_avatar_sizes` function.
    1079      *
    1080      * @since 4.4.0
    1081      *
    1082      * @param array $sizes An array of int values that are the pixel sizes for avatars.
    1083      *                     Default `[ 24, 48, 96 ]`.
    1084      */
    1085     return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
    1086 }
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php

    r39021 r39046  
    560560            }
    561561
    562             foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) {
     562            foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) {
    563563                if ( isset( $params[ $schema_prop ] ) ) {
    564564                    $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

    r39026 r39046  
    19721972                'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
    19731973                'type'        => 'array',
     1974                'items'       => array(
     1975                    'type'    => 'integer',
     1976                ),
    19741977                'context'     => array( 'view', 'edit' ),
    19751978            );
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php

    r39030 r39046  
    289289        foreach ( $options as $option_name => $option ) {
    290290            $schema['properties'][ $option_name ] = $option['schema'];
     291            $schema['properties'][ $option_name ]['arg_options'] = array(
     292                'sanitize_callback' => array( $this, 'sanitize_callback' ),
     293            );
    291294        }
    292295
    293296        return $this->add_additional_fields_schema( $schema );
    294297    }
     298
     299    /**
     300     * Custom sanitize callback used for all options to allow the use of 'null'.
     301     *
     302     * By default, the schema of settings will throw an error if a value is set to
     303     * `null` as it's not a valid value for something like "type => string". We
     304     * provide a wrapper sanitizer to whitelist the use of `null`.
     305     *
     306     * @param  mixed           $value   The value for the setting.
     307     * @param  WP_REST_Request $request The request object.
     308     * @param  string          $param   The parameter name.
     309     * @return mixed|WP_Error
     310     */
     311    public function sanitize_callback( $value, $request, $param ) {
     312        if ( is_null( $value ) ) {
     313            return $value;
     314        }
     315        return rest_parse_request_arg( $value, $request, $param );
     316    }
    295317}
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php

    r39036 r39046  
    10071007                    'description' => __( 'Roles assigned to the resource.' ),
    10081008                    'type'        => 'array',
     1009                    'items'       => array(
     1010                        'type'    => 'string',
     1011                    ),
    10091012                    'context'     => array( 'edit' ),
    10101013                ),
Note: See TracChangeset for help on using the changeset viewer.