<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[39046] trunk: REST API: Add support for arrays in schema validation and sanitization.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/39046">39046</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/39046","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>pento</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-10-31 01:47:36 +0000 (Mon, 31 Oct 2016)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>REST API: Add support for arrays in schema validation and sanitization.

By allowing more fine-grained validation and sanitisation of endpoint args, we can ensure the correct data is being passed to endpoints.

This can easily be extended to support new data types, such as CSV fields or objects.

Props joehoyle, rachelbaker, pento.
Fixes <a href="https://core.trac.wordpress.org/ticket/38531">#38531</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestcontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestpostscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestsettingscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestuserscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiphp">trunk/src/wp-includes/rest-api.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsrestapirestschemavalidationphp">trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesrestapiendpointsclasswprestcontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php     2016-10-31 01:26:10 UTC (rev 39045)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php       2016-10-31 01:47:36 UTC (rev 39046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -559,7 +559,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $endpoint_args[ $field_id ]['required'] = true;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( isset( $params[ $schema_prop ] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                        $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestpostscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php       2016-10-31 01:26:10 UTC (rev 39045)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php 2016-10-31 01:47:36 UTC (rev 39046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1971,6 +1971,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $schema['properties'][ $base ] = array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'type'        => 'array',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'items'       => array(
+                                       'type'    => 'integer',
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'context'     => array( 'view', 'edit' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><span class="cx" style="display: block; padding: 0 10px">                        $schema['properties'][ $base . '_exclude' ] = array(
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestsettingscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php    2016-10-31 01:26:10 UTC (rev 39045)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php      2016-10-31 01:47:36 UTC (rev 39046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -288,8 +288,30 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $options as $option_name => $option ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $schema['properties'][ $option_name ] = $option['schema'];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $schema['properties'][ $option_name ]['arg_options'] = array(
+                               'sanitize_callback' => array( $this, 'sanitize_callback' ),
+                       );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $this->add_additional_fields_schema( $schema );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Custom sanitize callback used for all options to allow the use of 'null'.
+        *
+        * By default, the schema of settings will throw an error if a value is set to
+        * `null` as it's not a valid value for something like "type => string". We
+        * provide a wrapper sanitizer to whitelist the use of `null`.
+        *
+        * @param  mixed           $value   The value for the setting.
+        * @param  WP_REST_Request $request The request object.
+        * @param  string          $param   The parameter name.
+        * @return mixed|WP_Error
+        */
+       public function sanitize_callback( $value, $request, $param ) {
+               if ( is_null( $value ) ) {
+                       return $value;
+               }
+               return rest_parse_request_arg( $value, $request, $param );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestuserscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php       2016-10-31 01:26:10 UTC (rev 39045)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php 2016-10-31 01:47:36 UTC (rev 39046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1006,6 +1006,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                'roles'           => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                        'description' => __( 'Roles assigned to the resource.' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        'type'        => 'array',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        'items'       => array(
+                                               'type'    => 'string',
+                                       ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                         'context'     => array( 'edit' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'password'        => array(
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api.php        2016-10-31 01:26:10 UTC (rev 39045)
+++ trunk/src/wp-includes/rest-api.php  2016-10-31 01:47:36 UTC (rev 39046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -820,80 +820,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px">        $args = $attributes['args'][ $param ];
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( ! empty( $args['enum'] ) ) {
-               if ( ! in_array( $value, $args['enum'], true ) ) {
-                       return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: list of valid values */ __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
-               }
-       }
-
-       if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) {
-               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
-       }
-
-       if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
-               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) );
-       }
-
-       if ( 'string' === $args['type'] && ! is_string( $value ) ) {
-               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
-       }
-
-       if ( isset( $args['format'] ) ) {
-               switch ( $args['format'] ) {
-                       case 'date-time' :
-                               if ( ! rest_parse_date( $value ) ) {
-                                       return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) );
-                               }
-                               break;
-
-                       case 'email' :
-                               if ( ! is_email( $value ) ) {
-                                       return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) );
-                               }
-                               break;
-                       case 'ipv4' :
-                               if ( ! rest_is_ip_address( $value ) ) {
-                                       return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) );
-                               }
-                               break;
-               }
-       }
-
-       if ( in_array( $args['type'], array( 'numeric', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
-               if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
-                       if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
-                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) );
-                       } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
-                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) );
-                       }
-               } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
-                       if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
-                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) );
-                       } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
-                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) );
-                       }
-               } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
-                       if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
-                               if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
-                                       return new WP_Error( 'rest_invalid_param', 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'] ) );
-                               }
-                       } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
-                               if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
-                                       return new WP_Error( 'rest_invalid_param', 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'] ) );
-                               }
-                       } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
-                               if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
-                                       return new WP_Error( 'rest_invalid_param', 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'] ) );
-                               }
-                       } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
-                               if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
-                                       return new WP_Error( 'rest_invalid_param', 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;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return rest_validate_value_from_schema( $value, $args, $param );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -913,34 +840,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px">        $args = $attributes['args'][ $param ];
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( 'integer' === $args['type'] ) {
-               return (int) $value;
-       }
-
-       if ( 'boolean' === $args['type'] ) {
-               return rest_sanitize_boolean( $value );
-       }
-
-       if ( isset( $args['format'] ) ) {
-               switch ( $args['format'] ) {
-                       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 esc_url_raw( $value );
-
-                       case 'ipv4' :
-                               return sanitize_text_field( $value );
-               }
-       }
-
-       return $value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return rest_sanitize_value_from_schema( $value, $args, $param );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1084,3 +984,154 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Validate a value based on a schema.
+ *
+ * @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 ( 'array' === $args['type'] ) {
+               if ( ! is_array( $value ) ) {
+                       return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
+               }
+               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 ( ! empty( $args['enum'] ) ) {
+               if ( ! in_array( $value, $args['enum'], true ) ) {
+                       return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: list of valid values */ __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
+               }
+       }
+
+       if ( in_array( $args['type'], array( 'integer', 'number' ) ) && ! is_numeric( $value ) ) {
+               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
+       }
+
+       if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) {
+               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
+       }
+
+       if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
+               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) );
+       }
+
+       if ( 'string' === $args['type'] && ! is_string( $value ) ) {
+               return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
+       }
+
+       if ( isset( $args['format'] ) ) {
+               switch ( $args['format'] ) {
+                       case 'date-time' :
+                               if ( ! rest_parse_date( $value ) ) {
+                                       return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) );
+                               }
+                               break;
+
+                       case 'email' :
+                               if ( ! is_email( $value ) ) {
+                                       return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) );
+                               }
+                               break;
+                       case 'ipv4' :
+                               if ( ! rest_is_ip_address( $value ) ) {
+                                       return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) );
+                               }
+                               break;
+               }
+       }
+
+       if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
+               if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
+                       if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
+                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) );
+                       } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
+                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) );
+                       }
+               } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
+                       if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
+                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) );
+                       } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
+                               return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) );
+                       }
+               } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
+                       if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
+                               if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
+                                       return new WP_Error( 'rest_invalid_param', 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'] ) );
+                               }
+                       } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
+                               if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
+                                       return new WP_Error( 'rest_invalid_param', 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'] ) );
+                               }
+                       } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
+                               if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
+                                       return new WP_Error( 'rest_invalid_param', 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'] ) );
+                               }
+                       } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
+                               if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
+                                       return new WP_Error( 'rest_invalid_param', 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;
+}
+
+/**
+ * Sanitize a value based on a schema.
+ *
+ * @param mixed $value The value to sanitize.
+ * @param array $args  Schema array to use for sanitization.
+ * @return true|WP_Error
+ */
+function rest_sanitize_value_from_schema( $value, $args ) {
+       if ( 'array' === $args['type'] ) {
+               if ( empty( $args['items'] ) ) {
+                       return (array) $value;
+               }
+               foreach ( $value as $index => $v ) {
+                       $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] );
+               }
+               return $value;
+       }
+       if ( 'integer' === $args['type'] ) {
+               return (int) $value;
+       }
+
+       if ( 'number' === $args['type'] ) {
+               return (float) $value;
+       }
+
+       if ( 'boolean' === $args['type'] ) {
+               return rest_sanitize_boolean( $value );
+       }
+
+       if ( isset( $args['format'] ) ) {
+               switch ( $args['format'] ) {
+                       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 esc_url_raw( $value );
+
+                       case 'ipv4' :
+                               return sanitize_text_field( $value );
+               }
+       }
+
+       return $value;
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestschemavalidationphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php                             (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php       2016-10-31 01:47:36 UTC (rev 39046)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,106 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering schema validation and sanitization functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Schema_Validation extends WP_UnitTestCase {
+
+       public function test_type_number() {
+               $schema = array(
+                       'type'    => 'number',
+                       'minimum' => 1,
+                       'maximum' => 2,
+               );
+               $this->assertTrue( rest_validate_value_from_schema( 1, $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 2, $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 3, $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( true, $schema ) );
+       }
+
+       public function test_type_integer() {
+               $schema = array(
+                       'type' => 'integer',
+                       'minimum' => 1,
+                       'maximum' => 2,
+               );
+               $this->assertTrue( rest_validate_value_from_schema( 1, $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 2, $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 3, $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 1.1, $schema ) );
+       }
+
+       public function test_type_string() {
+               $schema = array(
+                       'type' => 'string',
+               );
+               $this->assertTrue( rest_validate_value_from_schema( 'Hello :)', $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( '1', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 1, $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( array(), $schema ) );
+       }
+
+       public function test_type_boolean() {
+               $schema = array(
+                       'type' => 'boolean',
+               );
+               $this->assertTrue( rest_validate_value_from_schema( true, $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( false, $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 1, $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 0, $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 'true', $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 'false', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 'no', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 'yes', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 1123, $schema ) );
+       }
+
+       public function test_format_email() {
+               $schema = array(
+                       'type'  => 'string',
+                       'format' => 'email',
+               );
+               $this->assertTrue( rest_validate_value_from_schema( 'email@example.com', $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( 'a@b.c', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( 'email', $schema ) );
+       }
+
+       public function test_format_date_time() {
+               $schema = array(
+                       'type'  => 'string',
+                       'format' => 'date-time',
+               );
+               $this->assertTrue( rest_validate_value_from_schema( '2016-06-30T05:43:21', $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( '2016-06-30T05:43:21Z', $schema ) );
+               $this->assertTrue( rest_validate_value_from_schema( '2016-06-30T05:43:21+00:00', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( '20161027T163355Z', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( '2016', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( '2016-06-30', $schema ) );
+       }
+
+       public function test_format_ipv4() {
+               $schema = array(
+                       'type'  => 'string',
+                       'format' => 'ipv4',
+               );
+               $this->assertTrue( rest_validate_value_from_schema( '127.0.0.1', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( '3333.3333.3333.3333', $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( '1', $schema ) );
+       }
+
+       public function test_type_array() {
+               $schema = array(
+                       'type' => 'array',
+                       'items' => array(
+                               'type' => 'number',
+                       ),
+               );
+               $this->assertTrue( rest_validate_value_from_schema( array( 1 ), $schema ) );
+               $this->assertWPError( rest_validate_value_from_schema( array( true ), $schema ) );
+       }
+}
</ins></span></pre>
</div>
</div>

</body>
</html>