Make WordPress Core

Changeset 48306


Ignore:
Timestamp:
07/05/2020 12:13:37 AM (4 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.

Location:
trunk
Files:
4 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            }
  • trunk/tests/phpunit/tests/rest-api.php

    r48273 r48306  
    99require_once ABSPATH . 'wp-admin/includes/admin.php';
    1010require_once ABSPATH . WPINC . '/rest-api.php';
     11require_once __DIR__ . '/../includes/class-jsonserializable-object.php';
    1112
    1213/**
     
    14581459        $this->assertEquals( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term->term_id ) );
    14591460    }
     1461
     1462    /**
     1463     * @dataProvider _dp_rest_is_object
     1464     * @ticket 50300
     1465     * @param bool  $expected Expected result of the check.
     1466     * @param mixed $value    The value to check.
     1467     */
     1468    public function test_rest_is_object( $expected, $value ) {
     1469        $is_object = rest_is_object( $value );
     1470
     1471        if ( $expected ) {
     1472            $this->assertTrue( $is_object );
     1473        } else {
     1474            $this->assertFalse( $is_object );
     1475        }
     1476    }
     1477
     1478    public function _dp_rest_is_object() {
     1479        return array(
     1480            array(
     1481                true,
     1482                '',
     1483            ),
     1484            array(
     1485                true,
     1486                new stdClass(),
     1487            ),
     1488            array(
     1489                true,
     1490                new JsonSerializable_Object( array( 'hi' => 'there' ) ),
     1491            ),
     1492            array(
     1493                true,
     1494                array( 'hi' => 'there' ),
     1495            ),
     1496            array(
     1497                true,
     1498                array(),
     1499            ),
     1500            array(
     1501                true,
     1502                array( 'a', 'b' ),
     1503            ),
     1504            array(
     1505                false,
     1506                new Basic_Object(),
     1507            ),
     1508            array(
     1509                false,
     1510                new JsonSerializable_Object( 'str' ),
     1511            ),
     1512            array(
     1513                false,
     1514                'str',
     1515            ),
     1516            array(
     1517                false,
     1518                5,
     1519            ),
     1520        );
     1521    }
     1522
     1523    /**
     1524     * @dataProvider _dp_rest_sanitize_object
     1525     * @ticket       50300
     1526     * @param array $expected Expected sanitized version.
     1527     * @param mixed $value    The value to sanitize.
     1528     */
     1529    public function test_rest_sanitize_object( $expected, $value ) {
     1530        $sanitized = rest_sanitize_object( $value );
     1531        $this->assertEquals( $expected, $sanitized );
     1532    }
     1533
     1534    public function _dp_rest_sanitize_object() {
     1535        return array(
     1536            array(
     1537                array(),
     1538                '',
     1539            ),
     1540            array(
     1541                array( 'a' => '1' ),
     1542                (object) array( 'a' => '1' ),
     1543            ),
     1544            array(
     1545                array( 'hi' => 'there' ),
     1546                new JsonSerializable_Object( array( 'hi' => 'there' ) ),
     1547            ),
     1548            array(
     1549                array( 'hi' => 'there' ),
     1550                array( 'hi' => 'there' ),
     1551            ),
     1552            array(
     1553                array(),
     1554                array(),
     1555            ),
     1556            array(
     1557                array( 'a', 'b' ),
     1558                array( 'a', 'b' ),
     1559            ),
     1560            array(
     1561                array(),
     1562                new Basic_Object(),
     1563            ),
     1564            array(
     1565                array(),
     1566                new JsonSerializable_Object( 'str' ),
     1567            ),
     1568            array(
     1569                array(),
     1570                'str',
     1571            ),
     1572            array(
     1573                array(),
     1574                5,
     1575            ),
     1576        );
     1577    }
     1578
     1579    /**
     1580     * @dataProvider _dp_rest_is_array
     1581     * @ticket 50300
     1582     * @param bool  $expected Expected result of the check.
     1583     * @param mixed $value    The value to check.
     1584     */
     1585    public function test_rest_is_array( $expected, $value ) {
     1586        $is_array = rest_is_array( $value );
     1587
     1588        if ( $expected ) {
     1589            $this->assertTrue( $is_array );
     1590        } else {
     1591            $this->assertFalse( $is_array );
     1592        }
     1593    }
     1594
     1595    public function _dp_rest_is_array() {
     1596        return array(
     1597            array(
     1598                true,
     1599                '',
     1600            ),
     1601            array(
     1602                true,
     1603                array( 'a', 'b' ),
     1604            ),
     1605            array(
     1606                true,
     1607                array(),
     1608            ),
     1609            array(
     1610                true,
     1611                'a,b,c',
     1612            ),
     1613            array(
     1614                true,
     1615                'a',
     1616            ),
     1617            array(
     1618                true,
     1619                5,
     1620            ),
     1621            array(
     1622                false,
     1623                new stdClass(),
     1624            ),
     1625            array(
     1626                false,
     1627                new JsonSerializable_Object( array( 'hi' => 'there' ) ),
     1628            ),
     1629            array(
     1630                false,
     1631                array( 'hi' => 'there' ),
     1632            ),
     1633            array(
     1634                false,
     1635                new Basic_Object(),
     1636            ),
     1637            array(
     1638                false,
     1639                new JsonSerializable_Object( 'str' ),
     1640            ),
     1641            array(
     1642                false,
     1643                null,
     1644            ),
     1645        );
     1646    }
     1647
     1648    /**
     1649     * @dataProvider _dp_rest_sanitize_array
     1650     * @ticket       50300
     1651     * @param array $expected Expected sanitized version.
     1652     * @param mixed $value    The value to sanitize.
     1653     */
     1654    public function test_rest_sanitize_array( $expected, $value ) {
     1655        $sanitized = rest_sanitize_array( $value );
     1656        $this->assertEquals( $expected, $sanitized );
     1657    }
     1658
     1659    public function _dp_rest_sanitize_array() {
     1660        return array(
     1661            array(
     1662                array(),
     1663                '',
     1664            ),
     1665            array(
     1666                array( 'a', 'b' ),
     1667                array( 'a', 'b' ),
     1668            ),
     1669            array(
     1670                array(),
     1671                array(),
     1672            ),
     1673            array(
     1674                array( 'a', 'b', 'c' ),
     1675                'a,b,c',
     1676            ),
     1677            array(
     1678                array( 'a' ),
     1679                'a',
     1680            ),
     1681            array(
     1682                array( 'a', 'b' ),
     1683                'a,b,',
     1684            ),
     1685            array(
     1686                array( '5' ),
     1687                5,
     1688            ),
     1689            array(
     1690                array(),
     1691                new stdClass(),
     1692            ),
     1693            array(
     1694                array(),
     1695                new JsonSerializable_Object( array( 'hi' => 'there' ) ),
     1696            ),
     1697            array(
     1698                array( 'there' ),
     1699                array( 'hi' => 'there' ),
     1700            ),
     1701            array(
     1702                array(),
     1703                new Basic_Object(),
     1704            ),
     1705            array(
     1706                array(),
     1707                new JsonSerializable_Object( 'str' ),
     1708            ),
     1709            array(
     1710                array(),
     1711                null,
     1712            ),
     1713        );
     1714    }
     1715
     1716    /**
     1717     * @dataProvider _dp_get_best_type_for_value
     1718     * @ticket 50300
     1719     * @param string $expected The expected best type.
     1720     * @param mixed  $value    The value to test.
     1721     * @param array  $types    The list of available types.
     1722     */
     1723    public function test_get_best_type_for_value( $expected, $value, $types ) {
     1724        $this->assertEquals( $expected, rest_get_best_type_for_value( $value, $types ) );
     1725    }
     1726
     1727    public function _dp_get_best_type_for_value() {
     1728        return array(
     1729            array(
     1730                'array',
     1731                array( 'hi' ),
     1732                array( 'array' ),
     1733            ),
     1734            array(
     1735                'object',
     1736                array( 'hi' => 'there' ),
     1737                array( 'object' ),
     1738            ),
     1739            array(
     1740                'integer',
     1741                5,
     1742                array( 'integer' ),
     1743            ),
     1744            array(
     1745                'number',
     1746                4.0,
     1747                array( 'number' ),
     1748            ),
     1749            array(
     1750                'boolean',
     1751                true,
     1752                array( 'boolean' ),
     1753            ),
     1754            array(
     1755                'string',
     1756                'str',
     1757                array( 'string' ),
     1758            ),
     1759            array(
     1760                'null',
     1761                null,
     1762                array( 'null' ),
     1763            ),
     1764            array(
     1765                'string',
     1766                '',
     1767                array( 'array', 'string' ),
     1768            ),
     1769            array(
     1770                'string',
     1771                '',
     1772                array( 'object', 'string' ),
     1773            ),
     1774            array(
     1775                'string',
     1776                'Hello',
     1777                array( 'object', 'string' ),
     1778            ),
     1779            array(
     1780                'object',
     1781                array( 'hello' => 'world' ),
     1782                array( 'object', 'string' ),
     1783            ),
     1784            array(
     1785                'number',
     1786                '5.0',
     1787                array( 'number', 'string' ),
     1788            ),
     1789            array(
     1790                'string',
     1791                '5.0',
     1792                array( 'string', 'number' ),
     1793            ),
     1794            array(
     1795                'boolean',
     1796                'false',
     1797                array( 'boolean', 'string' ),
     1798            ),
     1799            array(
     1800                'string',
     1801                'false',
     1802                array( 'string', 'boolean' ),
     1803            ),
     1804            array(
     1805                'string',
     1806                'a,b',
     1807                array( 'string', 'array' ),
     1808            ),
     1809            array(
     1810                'array',
     1811                'a,b',
     1812                array( 'array', 'string' ),
     1813            ),
     1814        );
     1815    }
    14601816}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-sanitization.php

    r48300 r48306  
    342342        $this->assertNull( rest_sanitize_value_from_schema( null, $schema ) );
    343343        $this->assertEquals( '2019-09-19T18:00:00', rest_sanitize_value_from_schema( '2019-09-19T18:00:00', $schema ) );
    344         $this->assertNull( rest_sanitize_value_from_schema( 'lalala', $schema ) );
     344        $this->assertEquals( 'lalala', rest_sanitize_value_from_schema( 'lalala', $schema ) );
    345345    }
    346346
     
    395395        $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) );
    396396        $this->assertEquals( array( 'raw' => 'My Value' ), rest_sanitize_value_from_schema( array( 'raw' => 'My Value' ), $schema ) );
    397         $this->assertNull( rest_sanitize_value_from_schema( array( 'raw' => 1 ), $schema ) );
     397        $this->assertEquals( array( 'raw' => '1' ), rest_sanitize_value_from_schema( array( 'raw' => 1 ), $schema ) );
    398398    }
    399399
     
    424424        $this->assertEquals( array( 'raw' => false ), rest_sanitize_value_from_schema( array( 'raw' => 0 ), $schema ) );
    425425
    426         $this->assertNull( rest_sanitize_value_from_schema( array( 'raw' => 'something non boolean' ), $schema ) );
     426        $this->assertEquals( array( 'raw' => true ), rest_sanitize_value_from_schema( array( 'raw' => 'something non boolean' ), $schema ) );
     427    }
     428
     429    /**
     430     * @ticket 50300
     431     */
     432    public function test_multi_type_with_no_known_types() {
     433        $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' );
     434        $this->setExpectedIncorrectUsage( 'rest_sanitize_value_from_schema' );
     435
     436        $schema = array(
     437            'type' => array( 'invalid', 'type' ),
     438        );
     439
     440        $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) );
     441    }
     442
     443    /**
     444     * @ticket 50300
     445     */
     446    public function test_multi_type_with_some_unknown_types() {
     447        $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' );
     448        $this->setExpectedIncorrectUsage( 'rest_sanitize_value_from_schema' );
     449
     450        $schema = array(
     451            'type' => array( 'object', 'type' ),
     452        );
     453
     454        $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) );
     455    }
     456
     457    /**
     458     * @ticket 50300
     459     */
     460    public function test_multi_type_returns_null_if_no_valid_type() {
     461        $schema = array(
     462            'type' => array( 'number', 'string' ),
     463        );
     464
     465        $this->assertNull( rest_sanitize_value_from_schema( array( 'Hello!' ), $schema ) );
    427466    }
    428467}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php

    r48300 r48306  
    388388        $this->assertTrue( rest_validate_value_from_schema( null, $schema ) );
    389389        $this->assertTrue( rest_validate_value_from_schema( '2019-09-19T18:00:00', $schema ) );
    390         $this->assertWPError( rest_validate_value_from_schema( 'some random string', $schema ) );
     390
     391        $error = rest_validate_value_from_schema( 'some random string', $schema );
     392        $this->assertWPError( $error );
     393        $this->assertEquals( 'Invalid date.', $error->get_error_message() );
    391394    }
    392395
     
    403406        $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) );
    404407        $this->assertTrue( rest_validate_value_from_schema( array( 'raw' => 'My Value' ), $schema ) );
    405         $this->assertWPError( rest_validate_value_from_schema( array( 'raw' => array( 'a list' ) ), $schema ) );
     408
     409        $error = rest_validate_value_from_schema( array( 'raw' => array( 'a list' ) ), $schema );
     410        $this->assertWPError( $error );
     411        $this->assertEquals( '[raw] is not of type string.', $error->get_error_message() );
     412    }
     413
     414    /**
     415     * @ticket 50300
     416     */
     417    public function test_null_or_integer() {
     418        $schema = array(
     419            'type'    => array( 'null', 'integer' ),
     420            'minimum' => 10,
     421            'maximum' => 20,
     422        );
     423
     424        $this->assertTrue( rest_validate_value_from_schema( null, $schema ) );
     425        $this->assertTrue( rest_validate_value_from_schema( 15, $schema ) );
     426        $this->assertTrue( rest_validate_value_from_schema( '15', $schema ) );
     427
     428        $error = rest_validate_value_from_schema( 30, $schema, 'param' );
     429        $this->assertWPError( $error );
     430        $this->assertEquals( 'param must be between 10 (inclusive) and 20 (inclusive)', $error->get_error_message() );
     431    }
     432
     433    /**
     434     * @ticket 50300
     435     */
     436    public function test_multi_type_with_no_known_types() {
     437        $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' );
     438        $this->setExpectedIncorrectUsage( 'rest_validate_value_from_schema' );
     439
     440        $schema = array(
     441            'type' => array( 'invalid', 'type' ),
     442        );
     443
     444        $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) );
     445    }
     446
     447    /**
     448     * @ticket 50300
     449     */
     450    public function test_multi_type_with_some_unknown_types() {
     451        $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' );
     452        $this->setExpectedIncorrectUsage( 'rest_validate_value_from_schema' );
     453
     454        $schema = array(
     455            'type' => array( 'object', 'type' ),
     456        );
     457
     458        $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) );
    406459    }
    407460
Note: See TracChangeset for help on using the changeset viewer.