Make WordPress Core


Ignore:
Timestamp:
07/05/2020 12:13:37 AM (5 years ago)
Author:
TimothyBlynJacobs
Message:

REST API: Make multi-typed schemas more robust.

A multi-type schema is a schema where the type keyword is an array of possible types instead of a single type. For instance, [ 'object', 'string' ] would allow objects or string values.

In [46249] basic support for these schemas was introduced. The validator would loop over each schema type trying to find a version that matched. This worked for valid values, but for invalid values it provided unhelpful error messages. The sanitizer also had its utility restricted.

In this commit, the validators and sanitizers will first determine the best type of the passed value and then apply the schema with that set type. In the case that a value could match multiple types, the schema of the first matching type will be used.

To maintain backward compatibility, if unsupported schema types are used, the value will always pass validation. A doing it wrong notice is issued in this case.

Fixes #50300.
Props pentatonicfunk, dlh, TimothyBlynJacobs.

File:
1 edited

Legend:

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

    r48302 r48306  
    987987
    988988/**
    989  * Parses an RFC3339 time into a Unix timestamp.
    990  *
    991  * @since 4.4.0
    992  *
    993  * @param string $date      RFC3339 timestamp.
    994  * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
    995  *                          the timestamp's timezone. Default false.
    996  * @return int Unix timestamp.
    997  */
    998 function rest_parse_date( $date, $force_utc = false ) {
    999     if ( $force_utc ) {
    1000         $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
    1001     }
    1002 
    1003     $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
    1004 
    1005     if ( ! preg_match( $regex, $date, $matches ) ) {
    1006         return false;
    1007     }
    1008 
    1009     return strtotime( $date );
    1010 }
    1011 
    1012 /**
    1013  * Parses a 3 or 6 digit hex color (with #).
    1014  *
    1015  * @since 5.4.0
    1016  *
    1017  * @param string $color 3 or 6 digit hex color (with #).
    1018  * @return string|false
    1019  */
    1020 function rest_parse_hex_color( $color ) {
    1021     $regex = '|^#([A-Fa-f0-9]{3}){1,2}$|';
    1022     if ( ! preg_match( $regex, $color, $matches ) ) {
    1023         return false;
    1024     }
    1025 
    1026     return $color;
    1027 }
    1028 
    1029 /**
    1030  * Parses a date into both its local and UTC equivalent, in MySQL datetime format.
    1031  *
    1032  * @since 4.4.0
    1033  *
    1034  * @see rest_parse_date()
    1035  *
    1036  * @param string $date   RFC3339 timestamp.
    1037  * @param bool   $is_utc Whether the provided date should be interpreted as UTC. Default false.
    1038  * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
    1039  *                    null on failure.
    1040  */
    1041 function rest_get_date_with_gmt( $date, $is_utc = false ) {
    1042     /*
    1043      * Whether or not the original date actually has a timezone string
    1044      * changes the way we need to do timezone conversion.
    1045      * Store this info before parsing the date, and use it later.
    1046      */
    1047     $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date );
    1048 
    1049     $date = rest_parse_date( $date );
    1050 
    1051     if ( empty( $date ) ) {
    1052         return null;
    1053     }
    1054 
    1055     /*
    1056      * At this point $date could either be a local date (if we were passed
    1057      * a *local* date without a timezone offset) or a UTC date (otherwise).
    1058      * Timezone conversion needs to be handled differently between these two cases.
    1059      */
    1060     if ( ! $is_utc && ! $has_timezone ) {
    1061         $local = gmdate( 'Y-m-d H:i:s', $date );
    1062         $utc   = get_gmt_from_date( $local );
    1063     } else {
    1064         $utc   = gmdate( 'Y-m-d H:i:s', $date );
    1065         $local = get_date_from_gmt( $utc );
    1066     }
    1067 
    1068     return array( $local, $utc );
    1069 }
    1070 
    1071 /**
    1072  * Returns a contextual HTTP error code for authorization failure.
    1073  *
    1074  * @since 4.7.0
    1075  *
    1076  * @return integer 401 if the user is not logged in, 403 if the user is logged in.
    1077  */
    1078 function rest_authorization_required_code() {
    1079     return is_user_logged_in() ? 403 : 401;
    1080 }
    1081 
    1082 /**
    1083  * Validate a request argument based on details registered to the route.
    1084  *
    1085  * @since 4.7.0
    1086  *
    1087  * @param mixed           $value
    1088  * @param WP_REST_Request $request
    1089  * @param string          $param
    1090  * @return true|WP_Error
    1091  */
    1092 function rest_validate_request_arg( $value, $request, $param ) {
    1093     $attributes = $request->get_attributes();
    1094     if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
    1095         return true;
    1096     }
    1097     $args = $attributes['args'][ $param ];
    1098 
    1099     return rest_validate_value_from_schema( $value, $args, $param );
    1100 }
    1101 
    1102 /**
    1103  * Sanitize a request argument based on details registered to the route.
    1104  *
    1105  * @since 4.7.0
    1106  *
    1107  * @param mixed           $value
    1108  * @param WP_REST_Request $request
    1109  * @param string          $param
    1110  * @return mixed
    1111  */
    1112 function rest_sanitize_request_arg( $value, $request, $param ) {
    1113     $attributes = $request->get_attributes();
    1114     if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
    1115         return $value;
    1116     }
    1117     $args = $attributes['args'][ $param ];
    1118 
    1119     return rest_sanitize_value_from_schema( $value, $args );
    1120 }
    1121 
    1122 /**
    1123  * Parse a request argument based on details registered to the route.
    1124  *
    1125  * Runs a validation check and sanitizes the value, primarily to be used via
    1126  * the `sanitize_callback` arguments in the endpoint args registration.
    1127  *
    1128  * @since 4.7.0
    1129  *
    1130  * @param mixed           $value
    1131  * @param WP_REST_Request $request
    1132  * @param string          $param
    1133  * @return mixed
    1134  */
    1135 function rest_parse_request_arg( $value, $request, $param ) {
    1136     $is_valid = rest_validate_request_arg( $value, $request, $param );
    1137 
    1138     if ( is_wp_error( $is_valid ) ) {
    1139         return $is_valid;
    1140     }
    1141 
    1142     $value = rest_sanitize_request_arg( $value, $request, $param );
    1143 
    1144     return $value;
    1145 }
    1146 
    1147 /**
    1148  * Determines if an IP address is valid.
    1149  *
    1150  * Handles both IPv4 and IPv6 addresses.
    1151  *
    1152  * @since 4.7.0
    1153  *
    1154  * @param string $ip IP address.
    1155  * @return string|false The valid IP address, otherwise false.
    1156  */
    1157 function rest_is_ip_address( $ip ) {
    1158     $ipv4_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]?)$/';
    1159 
    1160     if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
    1161         return false;
    1162     }
    1163 
    1164     return $ip;
    1165 }
    1166 
    1167 /**
    1168  * Changes a boolean-like value into the proper boolean value.
    1169  *
    1170  * @since 4.7.0
    1171  *
    1172  * @param bool|string|int $value The value being evaluated.
    1173  * @return boolean Returns the proper associated boolean value.
    1174  */
    1175 function rest_sanitize_boolean( $value ) {
    1176     // String values are translated to `true`; make sure 'false' is false.
    1177     if ( is_string( $value ) ) {
    1178         $value = strtolower( $value );
    1179         if ( in_array( $value, array( 'false', '0' ), true ) ) {
    1180             $value = false;
    1181         }
    1182     }
    1183 
    1184     // Everything else will map nicely to boolean.
    1185     return (bool) $value;
    1186 }
    1187 
    1188 /**
    1189  * Determines if a given value is boolean-like.
    1190  *
    1191  * @since 4.7.0
    1192  *
    1193  * @param bool|string $maybe_bool The value being evaluated.
    1194  * @return boolean True if a boolean, otherwise false.
    1195  */
    1196 function rest_is_boolean( $maybe_bool ) {
    1197     if ( is_bool( $maybe_bool ) ) {
    1198         return true;
    1199     }
    1200 
    1201     if ( is_string( $maybe_bool ) ) {
    1202         $maybe_bool = strtolower( $maybe_bool );
    1203 
    1204         $valid_boolean_values = array(
    1205             'false',
    1206             'true',
    1207             '0',
    1208             '1',
    1209         );
    1210 
    1211         return in_array( $maybe_bool, $valid_boolean_values, true );
    1212     }
    1213 
    1214     if ( is_int( $maybe_bool ) ) {
    1215         return in_array( $maybe_bool, array( 0, 1 ), true );
    1216     }
    1217 
    1218     return false;
    1219 }
    1220 
    1221 /**
    1222989 * Retrieves the avatar urls in various sizes.
    1223990 *
     
    12641031
    12651032/**
     1033 * Parses an RFC3339 time into a Unix timestamp.
     1034 *
     1035 * @since 4.4.0
     1036 *
     1037 * @param string $date      RFC3339 timestamp.
     1038 * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
     1039 *                          the timestamp's timezone. Default false.
     1040 * @return int Unix timestamp.
     1041 */
     1042function rest_parse_date( $date, $force_utc = false ) {
     1043    if ( $force_utc ) {
     1044        $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
     1045    }
     1046
     1047    $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
     1048
     1049    if ( ! preg_match( $regex, $date, $matches ) ) {
     1050        return false;
     1051    }
     1052
     1053    return strtotime( $date );
     1054}
     1055
     1056/**
     1057 * Parses a 3 or 6 digit hex color (with #).
     1058 *
     1059 * @since 5.4.0
     1060 *
     1061 * @param string $color 3 or 6 digit hex color (with #).
     1062 * @return string|false
     1063 */
     1064function rest_parse_hex_color( $color ) {
     1065    $regex = '|^#([A-Fa-f0-9]{3}){1,2}$|';
     1066    if ( ! preg_match( $regex, $color, $matches ) ) {
     1067        return false;
     1068    }
     1069
     1070    return $color;
     1071}
     1072
     1073/**
     1074 * Parses a date into both its local and UTC equivalent, in MySQL datetime format.
     1075 *
     1076 * @since 4.4.0
     1077 *
     1078 * @see rest_parse_date()
     1079 *
     1080 * @param string $date   RFC3339 timestamp.
     1081 * @param bool   $is_utc Whether the provided date should be interpreted as UTC. Default false.
     1082 * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
     1083 *                    null on failure.
     1084 */
     1085function rest_get_date_with_gmt( $date, $is_utc = false ) {
     1086    /*
     1087     * Whether or not the original date actually has a timezone string
     1088     * changes the way we need to do timezone conversion.
     1089     * Store this info before parsing the date, and use it later.
     1090     */
     1091    $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date );
     1092
     1093    $date = rest_parse_date( $date );
     1094
     1095    if ( empty( $date ) ) {
     1096        return null;
     1097    }
     1098
     1099    /*
     1100     * At this point $date could either be a local date (if we were passed
     1101     * a *local* date without a timezone offset) or a UTC date (otherwise).
     1102     * Timezone conversion needs to be handled differently between these two cases.
     1103     */
     1104    if ( ! $is_utc && ! $has_timezone ) {
     1105        $local = gmdate( 'Y-m-d H:i:s', $date );
     1106        $utc   = get_gmt_from_date( $local );
     1107    } else {
     1108        $utc   = gmdate( 'Y-m-d H:i:s', $date );
     1109        $local = get_date_from_gmt( $utc );
     1110    }
     1111
     1112    return array( $local, $utc );
     1113}
     1114
     1115/**
     1116 * Returns a contextual HTTP error code for authorization failure.
     1117 *
     1118 * @since 4.7.0
     1119 *
     1120 * @return integer 401 if the user is not logged in, 403 if the user is logged in.
     1121 */
     1122function rest_authorization_required_code() {
     1123    return is_user_logged_in() ? 403 : 401;
     1124}
     1125
     1126/**
     1127 * Validate a request argument based on details registered to the route.
     1128 *
     1129 * @since 4.7.0
     1130 *
     1131 * @param mixed           $value
     1132 * @param WP_REST_Request $request
     1133 * @param string          $param
     1134 * @return true|WP_Error
     1135 */
     1136function rest_validate_request_arg( $value, $request, $param ) {
     1137    $attributes = $request->get_attributes();
     1138    if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
     1139        return true;
     1140    }
     1141    $args = $attributes['args'][ $param ];
     1142
     1143    return rest_validate_value_from_schema( $value, $args, $param );
     1144}
     1145
     1146/**
     1147 * Sanitize a request argument based on details registered to the route.
     1148 *
     1149 * @since 4.7.0
     1150 *
     1151 * @param mixed           $value
     1152 * @param WP_REST_Request $request
     1153 * @param string          $param
     1154 * @return mixed
     1155 */
     1156function rest_sanitize_request_arg( $value, $request, $param ) {
     1157    $attributes = $request->get_attributes();
     1158    if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
     1159        return $value;
     1160    }
     1161    $args = $attributes['args'][ $param ];
     1162
     1163    return rest_sanitize_value_from_schema( $value, $args, $param );
     1164}
     1165
     1166/**
     1167 * Parse a request argument based on details registered to the route.
     1168 *
     1169 * Runs a validation check and sanitizes the value, primarily to be used via
     1170 * the `sanitize_callback` arguments in the endpoint args registration.
     1171 *
     1172 * @since 4.7.0
     1173 *
     1174 * @param mixed           $value
     1175 * @param WP_REST_Request $request
     1176 * @param string          $param
     1177 * @return mixed
     1178 */
     1179function rest_parse_request_arg( $value, $request, $param ) {
     1180    $is_valid = rest_validate_request_arg( $value, $request, $param );
     1181
     1182    if ( is_wp_error( $is_valid ) ) {
     1183        return $is_valid;
     1184    }
     1185
     1186    $value = rest_sanitize_request_arg( $value, $request, $param );
     1187
     1188    return $value;
     1189}
     1190
     1191/**
     1192 * Determines if an IP address is valid.
     1193 *
     1194 * Handles both IPv4 and IPv6 addresses.
     1195 *
     1196 * @since 4.7.0
     1197 *
     1198 * @param string $ip IP address.
     1199 * @return string|false The valid IP address, otherwise false.
     1200 */
     1201function rest_is_ip_address( $ip ) {
     1202    $ipv4_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]?)$/';
     1203
     1204    if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
     1205        return false;
     1206    }
     1207
     1208    return $ip;
     1209}
     1210
     1211/**
     1212 * Changes a boolean-like value into the proper boolean value.
     1213 *
     1214 * @since 4.7.0
     1215 *
     1216 * @param bool|string|int $value The value being evaluated.
     1217 * @return boolean Returns the proper associated boolean value.
     1218 */
     1219function rest_sanitize_boolean( $value ) {
     1220    // String values are translated to `true`; make sure 'false' is false.
     1221    if ( is_string( $value ) ) {
     1222        $value = strtolower( $value );
     1223        if ( in_array( $value, array( 'false', '0' ), true ) ) {
     1224            $value = false;
     1225        }
     1226    }
     1227
     1228    // Everything else will map nicely to boolean.
     1229    return (bool) $value;
     1230}
     1231
     1232/**
     1233 * Determines if a given value is boolean-like.
     1234 *
     1235 * @since 4.7.0
     1236 *
     1237 * @param bool|string $maybe_bool The value being evaluated.
     1238 * @return boolean True if a boolean, otherwise false.
     1239 */
     1240function rest_is_boolean( $maybe_bool ) {
     1241    if ( is_bool( $maybe_bool ) ) {
     1242        return true;
     1243    }
     1244
     1245    if ( is_string( $maybe_bool ) ) {
     1246        $maybe_bool = strtolower( $maybe_bool );
     1247
     1248        $valid_boolean_values = array(
     1249            'false',
     1250            'true',
     1251            '0',
     1252            '1',
     1253        );
     1254
     1255        return in_array( $maybe_bool, $valid_boolean_values, true );
     1256    }
     1257
     1258    if ( is_int( $maybe_bool ) ) {
     1259        return in_array( $maybe_bool, array( 0, 1 ), true );
     1260    }
     1261
     1262    return false;
     1263}
     1264
     1265/**
     1266 * Determines if a given value is integer-like.
     1267 *
     1268 * @since 5.5.0
     1269 *
     1270 * @param mixed $maybe_integer The value being evaluated.
     1271 * @return bool True if an integer, otherwise false.
     1272 */
     1273function rest_is_integer( $maybe_integer ) {
     1274    return round( floatval( $maybe_integer ) ) === floatval( $maybe_integer );
     1275}
     1276
     1277/**
     1278 * Determines if a given value is array-like.
     1279 *
     1280 * @since 5.5.0
     1281 *
     1282 * @param mixed $maybe_array The value being evaluated.
     1283 * @return bool
     1284 */
     1285function rest_is_array( $maybe_array ) {
     1286    if ( is_scalar( $maybe_array ) ) {
     1287        $maybe_array = wp_parse_list( $maybe_array );
     1288    }
     1289
     1290    return wp_is_numeric_array( $maybe_array );
     1291}
     1292
     1293/**
     1294 * Converts an array-like value to an array.
     1295 *
     1296 * @since 5.5.0
     1297 *
     1298 * @param mixed $maybe_array The value being evaluated.
     1299 * @return array Returns the array extracted from the value.
     1300 */
     1301function rest_sanitize_array( $maybe_array ) {
     1302    if ( is_scalar( $maybe_array ) ) {
     1303        return wp_parse_list( $maybe_array );
     1304    }
     1305
     1306    if ( ! is_array( $maybe_array ) ) {
     1307        return array();
     1308    }
     1309
     1310    // Normalize to numeric array so nothing unexpected is in the keys.
     1311    return array_values( $maybe_array );
     1312}
     1313
     1314/**
     1315 * Determines if a given value is object-like.
     1316 *
     1317 * @since 5.5.0
     1318 *
     1319 * @param mixed $maybe_object The value being evaluated.
     1320 * @return bool True if object like, otherwise false.
     1321 */
     1322function rest_is_object( $maybe_object ) {
     1323    if ( '' === $maybe_object ) {
     1324        return true;
     1325    }
     1326
     1327    if ( $maybe_object instanceof stdClass ) {
     1328        return true;
     1329    }
     1330
     1331    if ( $maybe_object instanceof JsonSerializable ) {
     1332        $maybe_object = $maybe_object->jsonSerialize();
     1333    }
     1334
     1335    return is_array( $maybe_object );
     1336}
     1337
     1338/**
     1339 * Converts an object-like value to an object.
     1340 *
     1341 * @since 5.5.0
     1342 *
     1343 * @param mixed $maybe_object The value being evaluated.
     1344 * @return array Returns the object extracted from the value.
     1345 */
     1346function rest_sanitize_object( $maybe_object ) {
     1347    if ( '' === $maybe_object ) {
     1348        return array();
     1349    }
     1350
     1351    if ( $maybe_object instanceof stdClass ) {
     1352        return (array) $maybe_object;
     1353    }
     1354
     1355    if ( $maybe_object instanceof JsonSerializable ) {
     1356        $maybe_object = $maybe_object->jsonSerialize();
     1357    }
     1358
     1359    if ( ! is_array( $maybe_object ) ) {
     1360        return array();
     1361    }
     1362
     1363    return $maybe_object;
     1364}
     1365
     1366/**
     1367 * Gets the best type for a value.
     1368 *
     1369 * @since 5.5.0
     1370 *
     1371 * @param mixed $value The value to check.
     1372 * @param array $types The list of possible types.
     1373 * @return string The best matching type, an empty string if no types match.
     1374 */
     1375function rest_get_best_type_for_value( $value, $types ) {
     1376    static $checks = array(
     1377        'array'   => 'rest_is_array',
     1378        'object'  => 'rest_is_object',
     1379        'integer' => 'rest_is_integer',
     1380        'number'  => 'is_numeric',
     1381        'boolean' => 'rest_is_boolean',
     1382        'string'  => 'is_string',
     1383        'null'    => 'is_null',
     1384    );
     1385
     1386    // Both arrays and objects allow empty strings to be converted to their types.
     1387    // But the best answer for this type is a string.
     1388    if ( '' === $value && in_array( 'string', $types, true ) ) {
     1389        return 'string';
     1390    }
     1391
     1392    foreach ( $types as $type ) {
     1393        if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) {
     1394            return $type;
     1395        }
     1396    }
     1397
     1398    return '';
     1399}
     1400
     1401/**
     1402 * Handles getting the best type for a multi-type schema.
     1403 *
     1404 * This is a wrapper for {@see rest_get_best_type_for_value()} that handles
     1405 * backward compatibility for schemas that use invalid types.
     1406 *
     1407 * @since 5.5.0
     1408 *
     1409 * @param mixed  $value The value to check.
     1410 * @param array  $args  The schema array to use.
     1411 * @param string $param The parameter name, used in error messages.
     1412 * @return string
     1413 */
     1414function rest_handle_multi_type_schema( $value, $args, $param = '' ) {
     1415    $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
     1416    $invalid_types = array_diff( $args['type'], $allowed_types );
     1417
     1418    if ( $invalid_types ) {
     1419        _doing_it_wrong(
     1420            __FUNCTION__,
     1421            /* translators: 1. Parameter. 2. List of allowed types. */
     1422            wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ),
     1423            '5.5.0'
     1424        );
     1425    }
     1426
     1427    $best_type = rest_get_best_type_for_value( $value, $args['type'] );
     1428
     1429    if ( ! $best_type ) {
     1430        if ( ! $invalid_types ) {
     1431            return '';
     1432        }
     1433
     1434        // Backward compatibility for previous behavior which allowed the value if there was an invalid type used.
     1435        $best_type = reset( $invalid_types );
     1436    }
     1437
     1438    return $best_type;
     1439}
     1440
     1441/**
    12661442 * Validate a value based on a schema.
    12671443 *
     
    12851461
    12861462    if ( ! isset( $args['type'] ) ) {
    1287         _doing_it_wrong( __FUNCTION__, __( 'The "type" schema keyword is required.' ), '5.5.0' );
     1463        /* translators: 1. Parameter */
     1464        _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
    12881465    }
    12891466
    12901467    if ( is_array( $args['type'] ) ) {
    1291         foreach ( $args['type'] as $type ) {
    1292             $type_args         = $args;
    1293             $type_args['type'] = $type;
    1294 
    1295             if ( true === rest_validate_value_from_schema( $value, $type_args, $param ) ) {
    1296                 return true;
    1297             }
    1298         }
    1299 
    1300         /* translators: 1: Parameter, 2: List of types. */
    1301         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) );
     1468        $best_type = rest_handle_multi_type_schema( $value, $args, $param );
     1469
     1470        if ( ! $best_type ) {
     1471            /* translators: 1: Parameter, 2: List of types. */
     1472            return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) );
     1473        }
     1474
     1475        $args['type'] = $best_type;
    13021476    }
    13031477
     
    13051479        _doing_it_wrong(
    13061480            __FUNCTION__,
    1307             /* translators: 1. The list of allowed types. */
    1308             wp_sprintf( __( 'The "type" schema keyword can only be on of the built-in types: %l.' ), $allowed_types ),
     1481            /* translators: 1. Parameter 2. The list of allowed types. */
     1482            wp_sprintf( __( 'The "type" schema keyword for %1$s can only be on of the built-in types: %2$l.' ), $param, $allowed_types ),
    13091483            '5.5.0'
    13101484        );
     
    13121486
    13131487    if ( 'array' === $args['type'] ) {
    1314         if ( ! is_null( $value ) ) {
    1315             $value = wp_parse_list( $value );
    1316         }
    1317 
    1318         if ( ! wp_is_numeric_array( $value ) ) {
     1488        if ( ! rest_is_array( $value ) ) {
    13191489            /* translators: 1: Parameter, 2: Type name. */
    13201490            return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
    13211491        }
     1492
     1493        $value = rest_sanitize_array( $value );
    13221494
    13231495        foreach ( $value as $index => $v ) {
     
    13401512
    13411513    if ( 'object' === $args['type'] ) {
    1342         if ( '' === $value ) {
    1343             $value = array();
    1344         }
    1345 
    1346         if ( $value instanceof stdClass ) {
    1347             $value = (array) $value;
    1348         }
    1349 
    1350         if ( $value instanceof JsonSerializable ) {
    1351             $value = $value->jsonSerialize();
    1352         }
    1353 
    1354         if ( ! is_array( $value ) ) {
     1514        if ( ! rest_is_object( $value ) ) {
    13551515            /* translators: 1: Parameter, 2: Type name. */
    13561516            return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ) );
    13571517        }
     1518
     1519        $value = rest_sanitize_object( $value );
    13581520
    13591521        if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4
     
    14161578    }
    14171579
    1418     if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) {
     1580    if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) {
    14191581        /* translators: 1: Parameter, 2: Type name. */
    14201582        return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
     
    15521714 *
    15531715 * @since 4.7.0
    1554  *
    1555  * @param mixed $value The value to sanitize.
    1556  * @param array $args  Schema array to use for sanitization.
     1716 * @since 5.5.0 Added the `$param` parameter.
     1717 *
     1718 * @param mixed  $value The value to sanitize.
     1719 * @param array  $args  Schema array to use for sanitization.
     1720 * @param string $param The parameter name, used in error messages.
    15571721 * @return true|WP_Error
    15581722 */
    1559 function rest_sanitize_value_from_schema( $value, $args ) {
     1723function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
    15601724    $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
    15611725
    15621726    if ( ! isset( $args['type'] ) ) {
    1563         _doing_it_wrong( __FUNCTION__, __( 'The "type" schema keyword is required.' ), '5.5.0' );
     1727        /* translators: 1. Parameter */
     1728        _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
    15641729    }
    15651730
    15661731    if ( is_array( $args['type'] ) ) {
    1567         // Determine which type the value was validated against,
    1568         // and use that type when performing sanitization.
    1569         $validated_type = '';
    1570 
    1571         foreach ( $args['type'] as $type ) {
    1572             $type_args         = $args;
    1573             $type_args['type'] = $type;
    1574 
    1575             if ( ! is_wp_error( rest_validate_value_from_schema( $value, $type_args ) ) ) {
    1576                 $validated_type = $type;
    1577                 break;
    1578             }
    1579         }
    1580 
    1581         if ( ! $validated_type ) {
     1732        $best_type = rest_handle_multi_type_schema( $value, $args, $param );
     1733
     1734        if ( ! $best_type ) {
    15821735            return null;
    15831736        }
    15841737
    1585         $args['type'] = $validated_type;
     1738        $args['type'] = $best_type;
    15861739    }
    15871740
     
    15891742        _doing_it_wrong(
    15901743            __FUNCTION__,
    1591             /* translators: 1. The list of allowed types. */
    1592             wp_sprintf( __( 'The "type" schema keyword can only be on of the built-in types: %l.' ), $allowed_types ),
     1744            /* translators: 1. Parameter. 2. The list of allowed types. */
     1745            wp_sprintf( __( 'The "type" schema keyword for %1$s can only be on of the built-in types: %2$l.' ), $param, $allowed_types ),
    15931746            '5.5.0'
    15941747        );
     
    15961749
    15971750    if ( 'array' === $args['type'] ) {
     1751        $value = rest_sanitize_array( $value );
     1752
    15981753        if ( empty( $args['items'] ) ) {
    1599             return (array) $value;
    1600         }
    1601 
    1602         $value = wp_parse_list( $value );
     1754            return $value;
     1755        }
     1756
    16031757        foreach ( $value as $index => $v ) {
    1604             $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] );
    1605         }
    1606 
    1607         // Normalize to numeric array so nothing unexpected is in the keys.
    1608         $value = array_values( $value );
     1758            $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
     1759        }
     1760
    16091761        return $value;
    16101762    }
    16111763
    16121764    if ( 'object' === $args['type'] ) {
    1613         if ( $value instanceof stdClass ) {
    1614             $value = (array) $value;
    1615         }
    1616 
    1617         if ( $value instanceof JsonSerializable ) {
    1618             $value = $value->jsonSerialize();
    1619         }
    1620 
    1621         if ( ! is_array( $value ) ) {
    1622             return array();
    1623         }
     1765        $value = rest_sanitize_object( $value );
    16241766
    16251767        foreach ( $value as $property => $v ) {
    16261768            if ( isset( $args['properties'][ $property ] ) ) {
    1627                 $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ] );
     1769                $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
    16281770            } elseif ( isset( $args['additionalProperties'] ) ) {
    16291771                if ( false === $args['additionalProperties'] ) {
    16301772                    unset( $value[ $property ] );
    16311773                } elseif ( is_array( $args['additionalProperties'] ) ) {
    1632                     $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'] );
     1774                    $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
    16331775                }
    16341776            }
Note: See TracChangeset for help on using the changeset viewer.