Make WordPress Core

Changeset 39046


Ignore:
Timestamp:
10/31/2016 01:47:36 AM (10 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.