rn wp_is_numeric_array( $maybe_array ); } /** * Converts an array-like value to an array. * * @since 5.5.0 * * @param mixed $maybe_array The value being evaluated. * @return array Returns the array extracted from the value. */ function rest_sanitize_array( $maybe_array ) { if ( is_scalar( $maybe_array ) ) { return wp_parse_list( $maybe_array ); } if ( ! is_array( $maybe_array ) ) { return array(); } // Normalize to numeric array so nothing unexpected is in the keys. return array_values( $maybe_array ); } /** * Determines if a given value is object-like. * * @since 5.5.0 * * @param mixed $maybe_object The value being evaluated. * @return bool True if object like, otherwise false. */ function rest_is_object( $maybe_object ) { if ( '' === $maybe_object ) { return true; } if ( $maybe_object instanceof stdClass ) { return true; } if ( $maybe_object instanceof JsonSerializable ) { $maybe_object = $maybe_object->jsonSerialize(); } return is_array( $maybe_object ); } /** * Converts an object-like value to an array. * * @since 5.5.0 * * @param mixed $maybe_object The value being evaluated. * @return array Returns the object extracted from the value as an associative array. */ function rest_sanitize_object( $maybe_object ) { if ( '' === $maybe_object ) { return array(); } if ( $maybe_object instanceof stdClass ) { return (array) $maybe_object; } if ( $maybe_object instanceof JsonSerializable ) { $maybe_object = $maybe_object->jsonSerialize(); } if ( ! is_array( $maybe_object ) ) { return array(); } return $maybe_object; } /** * Gets the best type for a value. * * @since 5.5.0 * * @param mixed $value The value to check. * @param string[] $types The list of possible types. * @return string The best matching type, an empty string if no types match. */ function rest_get_best_type_for_value( $value, $types ) { static $checks = array( 'array' => 'rest_is_array', 'object' => 'rest_is_object', 'integer' => 'rest_is_integer', 'number' => 'is_numeric', 'boolean' => 'rest_is_boolean', 'string' => 'is_string', 'null' => 'is_null', ); /* * Both arrays and objects allow empty strings to be converted to their types. * But the best answer for this type is a string. */ if ( '' === $value && in_array( 'string', $types, true ) ) { return 'string'; } foreach ( $types as $type ) { if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) { return $type; } } return ''; } /** * Handles getting the best type for a multi-type schema. * * This is a wrapper for {@see rest_get_best_type_for_value()} that handles * backward compatibility for schemas that use invalid types. * * @since 5.5.0 * * @param mixed $value The value to check. * @param array $args The schema array to use. * @param string $param The parameter name, used in error messages. * @return string */ function rest_handle_multi_type_schema( $value, $args, $param = '' ) { $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); $invalid_types = array_diff( $args['type'], $allowed_types ); if ( $invalid_types ) { _doing_it_wrong( __FUNCTION__, /* translators: 1: Parameter, 2: List of allowed types. */ wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); } $best_type = rest_get_best_type_for_value( $value, $args['type'] ); if ( ! $best_type ) { if ( ! $invalid_types ) { return ''; } // Backward compatibility for previous behavior which allowed the value if there was an invalid type used. $best_type = reset( $invalid_types ); } return $best_type; } /** * Checks if an array is made up of unique items. * * @since 5.5.0 * * @param array $input_array The array to check. * @return bool True if the array contains unique items, false otherwise. */ function rest_validate_array_contains_unique_items( $input_array ) { $seen = array(); foreach ( $input_array as $item ) { $stabilized = rest_stabilize_value( $item ); $key = serialize( $stabilized ); if ( ! isset( $seen[ $key ] ) ) { $seen[ $key ] = true; continue; } return false; } return true; } /** * Stabilizes a value following JSON Schema semantics. * * For lists, order is preserved. For objects, properties are reordered alphabetically. * * @since 5.5.0 * * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays. * @return mixed The stabilized value. */ function rest_stabilize_value( $value ) { if ( is_scalar( $value ) || is_null( $value ) ) { return $value; } if ( is_object( $value ) ) { _doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' ); return $value; } ksort( $value ); foreach ( $value as $k => $v ) { $value[ $k ] = rest_stabilize_value( $v ); } return $value; } /** * Validates if the JSON Schema pattern matches a value. * * @since 5.6.0 * * @param string $pattern The pattern to match against. * @param string $value The value to check. * @return bool True if the pattern matches the given value, false otherwise. */ function rest_validate_json_schema_pattern( $pattern, $value ) { $escaped_pattern = str_replace( '#', '\\#', $pattern ); return 1 === preg_match( '#' . $escaped_pattern . '#u', $value ); } /** * Finds the schema for a property using the patternProperties keyword. * * @since 5.6.0 * * @param string $property The property name to check. * @param array $args The schema array to use. * @return array|null The schema of matching pattern property, or null if no patterns match. */ function rest_find_matching_pattern_property_schema( $property, $args ) { if ( isset( $args['patternProperties'] ) ) { foreach ( $args['patternProperties'] as $pattern => $child_schema ) { if ( rest_validate_json_schema_pattern( $pattern, $property ) ) { return $child_schema; } } } return null; } /** * Formats a combining operation error into a WP_Error object. * * @since 5.6.0 * * @param string $param The parameter name. * @param array $error The error details. * @return WP_Error */ function rest_format_combining_operation_error( $param, $error ) { $position = $error['index']; $reason = $error['error_object']->get_error_message(); if ( isset( $error['schema']['title'] ) ) { $title = $error['schema']['title']; return new WP_Error( 'rest_no_matching_schema', /* translators: 1: Parameter, 2: Schema title, 3: Reason. */ sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ), array( 'position' => $position ) ); } return new WP_Error( 'rest_no_matching_schema', /* translators: 1: Parameter, 2: Reason. */ sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ), array( 'position' => $position ) ); } /** * Gets the error of combining operation. * * @since 5.6.0 * * @param array $value The value to validate. * @param string $param The parameter name, used in error messages. * @param array $errors The errors array, to search for possible error. * @return WP_Error The combining operation error. */ function rest_get_combining_operation_error( $value, $param, $errors ) { // If there is only one error, simply return it. if ( 1 === count( $errors ) ) { return rest_format_combining_operation_error( $param, $errors[0] ); } // Filter out all errors related to type validation. $filtered_errors = array(); foreach ( $errors as $error ) { $error_code = $error['error_object']->get_error_code(); $error_data = $error['error_object']->get_error_data(); if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) { $filtered_errors[] = $error; } } // If there is only one error left, simply return it. if ( 1 === count( $filtered_errors ) ) { return rest_format_combining_operation_error( $param, $filtered_errors[0] ); } // If there are only errors related to object validation, try choosing the most appropriate one. if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) { $result = null; $number = 0; foreach ( $filtered_errors as $error ) { if ( isset( $error['schema']['properties'] ) ) { $n = count( array_intersect_key( $error['schema']['properties'], $value ) ); if ( $n > $number ) { $result = $error; $number = $n; } } } if ( null !== $result ) { return rest_format_combining_operation_error( $param, $result ); } } // If each schema has a title, include those titles in the error message. $schema_titles = array(); foreach ( $errors as $error ) { if ( isset( $error['schema']['title'] ) ) { $schema_titles[] = $error['schema']['title']; } } if ( count( $schema_titles ) === count( $errors ) ) { /* translators: 1: Parameter, 2: Schema titles. */ return new WP_Error( 'rest_no_matching_schema', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) ); } /* translators: %s: Parameter. */ return new WP_Error( 'rest_no_matching_schema', sprintf( __( '%s does not match any of the expected formats.' ), $param ) ); } /** * Finds the matching schema among the "anyOf" schemas. * * @since 5.6.0 * * @param mixed $value The value to validate. * @param array $args The schema array to use. * @param string $param The parameter name, used in error messages. * @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match. */ function rest_find_any_matching_schema( $value, $args, $param ) { $errors = array(); foreach ( $args['anyOf'] as $index => $schema ) { if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) { $schema['type'] = $args['type']; } $is_valid = rest_validate_value_from_schema( $value, $schema, $param ); if ( ! is_wp_error( $is_valid ) ) { return $schema; } $errors[] = array( 'error_object' => $is_valid, 'schema' => $schema, 'index' => $index, ); } return rest_get_combining_operation_error( $value, $param, $errors ); } /** * Finds the matching schema among the "oneOf" schemas. * * @since 5.6.0 * * @param mixed $value The value to validate. * @param array $args The schema array to use. * @param string $param The parameter name, used in error messages. * @param bool $stop_after_first_match Optional. Whether the process should stop after the first successful match. * @return array|WP_Error The matching schema or WP_Error instance if the number of matching schemas is not equal to one. */ function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) { $matching_schemas = array(); $errors = array(); foreach ( $args['oneOf'] as $index => $schema ) { if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) { $schema['type'] = $args['type']; } $is_valid = rest_validate_value_from_schema( $value, $schema, $param ); if ( ! is_wp_error( $is_valid ) ) { if ( $stop_after_first_match ) { return $schema; } $matching_schemas[] = array( 'schema_object' => $schema, 'index' => $index, ); } else { $errors[] = array( 'error_object' => $is_valid, 'schema' => $schema, 'index' => $index, ); } } if ( ! $matching_schemas ) { return rest_get_combining_operation_error( $value, $param, $errors ); } if ( count( $matching_schemas ) > 1 ) { $schema_positions = array(); $schema_titles = array(); foreach ( $matching_schemas as $schema ) { $schema_positions[] = $schema['index']; if ( isset( $schema['schema_object']['title'] ) ) { $schema_titles[] = $schema['schema_object']['title']; } } // If each schema has a title, include those titles in the error message. if ( count( $schema_titles ) === count( $matching_schemas ) ) { return new WP_Error( 'rest_one_of_multiple_matches', /* translators: 1: Parameter, 2: Schema titles. */ wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ), array( 'positions' => $schema_positions ) ); } return new WP_Error( 'rest_one_of_multiple_matches', /* translators: %s: Parameter. */ sprintf( __( '%s matches more than one of the expected formats.' ), $param ), array( 'positions' => $schema_positions ) ); } return $matching_schemas[0]['schema_object']; } /** * Checks the equality of two values, following JSON Schema semantics. * * Property order is ignored for objects. * * Values must have been previously sanitized/coerced to their native types. * * @since 5.7.0 * * @param mixed $value1 The first value to check. * @param mixed $value2 The second value to check. * @return bool True if the values are equal or false otherwise. */ function rest_are_values_equal( $value1, $value2 ) { if ( is_array( $value1 ) && is_array( $value2 ) ) { if ( count( $value1 ) !== count( $value2 ) ) { return false; } foreach ( $value1 as $index => $value ) { if ( ! array_key_exists( $index, $value2 ) || ! rest_are_values_equal( $value, $value2[ $index ] ) ) { return false; } } return true; } if ( is_int( $value1 ) && is_float( $value2 ) || is_float( $value1 ) && is_int( $value2 ) ) { return (float) $value1 === (float) $value2; } return $value1 === $value2; } /** * Validates that the given value is a member of the JSON Schema "enum". * * @since 5.7.0 * * @param mixed $value The value to validate. * @param array $args The schema array to use. * @param string $param The parameter name, used in error messages. * @return true|WP_Error True if the "enum" contains the value or a WP_Error instance otherwise. */ function rest_validate_enum( $value, $args, $param ) { $sanitized_value = rest_sanitize_value_from_schema( $value, $args, $param ); if ( is_wp_error( $sanitized_value ) ) { return $sanitized_value; } foreach ( $args['enum'] as $enum_value ) { if ( rest_are_values_equal( $sanitized_value, $enum_value ) ) { return true; } } $encoded_enum_values = array(); foreach ( $args['enum'] as $enum_value ) { $encoded_enum_values[] = is_scalar( $enum_value ) ? $enum_value : wp_json_encode( $enum_value ); } if ( count( $encoded_enum_values ) === 1 ) { /* translators: 1: Parameter, 2: Valid values. */ return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not %2$s.' ), $param, $encoded_enum_values[0] ) ); } /* translators: 1: Parameter, 2: List of valid values. */ return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not one of %2$l.' ), $param, $encoded_enum_values ) ); } /** * Get all valid JSON schema properties. * * @since 5.6.0 * * @return string[] All valid JSON schema properties. */ function rest_get_allowed_schema_keywords() { return array( 'title', 'description', 'default', 'type', 'format', 'enum', 'items', 'properties', 'additionalProperties', 'patternProperties', 'minProperties', 'maxProperties', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength', 'pattern', 'minItems', 'maxItems', 'uniqueItems', 'anyOf', 'oneOf', ); } /** * Validate a value based on a schema. * * @since 4.7.0 * @since 4.9.0 Support the "object" type. * @since 5.2.0 Support validating "additionalProperties" against a schema. * @since 5.3.0 Support multiple types. * @since 5.4.0 Convert an empty string to an empty object. * @since 5.5.0 Add the "uuid" and "hex-color" formats. * Support the "minLength", "maxLength" and "pattern" keywords for strings. * Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays. * Validate required properties. * @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects. * Support the "multipleOf" keyword for numbers and integers. * Support the "patternProperties" keyword for objects. * Support the "anyOf" and "oneOf" keywords. * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_value_from_schema( $value, $args, $param = '' ) { if ( isset( $args['anyOf'] ) ) { $matching_schema = rest_find_any_matching_schema( $value, $args, $param ); if ( is_wp_error( $matching_schema ) ) { return $matching_schema; } if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) { $args['type'] = $matching_schema['type']; } } if ( isset( $args['oneOf'] ) ) { $matching_schema = rest_find_one_matching_schema( $value, $args, $param ); if ( is_wp_error( $matching_schema ) ) { return $matching_schema; } if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) { $args['type'] = $matching_schema['type']; } } $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); if ( ! isset( $args['type'] ) ) { /* translators: %s: Parameter. */ _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); } if ( is_array( $args['type'] ) ) { $best_type = rest_handle_multi_type_schema( $value, $args, $param ); if ( ! $best_type ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: List of types. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ), array( 'param' => $param ) ); } $args['type'] = $best_type; } if ( ! in_array( $args['type'], $allowed_types, true ) ) { _doing_it_wrong( __FUNCTION__, /* translators: 1: Parameter, 2: The list of allowed types. */ wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); } switch ( $args['type'] ) { case 'null': $is_valid = rest_validate_null_value_from_schema( $value, $param ); break; case 'boolean': $is_valid = rest_validate_boolean_value_from_schema( $value, $param ); break; case 'object': $is_valid = rest_validate_object_value_from_schema( $value, $args, $param ); break; case 'array': $is_valid = rest_validate_array_value_from_schema( $value, $args, $param ); break; case 'number': $is_valid = rest_validate_number_value_from_schema( $value, $args, $param ); break; case 'string': $is_valid = rest_validate_string_value_from_schema( $value, $args, $param ); break; case 'integer': $is_valid = rest_validate_integer_value_from_schema( $value, $args, $param ); break; default: $is_valid = true; break; } if ( is_wp_error( $is_valid ) ) { return $is_valid; } if ( ! empty( $args['enum'] ) ) { $enum_contains_value = rest_validate_enum( $value, $args, $param ); if ( is_wp_error( $enum_contains_value ) ) { return $enum_contains_value; } } /* * The "format" keyword should only be applied to strings. However, for backward compatibility, * we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value. */ if ( isset( $args['format'] ) && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) ) ) { switch ( $args['format'] ) { case 'hex-color': if ( ! rest_parse_hex_color( $value ) ) { return new WP_Error( 'rest_invalid_hex_color', __( 'Invalid hex color.' ) ); } break; case 'date-time': if ( false === rest_parse_date( $value ) ) { return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) ); } break; case 'email': if ( ! is_email( $value ) ) { return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) ); } break; case 'ip': if ( ! rest_is_ip_address( $value ) ) { /* translators: %s: IP address. */ return new WP_Error( 'rest_invalid_ip', sprintf( __( '%s is not a valid IP address.' ), $param ) ); } break; case 'uuid': if ( ! wp_is_uuid( $value ) ) { /* translators: %s: The name of a JSON field expecting a valid UUID. */ return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) ); } break; } } return true; } /** * Validates a null value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_null_value_from_schema( $value, $param ) { if ( null !== $value ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ), array( 'param' => $param ) ); } return true; } /** * Validates a boolean value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_boolean_value_from_schema( $value, $param ) { if ( ! rest_is_boolean( $value ) ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ), array( 'param' => $param ) ); } return true; } /** * Validates an object value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_object_value_from_schema( $value, $args, $param ) { if ( ! rest_is_object( $value ) ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ), array( 'param' => $param ) ); } $value = rest_sanitize_object( $value ); if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4 foreach ( $args['required'] as $name ) { if ( ! array_key_exists( $name, $value ) ) { return new WP_Error( 'rest_property_required', /* translators: 1: Property of an object, 2: Parameter. */ sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) ); } } } elseif ( isset( $args['properties'] ) ) { // schema version 3 foreach ( $args['properties'] as $name => $property ) { if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) { return new WP_Error( 'rest_property_required', /* translators: 1: Property of an object, 2: Parameter. */ sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) ); } } } foreach ( $value as $property => $v ) { if ( isset( $args['properties'][ $property ] ) ) { $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); if ( is_wp_error( $is_valid ) ) { return $is_valid; } continue; } $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args ); if ( null !== $pattern_property_schema ) { $is_valid = rest_validate_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' ); if ( is_wp_error( $is_valid ) ) { return $is_valid; } continue; } if ( isset( $args['additionalProperties'] ) ) { if ( false === $args['additionalProperties'] ) { return new WP_Error( 'rest_additional_properties_forbidden', /* translators: %s: Property of an object. */ sprintf( __( '%1$s is not a valid property of Object.' ), $property ) ); } if ( is_array( $args['additionalProperties'] ) ) { $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); if ( is_wp_error( $is_valid ) ) { return $is_valid; } } } } if ( isset( $args['minProperties'] ) && count( $value ) < $args['minProperties'] ) { return new WP_Error( 'rest_too_few_properties', sprintf( /* translators: 1: Parameter, 2: Number. */ _n( '%1$s must contain at least %2$s property.', '%1$s must contain at least %2$s properties.', $args['minProperties'] ), $param, number_format_i18n( $args['minProperties'] ) ) ); } if ( isset( $args['maxProperties'] ) && count( $value ) > $args['maxProperties'] ) { return new WP_Error( 'rest_too_many_properties', sprintf( /* translators: 1: Parameter, 2: Number. */ _n( '%1$s must contain at most %2$s property.', '%1$s must contain at most %2$s properties.', $args['maxProperties'] ), $param, number_format_i18n( $args['maxProperties'] ) ) ); } return true; } /** * Validates an array value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_array_value_from_schema( $value, $args, $param ) { if ( ! rest_is_array( $value ) ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ), array( 'param' => $param ) ); } $value = rest_sanitize_array( $value ); if ( isset( $args['items'] ) ) { foreach ( $value as $index => $v ) { $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); if ( is_wp_error( $is_valid ) ) { return $is_valid; } } } if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) { return new WP_Error( 'rest_too_few_items', sprintf( /* translators: 1: Parameter, 2: Number. */ _n( '%1$s must contain at least %2$s item.', '%1$s must contain at least %2$s items.', $args['minItems'] ), $param, number_format_i18n( $args['minItems'] ) ) ); } if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) { return new WP_Error( 'rest_too_many_items', sprintf( /* translators: 1: Parameter, 2: Number. */ _n( '%1$s must contain at most %2$s item.', '%1$s must contain at most %2$s items.', $args['maxItems'] ), $param, number_format_i18n( $args['maxItems'] ) ) ); } if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { /* translators: %s: Parameter. */ return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) ); } return true; } /** * Validates a number value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_number_value_from_schema( $value, $args, $param ) { if ( ! is_numeric( $value ) ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ), array( 'param' => $param ) ); } if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) { return new WP_Error( 'rest_invalid_multiple', /* translators: 1: Parameter, 2: Multiplier. */ sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] ) ); } if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { return new WP_Error( 'rest_out_of_bounds', /* translators: 1: Parameter, 2: Minimum number. */ sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) ); } if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { return new WP_Error( 'rest_out_of_bounds', /* translators: 1: Parameter, 2: Minimum number. */ sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) ); } } if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { return new WP_Error( 'rest_out_of_bounds', /* translators: 1: Parameter, 2: Maximum number. */ sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) ); } if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { return new WP_Error( 'rest_out_of_bounds', /* translators: 1: Parameter, 2: Maximum number. */ sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) ); } } if ( isset( $args['minimum'], $args['maximum'] ) ) { if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { return new WP_Error( 'rest_out_of_bounds', sprintf( /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); } } if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { return new WP_Error( 'rest_out_of_bounds', sprintf( /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); } } if ( ! empty( $args['exclusiveMaximum'] ) && empty( $args['exclusiveMinimum'] ) ) { if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { return new WP_Error( 'rest_out_of_bounds', sprintf( /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); } } if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { if ( $value > $args['maximum'] || $value < $args['minimum'] ) { return new WP_Error( 'rest_out_of_bounds', sprintf( /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); } } } return true; } /** * Validates a string value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_string_value_from_schema( $value, $args, $param ) { if ( ! is_string( $value ) ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ), array( 'param' => $param ) ); } if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) { return new WP_Error( 'rest_too_short', sprintf( /* translators: 1: Parameter, 2: Number of characters. */ _n( '%1$s must be at least %2$s character long.', '%1$s must be at least %2$s characters long.', $args['minLength'] ), $param, number_format_i18n( $args['minLength'] ) ) ); } if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) { return new WP_Error( 'rest_too_long', sprintf( /* translators: 1: Parameter, 2: Number of characters. */ _n( '%1$s must be at most %2$s character long.', '%1$s must be at most %2$s characters long.', $args['maxLength'] ), $param, number_format_i18n( $args['maxLength'] ) ) ); } if ( isset( $args['pattern'] ) && ! rest_validate_json_schema_pattern( $args['pattern'], $value ) ) { return new WP_Error( 'rest_invalid_pattern', /* translators: 1: Parameter, 2: Pattern. */ sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] ) ); } return true; } /** * Validates an integer value based on a schema. * * @since 5.7.0 * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ function rest_validate_integer_value_from_schema( $value, $args, $param ) { $is_valid_number = rest_validate_number_value_from_schema( $value, $args, $param ); if ( is_wp_error( $is_valid_number ) ) { return $is_valid_number; } if ( ! rest_is_integer( $value ) ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ), array( 'param' => $param ) ); } return true; } /** * Sanitize a value based on a schema. * * @since 4.7.0 * @since 5.5.0 Added the `$param` parameter. * @since 5.6.0 Support the "anyOf" and "oneOf" keywords. * @since 5.9.0 Added `text-field` and `textarea-field` formats. * * @param mixed $value The value to sanitize. * @param array $args Schema array to use for sanitization. * @param string $param The parameter name, used in error messages. * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized. */ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { if ( isset( $args['anyOf'] ) ) { $matching_schema = rest_find_any_matching_schema( $value, $args, $param ); if ( is_wp_error( $matching_schema ) ) { return $matching_schema; } if ( ! isset( $args['type'] ) ) { $args['type'] = $matching_schema['type']; } $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param ); } if ( isset( $args['oneOf'] ) ) { $matching_schema = rest_find_one_matching_schema( $value, $args, $param ); if ( is_wp_error( $matching_schema ) ) { return $matching_schema; } if ( ! isset( $args['type'] ) ) { $args['type'] = $matching_schema['type']; } $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param ); } $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); if ( ! isset( $args['type'] ) ) { /* translators: %s: Parameter. */ _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); } if ( is_array( $args['type'] ) ) { $best_type = rest_handle_multi_type_schema( $value, $args, $param ); if ( ! $best_type ) { return null; } $args['type'] = $best_type; } if ( ! in_array( $args['type'], $allowed_types, true ) ) { _doing_it_wrong( __FUNCTION__, /* translators: 1: Parameter, 2: The list of allowed types. */ wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); } if ( 'array' === $args['type'] ) { $value = rest_sanitize_array( $value ); if ( ! empty( $args['items'] ) ) { foreach ( $value as $index => $v ) { $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); } } if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { /* translators: %s: Parameter. */ return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) ); } return $value; } if ( 'object' === $args['type'] ) { $value = rest_sanitize_object( $value ); foreach ( $value as $property => $v ) { if ( isset( $args['properties'][ $property ] ) ) { $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); continue; } $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args ); if ( null !== $pattern_property_schema ) { $value[ $property ] = rest_sanitize_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' ); continue; } if ( isset( $args['additionalProperties'] ) ) { if ( false === $args['additionalProperties'] ) { unset( $value[ $property ] ); } elseif ( is_array( $args['additionalProperties'] ) ) { $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); } } } return $value; } if ( 'null' === $args['type'] ) { return null; } if ( 'integer' === $args['type'] ) { return (int) $value; } if ( 'number' === $args['type'] ) { return (float) $value; } if ( 'boolean' === $args['type'] ) { return rest_sanitize_boolean( $value ); } // This behavior matches rest_validate_value_from_schema(). if ( isset( $args['format'] ) && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) ) ) { switch ( $args['format'] ) { case 'hex-color': return (string) sanitize_hex_color( $value ); case 'date-time': return sanitize_text_field( $value ); case 'email': // sanitize_email() validates, which would be unexpected. return sanitize_text_field( $value ); case 'uri': return sanitize_url( $value ); case 'ip': return sanitize_text_field( $value ); case 'uuid': return sanitize_text_field( $value ); case 'text-field': return sanitize_text_field( $value ); case 'textarea-field': return sanitize_textarea_field( $value ); } } if ( 'string' === $args['type'] ) { return (string) $value; } return $value; } /** * Append result of internal request to REST API for purpose of preloading data to be attached to a page. * Expected to be called in the context of `array_reduce`. * * @since 5.0.0 * * @param array $memo Reduce accumulator. * @param string $path REST API path to preload. * @return array Modified reduce accumulator. */ function rest_preload_api_request( $memo, $path ) { /* * array_reduce() doesn't support passing an array in PHP 5.2, * so we need to make sure we start with one. */ if ( ! is_array( $memo ) ) { $memo = array(); } if ( empty( $path ) ) { return $memo; } $method = 'GET'; if ( is_array( $path ) && 2 === count( $path ) ) { $method = end( $path ); $path = reset( $path ); if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) { $method = 'GET'; } } $path = untrailingslashit( $path ); if ( empty( $path ) ) { $path = '/'; } $path_parts = parse_url( $path ); if ( false === $path_parts ) { return $memo; } $request = new WP_REST_Request( $method, $path_parts['path'] ); if ( ! empty( $path_parts['query'] ) ) { parse_str( $path_parts['query'], $query_params ); $request->set_query_params( $query_params ); } $response = rest_do_request( $request ); if ( 200 === $response->status ) { $server = rest_get_server(); /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $server, $request ); $embed = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false; $data = (array) $server->response_to_data( $response, $embed ); if ( 'OPTIONS' === $method ) { $memo[ $method ][ $path ] = array( 'body' => $data, 'headers' => $response->headers, ); } else { $memo[ $path ] = array( 'body' => $data, 'headers' => $response->headers, ); } } return $memo; } /** * Parses the "_embed" parameter into the list of resources to embed. * * @since 5.4.0 * * @param string|array $embed Raw "_embed" parameter value. * @return true|string[] Either true to embed all embeds, or a list of relations to embed. */ function rest_parse_embed_param( $embed ) { if ( ! $embed || 'true' === $embed || '1' === $embed ) { return true; } $rels = wp_parse_list( $embed ); if ( ! $rels ) { return true; } return $rels; } /** * Filters the response to remove any fields not available in the given context. * * @since 5.5.0 * @since 5.6.0 Support the "patternProperties" keyword for objects. * Support the "anyOf" and "oneOf" keywords. * * @param array|object $response_data The response data to modify. * @param array $schema The schema for the endpoint used to filter the response. * @param string $context The requested context. * @return array|object The filtered response data. */ function rest_filter_response_by_context( $response_data, $schema, $context ) { if ( isset( $schema['anyOf'] ) ) { $matching_schema = rest_find_any_matching_schema( $response_data, $schema, '' ); if ( ! is_wp_error( $matching_schema ) ) { if ( ! isset( $schema['type'] ) ) { $schema['type'] = $matching_schema['type']; } $response_data = rest_filter_response_by_context( $response_data, $matching_schema, $context ); } } if ( isset( $schema['oneOf'] ) ) { $matching_schema = rest_find_one_matching_schema( $response_data, $schema, '', true ); if ( ! is_wp_error( $matching_schema ) ) { if ( ! isset( $schema['type'] ) ) { $schema['type'] = $matching_schema['type']; } $response_data = rest_filter_response_by_context( $response_data, $matching_schema, $context ); } } if ( ! is_array( $response_data ) && ! is_object( $response_data ) ) { return $response_data; } if ( isset( $schema['type'] ) ) { $type = $schema['type']; } elseif ( isset( $schema['properties'] ) ) { $type = 'object'; // Back compat if a developer accidentally omitted the type. } else { return $response_data; } $is_array_type = 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) ); $is_object_type = 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) ); if ( $is_array_type && $is_object_type ) { if ( rest_is_array( $response_data ) ) { $is_object_type = false; } else { $is_array_type = false; } } $has_additional_properties = $is_object_type && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ); foreach ( $response_data as $key => $value ) { $check = array(); if ( $is_array_type ) { $check = isset( $schema['items'] ) ? $schema['items'] : array(); } elseif ( $is_object_type ) { if ( isset( $schema['properties'][ $key ] ) ) { $check = $schema['properties'][ $key ]; } else { $pattern_property_schema = rest_find_matching_pattern_property_schema( $key, $schema ); if ( null !== $pattern_property_schema ) { $check = $pattern_property_schema; } elseif ( $has_additional_properties ) { $check = $schema['additionalProperties']; } } } if ( ! isset( $check['context'] ) ) { continue; } if ( ! in_array( $context, $check['context'], true ) ) { if ( $is_array_type ) { // All array items share schema, so there's no need to check each one. $response_data = array(); break; } if ( is_object( $response_data ) ) { unset( $response_data->$key ); } else { unset( $response_data[ $key ] ); } } elseif ( is_array( $value ) || is_object( $value ) ) { $new_value = rest_filter_response_by_context( $value, $check, $context ); if ( is_object( $response_data ) ) { $response_data->$key = $new_value; } else { $response_data[ $key ] = $new_value; } } } return $response_data; } /** * Sets the "additionalProperties" to false by default for all object definitions in the schema. * * @since 5.5.0 * @since 5.6.0 Support the "patternProperties" keyword. * * @param array $schema The schema to modify. * @return array The modified schema. */ function rest_default_additional_properties_to_false( $schema ) { $type = (array) $schema['type']; if ( in_array( 'object', $type, true ) ) { if ( isset( $schema['properties'] ) ) { foreach ( $schema['properties'] as $key => $child_schema ) { $schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); } } if ( isset( $schema['patternProperties'] ) ) { foreach ( $schema['patternProperties'] as $key => $child_schema ) { $schema['patternProperties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); } } if ( ! isset( $schema['additionalProperties'] ) ) { $schema['additionalProperties'] = false; } } if ( in_array( 'array', $type, true ) ) { if ( isset( $schema['items'] ) ) { $schema['items'] = rest_default_additional_properties_to_false( $schema['items'] ); } } return $schema; } /** * Gets the REST API route for a post. * * @since 5.5.0 * * @param int|WP_Post $post Post ID or post object. * @return string The route path with a leading slash for the given post, * or an empty string if there is not a route. */ function rest_get_route_for_post( $post ) { $post = get_post( $post ); if ( ! $post instanceof WP_Post ) { return ''; } $post_type_route = rest_get_route_for_post_type_items( $post->post_type ); if ( ! $post_type_route ) { return ''; } $route = sprintf( '%s/%d', $post_type_route, $post->ID ); /** * Filters the REST API route for a post. * * @since 5.5.0 * * @param string $route The route path. * @param WP_Post $post The post object. */ return apply_filters( 'rest_route_for_post', $route, $post ); } /** * Gets the REST API route for a post type. * * @since 5.9.0 * * @param string $post_type The name of a registered post type. * @return string The route path with a leading slash for the given post type, * or an empty string if there is not a route. */ function rest_get_route_for_post_type_items( $post_type ) { $post_type = get_post_type_object( $post_type ); if ( ! $post_type ) { return ''; } if ( ! $post_type->show_in_rest ) { return ''; } $namespace = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2'; $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; $route = sprintf( '/%s/%s', $namespace, $rest_base ); /** * Filters the REST API route for a post type. * * @since 5.9.0 * * @param string $route The route path. * @param WP_Post_Type $post_type The post type object. */ return apply_filters( 'rest_route_for_post_type_items', $route, $post_type ); } /** * Gets the REST API route for a term. * * @since 5.5.0 * * @param int|WP_Term $term Term ID or term object. * @return string The route path with a leading slash for the given term, * or an empty string if there is not a route. */ function rest_get_route_for_term( $term ) { $term = get_term( $term ); if ( ! $term instanceof WP_Term ) { return ''; } $taxonomy_route = rest_get_route_for_taxonomy_items( $term->taxonomy ); if ( ! $taxonomy_route ) { return ''; } $route = sprintf( '%s/%d', $taxonomy_route, $term->term_id ); /** * Filters the REST API route for a term. * * @since 5.5.0 * * @param string $route The route path. * @param WP_Term $term The term object. */ return apply_filters( 'rest_route_for_term', $route, $term ); } /** * Gets the REST API route for a taxonomy. * * @since 5.9.0 * * @param string $taxonomy Name of taxonomy. * @return string The route path with a leading slash for the given taxonomy. */ function rest_get_route_for_taxonomy_items( $taxonomy ) { $taxonomy = get_taxonomy( $taxonomy ); if ( ! $taxonomy ) { return ''; } if ( ! $taxonomy->show_in_rest ) { return ''; } $namespace = ! empty( $taxonomy->rest_namespace ) ? $taxonomy->rest_namespace : 'wp/v2'; $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; $route = sprintf( '/%s/%s', $namespace, $rest_base ); /** * Filters the REST API route for a taxonomy. * * @since 5.9.0 * * @param string $route The route path. * @param WP_Taxonomy $taxonomy The taxonomy object. */ return apply_filters( 'rest_route_for_taxonomy_items', $route, $taxonomy ); } /** * Gets the REST route for the currently queried object. * * @since 5.5.0 * * @return string The REST route of the resource, or an empty string if no resource identified. */ function rest_get_queried_resource_route() { if ( is_singular() ) { $route = rest_get_route_for_post( get_queried_object() ); } elseif ( is_category() || is_tag() || is_tax() ) { $route = rest_get_route_for_term( get_queried_object() ); } elseif ( is_author() ) { $route = '/wp/v2/users/' . get_queried_object_id(); } else { $route = ''; } /** * Filters the REST route for the currently queried object. * * @since 5.5.0 * * @param string $link The route with a leading slash, or an empty string. */ return apply_filters( 'rest_queried_resource_route', $route ); } /** * Retrieves an array of endpoint arguments from the item schema and endpoint method. * * @since 5.6.0 * * @param array $schema The full JSON schema for the endpoint. * @param string $method Optional. HTTP method of the endpoint. The arguments for `CREATABLE` endpoints are * checked for required values and may fall-back to a given default, this is not done * on `EDITABLE` endpoints. Default WP_REST_Server::CREATABLE. * @return array The endpoint arguments. */ function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) { $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array(); $endpoint_args = array(); $valid_schema_properties = rest_get_allowed_schema_keywords(); $valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) ); foreach ( $schema_properties as $field_id => $params ) { // Arguments specified as `readonly` are not allowed to be set. if ( ! empty( $params['readonly'] ) ) { continue; } $endpoint_args[ $field_id ] = array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'rest_sanitize_request_arg', ); if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) { $endpoint_args[ $field_id ]['default'] = $params['default']; } if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) { $endpoint_args[ $field_id ]['required'] = true; } foreach ( $valid_schema_properties as $schema_prop ) { if ( isset( $params[ $schema_prop ] ) ) { $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; } } // Merge in any options provided by the schema property. if ( isset( $params['arg_options'] ) ) { // Only use required / default from arg_options on CREATABLE endpoints. if ( WP_REST_Server::CREATABLE !== $method ) { $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '', ) ); } $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); } } return $endpoint_args; } /** * Converts an error to a response object. * * This iterates over all error codes and messages to change it into a flat * array. This enables simpler client behavior, as it is represented as a * list in JSON rather than an object/map. * * @since 5.7.0 * * @param WP_Error $error WP_Error instance. * * @return WP_REST_Response List of associative arrays with code and message keys. */ function rest_convert_error_to_response( $error ) { $status = array_reduce( $error->get_all_error_data(), static function ( $status, $error_data ) { return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status; }, 500 ); $errors = array(); foreach ( (array) $error->errors as $code => $messages ) { $all_data = $error->get_all_error_data( $code ); $last_data = array_pop( $all_data ); foreach ( (array) $messages as $message ) { $formatted = array( 'code' => $code, 'message' => $message, 'data' => $last_data, ); if ( $all_data ) { $formatted['additional_data'] = $all_data; } $errors[] = $formatted; } } $data = $errors[0]; if ( count( $errors ) > 1 ) { // Remove the primary error. array_shift( $errors ); $data['additional_errors'] = $errors; } return new WP_REST_Response( $data, $status ); } /** * Checks whether a REST API endpoint request is currently being handled. * * This may be a standalone REST API request, or an internal request dispatched from within a regular page load. * * @since 6.5.0 * * @global WP_REST_Server $wp_rest_server REST server instance. * * @return bool True if a REST endpoint request is currently being handled, false otherwise. */ function wp_is_rest_endpoint() { /* @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; // Check whether this is a standalone REST request. $is_rest_endpoint = wp_is_serving_rest_request(); if ( ! $is_rest_endpoint ) { // Otherwise, check whether an internal REST request is currently being handled. $is_rest_endpoint = isset( $wp_rest_server ) && $wp_rest_server->is_dispatching(); } /** * Filters whether a REST endpoint request is currently being handled. * * This may be a standalone REST API request, or an internal request dispatched from within a regular page load. * * @since 6.5.0 * * @param bool $is_request_endpoint Whether a REST endpoint request is currently being handled. */ return (bool) apply_filters( 'wp_is_rest_endpoint', $is_rest_endpoint ); } onse. * * @since 5.9.0 * * @param WP_Post $item Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { // Base fields for every post. $fields = $this->get_fields_for_response( $request ); $menu_item = $this->get_nav_menu_item( $item->ID ); $data = array(); if ( rest_is_field_included( 'id', $fields ) ) { $data['id'] = $menu_item->ID; } if ( rest_is_field_included( 'title', $fields ) ) { $data['title'] = array(); } if ( rest_is_field_included( 'title.raw', $fields ) ) { $data['title']['raw'] = $menu_item->title; } if ( rest_is_field_included( 'title.rendered', $fields ) ) { add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); add_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); /** This filter is documented in wp-includes/post-template.php */ $title = apply_filters( 'the_title', $menu_item->title, $menu_item->ID ); $data['title']['rendered'] = $title; remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); } if ( rest_is_field_included( 'status', $fields ) ) { $data['status'] = $menu_item->post_status; } if ( rest_is_field_included( 'url', $fields ) ) { $data['url'] = $menu_item->url; } if ( rest_is_field_included( 'attr_title', $fields ) ) { // Same as post_excerpt. $data['attr_title'] = $menu_item->attr_title; } if ( rest_is_field_included( 'description', $fields ) ) { // Same as post_content. $data['description'] = $menu_item->description; } if ( rest_is_field_included( 'type', $fields ) ) { $data['type'] = $menu_item->type; } if ( rest_is_field_included( 'type_label', $fields ) ) { $data['type_label'] = $menu_item->type_label; } if ( rest_is_field_included( 'object', $fields ) ) { $data['object'] = $menu_item->object; } if ( rest_is_field_included( 'object_id', $fields ) ) { // It is stored as a string, but should be exposed as an integer. $data['object_id'] = absint( $menu_item->object_id ); } if ( rest_is_field_included( 'parent', $fields ) ) { // Same as post_parent, exposed as an integer. $data['parent'] = (int) $menu_item->menu_item_parent; } if ( rest_is_field_included( 'menu_order', $fields ) ) { // Same as post_parent, exposed as an integer. $data['menu_order'] = (int) $menu_item->menu_order; } if ( rest_is_field_included( 'target', $fields ) ) { $data['target'] = $menu_item->target; } if ( rest_is_field_included( 'classes', $fields ) ) { $data['classes'] = (array) $menu_item->classes; } if ( rest_is_field_included( 'xfn', $fields ) ) { $data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) ); } if ( rest_is_field_included( 'invalid', $fields ) ) { $data['invalid'] = (bool) $menu_item->_invalid; } if ( rest_is_field_included( 'meta', $fields ) ) { $data['meta'] = $this->meta->get_value( $menu_item->ID, $request ); } $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; if ( rest_is_field_included( $base, $fields ) ) { $terms = get_the_terms( $item, $taxonomy->name ); if ( ! is_array( $terms ) ) { continue; } $term_ids = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array(); if ( 'nav_menu' === $taxonomy->name ) { $data[ $base ] = $term_ids ? array_shift( $term_ids ) : 0; } else { $data[ $base ] = $term_ids; } } } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $links = $this->prepare_links( $item ); $response->add_links( $links ); if ( ! empty( $links['self']['href'] ) ) { $actions = $this->get_available_actions( $item, $request ); $self = $links['self']['href']; foreach ( $actions as $rel ) { $response->add_link( $rel, $self ); } } } /** * Filters the menu item data for a REST API response. * * @since 5.9.0 * * @param WP_REST_Response $response The response object. * @param object $menu_item Menu item setup by {@see wp_setup_nav_menu_item()}. * @param WP_REST_Request $request Request object. */ return apply_filters( 'rest_prepare_nav_menu_item', $response, $menu_item, $request ); } /** * Prepares links for the request. * * @since 5.9.0 * * @param WP_Post $post Post object. * @return array Links for the given post. */ protected function prepare_links( $post ) { $links = parent::prepare_links( $post ); $menu_item = $this->get_nav_menu_item( $post->ID ); if ( empty( $menu_item->object_id ) ) { return $links; } $path = ''; $type = ''; $key = $menu_item->type; if ( 'post_type' === $menu_item->type ) { $path = rest_get_route_for_post( $menu_item->object_id ); $type = get_post_type( $menu_item->object_id ); } elseif ( 'taxonomy' === $menu_item->type ) { $path = rest_get_route_for_term( $menu_item->object_id ); $type = get_term_field( 'taxonomy', $menu_item->object_id ); } if ( $path && $type ) { $links['https://api.w.org/menu-item-object'][] = array( 'href' => rest_url( $path ), $key => $type, 'embeddable' => true, ); } return $links; } /** * Retrieves Link Description Objects that should be added to the Schema for the nav menu items collection. * * @since 5.9.0 * * @return array */ protected function get_schema_links() { $links = parent::get_schema_links(); $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" ); $links[] = array( 'rel' => 'https://api.w.org/menu-item-object', 'title' => __( 'Get linked object.' ), 'href' => $href, 'targetSchema' => array( 'type' => 'object', 'properties' => array( 'object' => array( 'type' => 'integer', ), ), ), ); return $links; } /** * Retrieves the nav menu item's schema, conforming to JSON Schema. * * @since 5.9.0 * * @return array Item schema data. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, 'type' => 'object', ); $schema['properties']['title'] = array( 'description' => __( 'The title for the object.' ), 'type' => array( 'string', 'object' ), 'context' => array( 'view', 'edit', 'embed' ), 'properties' => array( 'raw' => array( 'description' => __( 'Title for the object, as it exists in the database.' ), 'type' => 'string', 'context' => array( 'edit' ), ), 'rendered' => array( 'description' => __( 'HTML title for the object, transformed for display.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), ); $schema['properties']['id'] = array( 'description' => __( 'Unique identifier for the object.' ), 'type' => 'integer', 'default' => 0, 'minimum' => 0, 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['type_label'] = array( 'description' => __( 'The singular label used to describe this type of menu item.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); $schema['properties']['type'] = array( 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ), 'type' => 'string', 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ), 'context' => array( 'view', 'edit', 'embed' ), 'default' => 'custom', ); $schema['properties']['status'] = array( 'description' => __( 'A named status for the object.' ), 'type' => 'string', 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 'default' => 'publish', 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['parent'] = array( 'description' => __( 'The ID for the parent of the object.' ), 'type' => 'integer', 'minimum' => 0, 'default' => 0, 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['attr_title'] = array( 'description' => __( 'Text for the title attribute of the link element for this menu item.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ); $schema['properties']['classes'] = array( 'description' => __( 'Class names for the link element of this menu item.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => static function ( $value ) { return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); }, ), ); $schema['properties']['description'] = array( 'description' => __( 'The description of this menu item.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ); $schema['properties']['menu_order'] = array( 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', 'minimum' => 1, 'default' => 1, ); $schema['properties']['object'] = array( 'description' => __( 'The type of object originally represented, such as "category", "post", or "attachment".' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', 'arg_options' => array( 'sanitize_callback' => 'sanitize_key', ), ); $schema['properties']['object_id'] = array( 'description' => __( 'The database ID of the original object this menu item represents, for example the ID for posts or the term_id for categories.' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', 'minimum' => 0, 'default' => 0, ); $schema['properties']['target'] = array( 'description' => __( 'The target attribute of the link element for this menu item.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'enum' => array( '_blank', '', ), ); $schema['properties']['url'] = array( 'description' => __( 'The URL to which this menu item points.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'validate_callback' => static function ( $url ) { if ( '' === $url ) { return true; } if ( sanitize_url( $url ) ) { return true; } return new WP_Error( 'rest_invalid_url', __( 'Invalid URL.' ) ); }, ), ); $schema['properties']['xfn'] = array( 'description' => __( 'The XFN relationship expressed in the link of this menu item.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => static function ( $value ) { return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); }, ), ); $schema['properties']['invalid'] = array( 'description' => __( 'Whether the menu item represents an object that no longer exists.' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'boolean', 'readonly' => true, ); $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; $schema['properties'][ $base ] = array( /* translators: %s: taxonomy name */ 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ), 'type' => 'array', 'items' => array( 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ); if ( 'nav_menu' === $taxonomy->name ) { $schema['properties'][ $base ]['type'] = 'integer'; unset( $schema['properties'][ $base ]['items'] ); } } $schema['properties']['meta'] = $this->meta->get_field_schema(); $schema_links = $this->get_schema_links(); if ( $schema_links ) { $schema['links'] = $schema_links; } $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Retrieves the query params for the nav menu items collection. * * @since 5.9.0 * * @return array Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['menu_order'] = array( 'description' => __( 'Limit result set to posts with a specific menu_order value.' ), 'type' => 'integer', ); $query_params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.' ), 'type' => 'string', 'default' => 'asc', 'enum' => array( 'asc', 'desc' ), ); $query_params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.' ), 'type' => 'string', 'default' => 'menu_order', 'enum' => array( 'author', 'date', 'id', 'include', 'modified', 'parent', 'relevance', 'slug', 'include_slugs', 'title', 'menu_order', ), ); // Change default to 100 items. $query_params['per_page']['default'] = 100; return $query_params; } /** * Determines the allowed query_vars for a get_items() response and prepares * them for WP_Query. * * @since 5.9.0 * * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. * @param WP_REST_Request $request Optional. Full details about the request. * @return array Items query arguments. */ protected function prepare_items_query( $prepared_args = array(), $request = null ) { $query_args = parent::prepare_items_query( $prepared_args, $request ); // Map to proper WP_Query orderby param. if ( isset( $query_args['orderby'], $request['orderby'] ) ) { $orderby_mappings = array( 'id' => 'ID', 'include' => 'post__in', 'slug' => 'post_name', 'include_slugs' => 'post_name__in', 'menu_order' => 'menu_order', ); if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) { $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ]; } } $query_args['update_menu_item_cache'] = true; return $query_args; } /** * Gets the id of the menu that the given menu item belongs to. * * @since 5.9.0 * * @param int $menu_item_id Menu item id. * @return int */ protected function get_menu_id( $menu_item_id ) { $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) ); $menu_id = 0; if ( $menu_ids && ! is_wp_error( $menu_ids ) ) { $menu_id = array_shift( $menu_ids ); } return $menu_id; } }