Make WordPress Core

Changeset 48306


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