diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index c524f9e22a12f..4854a46aa15c8 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1845,7 +1845,7 @@ function rest_find_matching_pattern_property_schema( $property, $args ) { * @since 5.6.0 * * @param string $param The parameter name. - * @param array $error The error details. + * @param array $error The error details. * @return WP_Error */ function rest_format_combining_operation_error( $param, $error ) { @@ -2182,6 +2182,7 @@ function rest_get_allowed_schema_keywords() { * @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 ) ) { @@ -2243,9 +2244,32 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { $is_valid = rest_validate_boolean_value_from_schema( $value, $param ); break; case 'object': + /* + * A JSON-encoded string (e.g. from a GET query parameter) should be + * decoded before validation, mirroring what parse_json_params() does + * for application/json request bodies. + */ + if ( is_string( $value ) ) { + $decoded = json_decode( $value, true ); + if ( null !== $decoded && JSON_ERROR_NONE === json_last_error() ) { + $value = $decoded; + } + } $is_valid = rest_validate_object_value_from_schema( $value, $args, $param ); break; case 'array': + /* + * A JSON-encoded string (e.g. ?ids=[1,2,3]) should be decoded before + * validation. This takes priority over the comma-separated-value + * fallback in rest_is_array() / wp_parse_list(), which cannot + * preserve value types. + */ + if ( is_string( $value ) && str_starts_with( ltrim( $value ), '[' ) ) { + $decoded = json_decode( $value, true ); + if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) { + $value = $decoded; + } + } $is_valid = rest_validate_array_value_from_schema( $value, $args, $param ); break; case 'number': @@ -2780,6 +2804,7 @@ function rest_validate_integer_value_from_schema( $value, $args, $param ) { * @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 ) ) { @@ -2833,6 +2858,19 @@ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { } if ( 'array' === $args['type'] ) { + /* + * A JSON-encoded string (e.g. ?ids=[1,2,3]) should be decoded before + * sanitization. This takes priority over the comma-separated-value + * fallback in rest_sanitize_array() / wp_parse_list(), which cannot + * preserve value types. + */ + if ( is_string( $value ) && str_starts_with( ltrim( $value ), '[' ) ) { + $decoded = json_decode( $value, true ); + if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) { + $value = $decoded; + } + } + $value = rest_sanitize_array( $value ); if ( ! empty( $args['items'] ) ) { @@ -2850,6 +2888,18 @@ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { } if ( 'object' === $args['type'] ) { + /* + * A JSON-encoded string (e.g. from a GET query parameter) should be + * decoded before sanitization, mirroring what parse_json_params() does + * for application/json request bodies. + */ + if ( is_string( $value ) ) { + $decoded = json_decode( $value, true ); + if ( null !== $decoded && JSON_ERROR_NONE === json_last_error() ) { + $value = $decoded; + } + } + $value = rest_sanitize_object( $value ); foreach ( $value as $property => $v ) { diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 90de3e13eecea..7f94aedeba72d 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -2583,4 +2583,94 @@ public function test_should_return_error_if_rest_route_not_string() { throw $e; // Re-throw to satisfy expectException } } + + + /** + * @ticket 64926 + * REST API: GET requests fail object/array schema validation + * when params are JSON-serialized strings. + * Test that rest_validate_value_from_schema correctly decodes and validates a JSON string for array types. + */ + public function test_validate_json_string_array() { + $schema = array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + ); + + $value = '[1, 2, 3]'; + $is_valid = rest_validate_value_from_schema( $value, $schema, 'test_param' ); + + $this->assertTrue( $is_valid, 'The JSON array string should be correctly decoded and validated.' ); + } + + /** + * @ticket 64926 + * Test that rest_validate_value_from_schema correctly decodes and validates a JSON string for object types. + */ + public function test_validate_json_string_object() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'name' => array( 'type' => 'string' ), + ), + ); + + $value = '{"id": 123, "name": "Gemini"}'; + $is_valid = rest_validate_value_from_schema( $value, $schema, 'test_param' ); + + $this->assertTrue( $is_valid, 'The JSON object string should be correctly decoded and validated.' ); + } + + /** + * @ticket 64926 + * Test that rest_sanitize_value_from_schema correctly decodes and sanitizes a JSON array string. + */ + public function test_sanitize_json_string_array() { + $schema = array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + ); + + $value = '[10, "20", 30]'; + $sanitized = rest_sanitize_value_from_schema( $value, $schema, 'test_param' ); + + $this->assertIsArray( $sanitized ); + $this->assertSame( array( 10, 20, 30 ), $sanitized ); + } + + /** + * @ticket 64926 + * Test that rest_sanitize_value_from_schema correctly decodes and sanitizes a JSON object string. + */ + public function test_sanitize_json_string_object() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'active' => array( 'type' => 'boolean' ), + ), + ); + + $value = '{"active": "true"}'; + $sanitized = rest_sanitize_value_from_schema( $value, $schema, 'test_param' ); + + $this->assertIsArray( $sanitized ); + $this->assertTrue( $sanitized['active'] ); + } + + /** + * @ticket 64926 + * Test that invalid JSON strings fall back to the original validation logic. + */ + public function test_validate_invalid_json_falls_back() { + $schema = array( + 'type' => 'array', + ); + + $value = '1,2,3'; + + $is_valid = rest_validate_value_from_schema( $value, $schema, 'test_param' ); + + $this->assertNotWPError( $is_valid ); + } }