diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 7b615e5a5f..99cdd3e749 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1552,6 +1552,7 @@ function rest_stabilize_value( $value ) { * 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. * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. @@ -1691,9 +1692,16 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { } } - if ( in_array( $args['type'], array( 'integer', 'number' ), true ) && ! is_numeric( $value ) ) { - /* translators: 1: Parameter, 2: Type name. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) ); + if ( in_array( $args['type'], array( 'integer', 'number' ), true ) ) { + if ( ! is_numeric( $value ) ) { + /* translators: 1: Parameter, 2: Type name. */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) ); + } + + if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) { + /* translators: 1: Parameter, 2: Multiplier. */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] ) ); + } } if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) { @@ -2298,6 +2306,7 @@ function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::C 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', + 'multipleOf', 'minLength', 'maxLength', 'pattern', diff --git a/tests/phpunit/tests/rest-api/rest-controller.php b/tests/phpunit/tests/rest-api/rest-controller.php index 74364064e6..d6dc7f8fae 100644 --- a/tests/phpunit/tests/rest-api/rest-controller.php +++ b/tests/phpunit/tests/rest-api/rest-controller.php @@ -281,7 +281,7 @@ class WP_Test_REST_Controller extends WP_Test_REST_TestCase { $this->assertArrayHasKey( $property, $args['somestring'] ); } - foreach ( array( 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum' ) as $property ) { + foreach ( array( 'multipleOf', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum' ) as $property ) { $this->assertArrayHasKey( $property, $args['someinteger'] ); } diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index 5889341c7b..77a2c2ec14 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -435,6 +435,42 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertSame( 'param must be between 10 (inclusive) and 20 (inclusive)', $error->get_error_message() ); } + /** + * @ticket 51022 + * + * @dataProvider data_multiply_of + * + * @param int|float $value + * @param int|float $divisor + * @param bool $expected + */ + public function test_numeric_multiple_of( $value, $divisor, $expected ) { + $schema = array( + 'type' => 'number', + 'multipleOf' => $divisor, + ); + + $result = rest_validate_value_from_schema( $value, $schema ); + + if ( $expected ) { + $this->assertTrue( $result ); + } else { + $this->assertWPError( $result ); + } + } + + public function data_multiply_of() { + return array( + array( 0, 2, true ), + array( 4, 2, true ), + array( 3, 1.5, true ), + array( 2.4, 1.2, true ), + array( 1, 2, false ), + array( 2, 1.5, false ), + array( 2.1, 1.5, false ), + ); + } + /** * @ticket 50300 */ diff --git a/tests/phpunit/tests/rest-api/rest-test-controller.php b/tests/phpunit/tests/rest-api/rest-test-controller.php index 88c0ce7779..04732100a4 100644 --- a/tests/phpunit/tests/rest-api/rest-test-controller.php +++ b/tests/phpunit/tests/rest-api/rest-test-controller.php @@ -46,6 +46,7 @@ class WP_REST_Test_Controller extends WP_REST_Controller { ), 'someinteger' => array( 'type' => 'integer', + 'multipleOf' => 10, 'minimum' => 100, 'maximum' => 200, 'exclusiveMinimum' => true,