<!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>[38832] trunk: REST API: Introduce the Content API endpoints.</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/38832">38832</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/38832","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>rachelbaker</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-10-20 02:54:12 +0000 (Thu, 20 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: Introduce the Content API endpoints.

REST API endpoints for your WordPress content. These endpoints provide machine-readable external access to your WordPress site with a clear, standards-driven interface, allowing new and innovative apps for interacting with your site. These endpoints support all of the following:
- Posts: Read and write access to all post data, for all types of post-based data, including pages and media.
- Comments: Read and write access to all comment data. This includes pingbacks and trackbacks.
- Terms: Read and write access to all term data.
- Users: Read and write access to all user data. This includes public access to some data for post authors.
- Meta: Read and write access to metadata for posts, comments, terms, and users, on an opt-in basis from plugins.
- Settings: Read and write access to settings, on an opt-in basis from plugins and core. This enables API management of key site content values that are technically stored in options, such as site title and byline.

Love your REST API, WordPress!  The infrastructure says, "Let's do lunch!" but the content API endpoints say, "You're paying!"

Props rmccue, rachelbaker, danielbachhuber, joehoyle, adamsilverstein, afurculita, ahmadawais, airesvsg, alisspers, antisilent, apokalyptik, artoliukkonen, attitude, boonebgorges, bradyvercher, brianhogg, caseypatrickdriscoll, chopinbach, chredd, christianesperar, chrisvanpatten, claudiolabarbera, claudiosmweb, cmmarslender, codebykat, coderkevin, codfish, codonnell822, daggerhart, danielpunkass, davidbhayes, delphinus, desrosj, dimadin, dotancohen, DrewAPicture, Dudo1985, duncanjbrown, eherman24, eivhyl, eliorivero, elyobo, en-alis, ericandrewlewis, ericpedia, evansobkowicz, fjarrett, frozzare, georgestephanis, greatislander, guavaworks, hideokamoto, hkdobrev, hubdotcom, hurtige, iandunn, ircrash, ironpaperweight, iseulde, Japh, jaredcobb, JDGrimes, jdolan, jdoubleu, jeremyfelt, jimt, jjeaton, jmusal, jnylen0, johanmynhardt, johnbillion, jonathanbardo, jorbin, joshkadis, JPry, jshreve, jtsternberg, JustinSainton, kacperszurek, kadamwhite, kalenjohnson, kellbot, kjbenk, kokarn, krogs
 gard, kuchenundkakao, kuldipem, kwight, lgedeon, lukepettway, mantismamita, markoheijnen, matrixik, mattheu, mauteri, maxcutler, mayukojpn, michael-arestad, miyauchi, mjbanks, modemlooper, mrbobbybryant, NateWr, nathanrice, netweb, NikV, nullvariable, oskosk, oso96_2000, oxymoron, pcfreak30, pento, peterwilsoncc, Pezzab, phh, pippinsplugins, pjgalbraith, pkevan, pollyplummer, pushred, quasel, QWp6t, schlessera, schrapel, Shelob9, shprink, simonlampen, Soean, solal, tapsboy, tfrommen, tharsheblows, thenbrent, tierra, tlovett1, tnegri, tobych, Toddses, toro_unit, traversal, vanillalounge, vishalkakadiya, wanecek, web2style, webbgaraget, websupporter, westonruter, whyisjake, wonderboymusic, wpsmith, xknown, zyphonic.
Fixes <a href="https://core.trac.wordpress.org/ticket/38373">#38373</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesfunctionsphp">trunk/src/wp-includes/functions.php</a></li>
<li><a href="#trunksrcwpincludesoptionphp">trunk/src/wp-includes/option.php</a></li>
<li><a href="#trunksrcwpincludespostphp">trunk/src/wp-includes/post.php</a></li>
<li><a href="#trunksrcwpincludesrestapiphp">trunk/src/wp-includes/rest-api.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunksrcwpincludestaxonomyphp">trunk/src/wp-includes/taxonomy.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
<li><a href="#trunktestsphpunitincludesbootstrapphp">trunk/tests/phpunit/includes/bootstrap.php</a></li>
<li><a href="#trunktestsphpunitincludesutilsphp">trunk/tests/phpunit/includes/utils.php</a></li>
<li><a href="#trunktestsphpunittestsrestapiphp">trunk/tests/phpunit/tests/rest-api.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesjswpapijs">trunk/src/wp-includes/js/wp-api.js</a></li>
<li>trunk/src/wp-includes/rest-api/endpoints/</li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestattachmentscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestcommentscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestcontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestpoststatusescontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestposttypescontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-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="#trunksrcwpincludesrestapiendpointsclasswprestrevisionscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-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="#trunksrcwpincludesrestapiendpointsclasswpresttaxonomiescontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswpresttermscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestuserscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php</a></li>
<li>trunk/src/wp-includes/rest-api/fields/</li>
<li><a href="#trunksrcwpincludesrestapifieldsclasswprestcommentmetafieldsphp">trunk/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php</a></li>
<li><a href="#trunksrcwpincludesrestapifieldsclasswprestmetafieldsphp">trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php</a></li>
<li><a href="#trunksrcwpincludesrestapifieldsclasswprestpostmetafieldsphp">trunk/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php</a></li>
<li><a href="#trunksrcwpincludesrestapifieldsclasswpresttermmetafieldsphp">trunk/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php</a></li>
<li><a href="#trunksrcwpincludesrestapifieldsclasswprestusermetafieldsphp">trunk/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php</a></li>
<li><a href="#trunktestsphpunitdataimagescodeispoetrypng">trunk/tests/phpunit/data/images/codeispoetry.png</a></li>
<li><a href="#trunktestsphpunitincludestestcaserestcontrollerphp">trunk/tests/phpunit/includes/testcase-rest-controller.php</a></li>
<li><a href="#trunktestsphpunitincludestestcaserestposttypecontrollerphp">trunk/tests/phpunit/includes/testcase-rest-post-type-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestattachmentscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestcategoriescontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-categories-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestcommentscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-comments-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestcontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestpagescontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-pages-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestpostmetafieldsphp">trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestpoststatusescontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestposttypescontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-post-types-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestpostscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestrequestvalidationphp">trunk/tests/phpunit/tests/rest-api/rest-request-validation.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestrevisionscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-revisions-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestsettingscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-settings-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapiresttagscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-tags-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapiresttaxonomiescontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapiresttestcontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-test-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestuserscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-users-controller.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesdefaultfiltersphp"></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/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/default-filters.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -374,7 +374,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // REST API actions.
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'init',          'rest_api_init' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'rest_api_init', 'rest_api_default_filters',   10, 1 );
+add_action( 'rest_api_init', 'register_initial_settings',  10 );
+add_action( 'rest_api_init', 'create_initial_rest_routes', 99 );
</ins><span class="cx" style="display: block; padding: 0 10px"> add_action( 'parse_request', 'rest_api_loaded' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre></div>
<a id="trunksrcwpincludesfunctionsphp"></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/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.php       2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/functions.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3430,6 +3430,26 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Clean up an array, comma- or space-separated list of slugs.
+ *
+ * @since 4.7.0
+ *
+ * @param  array|string $list List of slugs.
+ * @return array Sanitized array of slugs.
+ */
+function wp_parse_slug_list( $list ) {
+       if ( ! is_array( $list ) ) {
+               $list = preg_split( '/[\s,]+/', $list );
+       }
+
+       foreach ( $list as $key => $value ) {
+               $list[ $key ] = sanitize_title( $value );
+       }
+
+       return array_unique( $list );
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Extract a slice of an array, given a list of keys.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.1.0
</span></span></pre></div>
<a id="trunksrcwpincludesjswpapijs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/js/wp-api.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/wp-api.js                                (rev 0)
+++ trunk/src/wp-includes/js/wp-api.js  2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1337 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+(function( window, undefined ) {
+
+       'use strict';
+
+       /**
+        * Initialise the WP_API.
+        */
+       function WP_API() {
+               this.models = {};
+               this.collections = {};
+               this.views = {};
+       }
+
+       window.wp            = window.wp || {};
+       wp.api               = wp.api || new WP_API();
+       wp.api.versionString = wp.api.versionString || 'wp/v2/';
+
+       // Alias _includes to _.contains, ensuring it is available if lodash is used.
+       if ( ! _.isFunction( _.includes ) && _.isFunction( _.contains ) ) {
+         _.includes = _.contains;
+       }
+
+})( window );
+
+(function( window, undefined ) {
+
+       'use strict';
+
+       var pad, r;
+
+       window.wp = window.wp || {};
+       wp.api = wp.api || {};
+       wp.api.utils = wp.api.utils || {};
+
+       /**
+        * ECMAScript 5 shim, adapted from MDN.
+        * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+        */
+       if ( ! Date.prototype.toISOString ) {
+               pad = function( number ) {
+                       r = String( number );
+                       if ( 1 === r.length ) {
+                               r = '0' + r;
+                       }
+
+                       return r;
+               };
+
+               Date.prototype.toISOString = function() {
+                       return this.getUTCFullYear() +
+                               '-' + pad( this.getUTCMonth() + 1 ) +
+                               '-' + pad( this.getUTCDate() ) +
+                               'T' + pad( this.getUTCHours() ) +
+                               ':' + pad( this.getUTCMinutes() ) +
+                               ':' + pad( this.getUTCSeconds() ) +
+                               '.' + String( ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ) ).slice( 2, 5 ) +
+                               'Z';
+               };
+       }
+
+       /**
+        * Parse date into ISO8601 format.
+        *
+        * @param {Date} date.
+        */
+       wp.api.utils.parseISO8601 = function( date ) {
+               var timestamp, struct, i, k,
+                       minutesOffset = 0,
+                       numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
+
+               // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
+               // before falling back to any implementation-specific date parsing, so that’s what we do, even if native
+               // implementations could be faster.
+               //              1 YYYY                2 MM       3 DD           4 HH    5 mm       6 ss        7 msec        8 Z 9 ±    10 tzHH    11 tzmm
+               if ( ( struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec( date ) ) ) {
+
+                       // Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC.
+                       for ( i = 0; ( k = numericKeys[i] ); ++i ) {
+                               struct[k] = +struct[k] || 0;
+                       }
+
+                       // Allow undefined days and months.
+                       struct[2] = ( +struct[2] || 1 ) - 1;
+                       struct[3] = +struct[3] || 1;
+
+                       if ( 'Z' !== struct[8]  && undefined !== struct[9] ) {
+                               minutesOffset = struct[10] * 60 + struct[11];
+
+                               if ( '+' === struct[9] ) {
+                                       minutesOffset = 0 - minutesOffset;
+                               }
+                       }
+
+                       timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] );
+               } else {
+                       timestamp = Date.parse ? Date.parse( date ) : NaN;
+               }
+
+               return timestamp;
+       };
+
+       /**
+        * Helper function for getting the root URL.
+        * @return {[type]} [description]
+        */
+       wp.api.utils.getRootUrl = function() {
+               return window.location.origin ?
+                       window.location.origin + '/' :
+                       window.location.protocol + '/' + window.location.host + '/';
+       };
+
+       /**
+        * Helper for capitalizing strings.
+        */
+       wp.api.utils.capitalize = function( str ) {
+               if ( _.isUndefined( str ) ) {
+                       return str;
+               }
+               return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
+       };
+
+       /**
+        * Extract a route part based on negative index.
+        *
+        * @param {string} route The endpoint route.
+        * @param {int}    part  The number of parts from the end of the route to retrieve. Default 1.
+        *                       Example route `/a/b/c`: part 1 is `c`, part 2 is `b`, part 3 is `a`.
+        */
+       wp.api.utils.extractRoutePart = function( route, part ) {
+               var routeParts;
+
+               part  = part || 1;
+
+               // Remove versions string from route to avoid returning it.
+               route = route.replace( wp.api.versionString, '' );
+               routeParts = route.split( '/' ).reverse();
+               if ( _.isUndefined( routeParts[ --part ] ) ) {
+                       return '';
+               }
+               return routeParts[ part ];
+       };
+
+       /**
+        * Extract a parent name from a passed route.
+        *
+        * @param {string} route The route to extract a name from.
+        */
+       wp.api.utils.extractParentName = function( route ) {
+               var name,
+                       lastSlash = route.lastIndexOf( '_id>[\\d]+)/' );
+
+               if ( lastSlash < 0 ) {
+                       return '';
+               }
+               name = route.substr( 0, lastSlash - 1 );
+               name = name.split( '/' );
+               name.pop();
+               name = name.pop();
+               return name;
+       };
+
+       /**
+        * Add args and options to a model prototype from a route's endpoints.
+        *
+        * @param {array}  routeEndpoints Array of route endpoints.
+        * @param {Object} modelInstance  An instance of the model (or collection)
+        *                                to add the args to.
+        */
+       wp.api.utils.decorateFromRoute = function( routeEndpoints, modelInstance ) {
+
+               /**
+                * Build the args based on route endpoint data.
+                */
+               _.each( routeEndpoints, function( routeEndpoint ) {
+
+                       // Add post and edit endpoints as model args.
+                       if ( _.includes( routeEndpoint.methods, 'POST' ) || _.includes( routeEndpoint.methods, 'PUT' ) ) {
+
+                               // Add any non empty args, merging them into the args object.
+                               if ( ! _.isEmpty( routeEndpoint.args ) ) {
+
+                                       // Set as defauls if no args yet.
+                                       if ( _.isEmpty( modelInstance.prototype.args ) ) {
+                                               modelInstance.prototype.args = routeEndpoint.args;
+                                       } else {
+
+                                               // We already have args, merge these new args in.
+                                               modelInstance.prototype.args = _.union( routeEndpoint.args, modelInstance.prototype.defaults );
+                                       }
+                               }
+                       } else {
+
+                               // Add GET method as model options.
+                               if ( _.includes( routeEndpoint.methods, 'GET' ) ) {
+
+                                       // Add any non empty args, merging them into the defaults object.
+                                       if ( ! _.isEmpty( routeEndpoint.args ) ) {
+
+                                               // Set as defauls if no defaults yet.
+                                               if ( _.isEmpty( modelInstance.prototype.options ) ) {
+                                                       modelInstance.prototype.options = routeEndpoint.args;
+                                               } else {
+
+                                                       // We already have options, merge these new args in.
+                                                       modelInstance.prototype.options = _.union( routeEndpoint.args, modelInstance.prototype.options );
+                                               }
+                                       }
+
+                               }
+                       }
+
+               } );
+
+       };
+
+       /**
+        * Add mixins and helpers to models depending on their defaults.
+        *
+        * @param {Backbone Model} model          The model to attach helpers and mixins to.
+        * @param {string}         modelClassName The classname of the constructed model.
+        * @param {Object}             loadingObjects An object containing the models and collections we are building.
+        */
+       wp.api.utils.addMixinsAndHelpers = function( model, modelClassName, loadingObjects ) {
+
+               var hasDate = false,
+
+                       /**
+                        * Array of parseable dates.
+                        *
+                        * @type {string[]}.
+                        */
+                       parseableDates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ],
+
+                       /**
+                        * Mixin for all content that is time stamped.
+                        *
+                        * This mixin converts between mysql timestamps and JavaScript Dates when syncing a model
+                        * to or from the server. For example, a date stored as `2015-12-27T21:22:24` on the server
+                        * gets expanded to `Sun Dec 27 2015 14:22:24 GMT-0700 (MST)` when the model is fetched.
+                        *
+                        * @type {{toJSON: toJSON, parse: parse}}.
+                        */
+                       TimeStampedMixin = {
+
+                               /**
+                                * Prepare a JavaScript Date for transmitting to the server.
+                                *
+                                * This helper function accepts a field and Date object. It converts the passed Date
+                                * to an ISO string and sets that on the model field.
+                                *
+                                * @param {Date}   date   A JavaScript date object. WordPress expects dates in UTC.
+                                * @param {string} field  The date field to set. One of 'date', 'date_gmt', 'date_modified'
+                                *                        or 'date_modified_gmt'. Optional, defaults to 'date'.
+                                */
+                               setDate: function( date, field ) {
+                                       var theField = field || 'date';
+
+                                       // Don't alter non parsable date fields.
+                                       if ( _.indexOf( parseableDates, theField ) < 0 ) {
+                                               return false;
+                                       }
+
+                                       this.set( theField, date.toISOString() );
+                               },
+
+                               /**
+                                * Get a JavaScript Date from the passed field.
+                                *
+                                * WordPress returns 'date' and 'date_modified' in the timezone of the server as well as
+                                * UTC dates as 'date_gmt' and 'date_modified_gmt'. Draft posts do not include UTC dates.
+                                *
+                                * @param {string} field  The date field to set. One of 'date', 'date_gmt', 'date_modified'
+                                *                        or 'date_modified_gmt'. Optional, defaults to 'date'.
+                                */
+                               getDate: function( field ) {
+                                       var theField   = field || 'date',
+                                               theISODate = this.get( theField );
+
+                                       // Only get date fields and non null values.
+                                       if ( _.indexOf( parseableDates, theField ) < 0 || _.isNull( theISODate ) ) {
+                                               return false;
+                                       }
+
+                                       return new Date( wp.api.utils.parseISO8601( theISODate ) );
+                               }
+                       },
+
+                       /**
+                        * Build a helper function to retrieve related model.
+                        *
+                        * @param  {string} parentModel      The parent model.
+                        * @param  {int}    modelId          The model ID if the object to request
+                        * @param  {string} modelName        The model name to use when constructing the model.
+                        * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
+                        * @param  {string} embedCheckField  Which model field to check to see if the model has data.
+                        *
+                        * @return {Deferred.promise}        A promise which resolves to the constructed model.
+                        */
+                       buildModelGetter = function( parentModel, modelId, modelName, embedSourcePoint, embedCheckField ) {
+                               var getModel, embeddeds, attributes, deferred;
+
+                               deferred  = jQuery.Deferred();
+                               embeddeds = parentModel.get( '_embedded' ) || {};
+
+                               // Verify that we have a valied object id.
+                               if ( ! _.isNumber( modelId ) || 0 === modelId ) {
+                                       deferred.reject();
+                                       return deferred;
+                               }
+
+                               // If we have embedded object data, use that when constructing the getModel.
+                               if ( embeddeds[ embedSourcePoint ] ) {
+                                       attributes = _.findWhere( embeddeds[ embedSourcePoint ], { id: modelId } );
+                               }
+
+                               // Otherwise use the modelId.
+                               if ( ! attributes ) {
+                                       attributes = { id: modelId };
+                               }
+
+                               // Create the new getModel model.
+                               getModel = new wp.api.models[ modelName ]( attributes );
+
+                               // If we didn’t have an embedded getModel, fetch the getModel data.
+                               if ( ! getModel.get( embedCheckField ) ) {
+                                       getModel.fetch( { success: function( getModel ) {
+                                               deferred.resolve( getModel );
+                                       } } );
+                               } else {
+                                       deferred.resolve( getModel );
+                               }
+
+                               // Return a promise.
+                               return deferred.promise();
+                       },
+
+                       /**
+                        * Build a helper to retrieve a collection.
+                        *
+                        * @param  {string} parentModel      The parent model.
+                        * @param  {string} collectionName   The name to use when constructing the collection.
+                        * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
+                        * @param  {string} embedIndex       An addiitonal optional index for the _embed data.
+                        *
+                        * @return {Deferred.promise}        A promise which resolves to the constructed collection.
+                        */
+                       buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) {
+                               /**
+                                * Returns a promise that resolves to the requested collection
+                                *
+                                * Uses the embedded data if available, otherwises fetches the
+                                * data from the server.
+                                *
+                                * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ]
+                                * collection.
+                                */
+                               var postId, embeddeds, getObjects,
+                                       classProperties = '',
+                                       properties      = '',
+                                       deferred        = jQuery.Deferred();
+
+                               postId    = parentModel.get( 'id' );
+                               embeddeds = parentModel.get( '_embedded' ) || {};
+
+                               // Verify that we have a valied post id.
+                               if ( ! _.isNumber( postId ) || 0 === postId ) {
+                                       deferred.reject();
+                                       return deferred;
+                               }
+
+                               // If we have embedded getObjects data, use that when constructing the getObjects.
+                               if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) {
+
+                                       // Some embeds also include an index offset, check for that.
+                                       if ( _.isUndefined( embedIndex ) ) {
+
+                                               // Use the embed source point directly.
+                                               properties = embeddeds[ embedSourcePoint ];
+                                       } else {
+
+                                               // Add the index to the embed source point.
+                                               properties = embeddeds[ embedSourcePoint ][ embedIndex ];
+                                       }
+                               } else {
+
+                                       // Otherwise use the postId.
+                                       classProperties = { parent: postId };
+                               }
+
+                               // Create the new getObjects collection.
+                               getObjects = new wp.api.collections[ collectionName ]( properties, classProperties );
+
+                               // If we didn’t have embedded getObjects, fetch the getObjects data.
+                               if ( _.isUndefined( getObjects.models[0] ) ) {
+                                       getObjects.fetch( { success: function( getObjects ) {
+
+                                               // Add a helper 'parent_post' attribute onto the model.
+                                               setHelperParentPost( getObjects, postId );
+                                               deferred.resolve( getObjects );
+                                       } } );
+                               } else {
+
+                                       // Add a helper 'parent_post' attribute onto the model.
+                                       setHelperParentPost( getObjects, postId );
+                                       deferred.resolve( getObjects );
+                               }
+
+                               // Return a promise.
+                               return deferred.promise();
+
+                       },
+
+                       /**
+                        * Set the model post parent.
+                        */
+                       setHelperParentPost = function( collection, postId ) {
+
+                               // Attach post_parent id to the collection.
+                               _.each( collection.models, function( model ) {
+                                       model.set( 'parent_post', postId );
+                               } );
+                       },
+
+                       /**
+                        * Add a helper funtion to handle post Meta.
+                        */
+                       MetaMixin = {
+                               getMeta: function() {
+                                       return buildCollectionGetter( this, 'PostMeta', 'https://api.w.org/meta' );
+                               }
+                       },
+
+                       /**
+                        * Add a helper funtion to handle post Revisions.
+                        */
+                       RevisionsMixin = {
+                               getRevisions: function() {
+                                       return buildCollectionGetter( this, 'PostRevisions' );
+                               }
+                       },
+
+                       /**
+                        * Add a helper funtion to handle post Tags.
+                        */
+                       TagsMixin = {
+
+                               /**
+                                * Get the tags for a post.
+                                *
+                                * @return {Deferred.promise} promise Resolves to an array of tags.
+                                */
+                               getTags: function() {
+                                       var tagIds = this.get( 'tags' ),
+                                               tags  = new wp.api.collections.Tags();
+
+                                       // Resolve with an empty array if no tags.
+                                       if ( _.isEmpty( tagIds ) ) {
+                                               return jQuery.Deferred().resolve( [] );
+                                       }
+
+                                       return tags.fetch( { data: { include: tagIds } } );
+                               },
+
+                               /**
+                                * Set the tags for a post.
+                                *
+                                * Accepts an array of tag slugs, or a Tags collection.
+                                *
+                                * @param {array|Backbone.Collection} tags The tags to set on the post.
+                                *
+                                */
+                               setTags: function( tags ) {
+                                       var allTags, newTag,
+                                               self = this,
+                                               newTags = [];
+
+                                       if ( _.isString( tags ) ) {
+                                               return false;
+                                       }
+
+                                       // If this is an array of slugs, build a collection.
+                                       if ( _.isArray( tags ) ) {
+
+                                               // Get all the tags.
+                                               allTags = new wp.api.collections.Tags();
+                                               allTags.fetch( {
+                                                       data:    { per_page: 100 },
+                                                       success: function( alltags ) {
+
+                                                               // Find the passed tags and set them up.
+                                                               _.each( tags, function( tag ) {
+                                                                       newTag = new wp.api.models.Tag( alltags.findWhere( { slug: tag } ) );
+
+                                                                       // Tie the new tag to the post.
+                                                                       newTag.set( 'parent_post', self.get( 'id' ) );
+
+                                                                       // Add the new tag to the collection.
+                                                                       newTags.push( newTag );
+                                                               } );
+                                                               tags = new wp.api.collections.Tags( newTags );
+                                                               self.setTagsWithCollection( tags );
+                                                       }
+                                               } );
+
+                                       } else {
+                                               this.setTagsWithCollection( tags );
+                                       }
+                               },
+
+                               /**
+                                * Set the tags for a post.
+                                *
+                                * Accepts a Tags collection.
+                                *
+                                * @param {array|Backbone.Collection} tags The tags to set on the post.
+                                *
+                                */
+                               setTagsWithCollection: function( tags ) {
+
+                                       // Pluck out the category ids.
+                                       this.set( 'tags', tags.pluck( 'id' ) );
+                                       return this.save();
+                               }
+                       },
+
+                       /**
+                        * Add a helper funtion to handle post Categories.
+                        */
+                       CategoriesMixin = {
+
+                               /**
+                                * Get a the categories for a post.
+                                *
+                                * @return {Deferred.promise} promise Resolves to an array of categories.
+                                */
+                               getCategories: function() {
+                                       var categoryIds = this.get( 'categories' ),
+                                               categories  = new wp.api.collections.Categories();
+
+                                       // Resolve with an empty array if no categories.
+                                       if ( _.isEmpty( categoryIds ) ) {
+                                               return jQuery.Deferred().resolve( [] );
+                                       }
+
+                                       return categories.fetch( { data: { include: categoryIds } } );
+                               },
+
+                               /**
+                                * Set the categories for a post.
+                                *
+                                * Accepts an array of category slugs, or a Categories collection.
+                                *
+                                * @param {array|Backbone.Collection} categories The categories to set on the post.
+                                *
+                                */
+                               setCategories: function( categories ) {
+                                       var allCategories, newCategory,
+                                               self = this,
+                                               newCategories = [];
+
+                                       if ( _.isString( categories ) ) {
+                                               return false;
+                                       }
+
+                                       // If this is an array of slugs, build a collection.
+                                       if ( _.isArray( categories ) ) {
+
+                                               // Get all the categories.
+                                               allCategories = new wp.api.collections.Categories();
+                                               allCategories.fetch( {
+                                                       data:    { per_page: 100 },
+                                                       success: function( allcats ) {
+
+                                                               // Find the passed categories and set them up.
+                                                               _.each( categories, function( category ) {
+                                                                       newCategory = new wp.api.models.Category( allcats.findWhere( { slug: category } ) );
+
+                                                                       // Tie the new category to the post.
+                                                                       newCategory.set( 'parent_post', self.get( 'id' ) );
+
+                                                                       // Add the new category to the collection.
+                                                                       newCategories.push( newCategory );
+                                                               } );
+                                                               categories = new wp.api.collections.Categories( newCategories );
+                                                               self.setCategoriesWithCollection( categories );
+                                                       }
+                                               } );
+
+                                       } else {
+                                               this.setCategoriesWithCollection( categories );
+                                       }
+
+                               },
+
+                               /**
+                                * Set the categories for a post.
+                                *
+                                * Accepts Categories collection.
+                                *
+                                * @param {array|Backbone.Collection} categories The categories to set on the post.
+                                *
+                                */
+                               setCategoriesWithCollection: function( categories ) {
+
+                                       // Pluck out the category ids.
+                                       this.set( 'categories', categories.pluck( 'id' ) );
+                                       return this.save();
+                               }
+                       },
+
+                       /**
+                        * Add a helper function to retrieve the author user model.
+                        */
+                       AuthorMixin = {
+                               getAuthorUser: function() {
+                                       return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' );
+                               }
+                       },
+
+                       /**
+                        * Add a helper function to retrieve the featured media.
+                        */
+                       FeaturedMediaMixin = {
+                               getFeaturedMedia: function() {
+                                       return buildModelGetter( this, this.get( 'featured_media' ), 'Media', 'wp:featuredmedia', 'source_url' );
+                               }
+                       };
+
+               // Exit if we don't have valid model defaults.
+               if ( _.isUndefined( model.prototype.args ) ) {
+                       return model;
+               }
+
+               // Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin.
+               _.each( parseableDates, function( theDateKey ) {
+                       if ( ! _.isUndefined( model.prototype.args[ theDateKey ] ) ) {
+                               hasDate = true;
+                       }
+               } );
+
+               // Add the TimeStampedMixin for models that contain a date field.
+               if ( hasDate ) {
+                       model = model.extend( TimeStampedMixin );
+               }
+
+               // Add the AuthorMixin for models that contain an author.
+               if ( ! _.isUndefined( model.prototype.args.author ) ) {
+                       model = model.extend( AuthorMixin );
+               }
+
+               // Add the FeaturedMediaMixin for models that contain a featured_media.
+               if ( ! _.isUndefined( model.prototype.args.featured_media ) ) {
+                       model = model.extend( FeaturedMediaMixin );
+               }
+
+               // Add the CategoriesMixin for models that support categories collections.
+               if ( ! _.isUndefined( model.prototype.args.categories ) ) {
+                       model = model.extend( CategoriesMixin );
+               }
+
+               // Add the MetaMixin for models that support meta collections.
+               if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Meta' ] ) ) {
+                       model = model.extend( MetaMixin );
+               }
+
+               // Add the TagsMixin for models that support tags collections.
+               if ( ! _.isUndefined( model.prototype.args.tags ) ) {
+                       model = model.extend( TagsMixin );
+               }
+
+               // Add the RevisionsMixin for models that support revisions collections.
+               if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) {
+                       model = model.extend( RevisionsMixin );
+               }
+
+               return model;
+       };
+
+})( window );
+
+/* global wpApiSettings:false */
+
+// Suppress warning about parse function's unused "options" argument:
+/* jshint unused:false */
+(function() {
+
+       'use strict';
+
+       var wpApiSettings = window.wpApiSettings || {};
+
+       /**
+        * Backbone base model for all models.
+        */
+       wp.api.WPApiBaseModel = Backbone.Model.extend(
+               /** @lends WPApiBaseModel.prototype  */
+               {
+                       /**
+                        * Set nonce header before every Backbone sync.
+                        *
+                        * @param {string} method.
+                        * @param {Backbone.Model} model.
+                        * @param {{beforeSend}, *} options.
+                        * @returns {*}.
+                        */
+                       sync: function( method, model, options ) {
+                               var beforeSend;
+
+                               options = options || {};
+
+                               // Remove date_gmt if null.
+                               if ( _.isNull( model.get( 'date_gmt' ) ) ) {
+                                       model.unset( 'date_gmt' );
+                               }
+
+                               // Remove slug if empty.
+                               if ( _.isEmpty( model.get( 'slug' ) ) ) {
+                                       model.unset( 'slug' );
+                               }
+
+                               if ( ! _.isUndefined( wpApiSettings.nonce ) && ! _.isNull( wpApiSettings.nonce ) ) {
+                                       beforeSend = options.beforeSend;
+
+                                       // @todo enable option for jsonp endpoints
+                                       // options.dataType = 'jsonp';
+
+                                       options.beforeSend = function( xhr ) {
+                                               xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
+
+                                               if ( beforeSend ) {
+                                                       return beforeSend.apply( this, arguments );
+                                               }
+                                       };
+                               }
+
+                               // Add '?force=true' to use delete method when required.
+                               if ( this.requireForceForDelete && 'delete' === method ) {
+                                       model.url = model.url() + '?force=true';
+                               }
+                               return Backbone.sync( method, model, options );
+                       },
+
+                       /**
+                        * Save is only allowed when the PUT OR POST methods are available for the endpoint.
+                        */
+                       save: function( attrs, options ) {
+
+                               // Do we have the put method, then execute the save.
+                               if ( _.includes( this.methods, 'PUT' ) || _.includes( this.methods, 'POST' ) ) {
+
+                                       // Proxy the call to the original save function.
+                                       return Backbone.Model.prototype.save.call( this, attrs, options );
+                               } else {
+
+                                       // Otherwise bail, disallowing action.
+                                       return false;
+                               }
+                       },
+
+                       /**
+                        * Delete is only allowed when the DELETE method is available for the endpoint.
+                        */
+                       destroy: function( options ) {
+
+                               // Do we have the DELETE method, then execute the destroy.
+                               if ( _.includes( this.methods, 'DELETE' ) ) {
+
+                                       // Proxy the call to the original save function.
+                                       return Backbone.Model.prototype.destroy.call( this, options );
+                               } else {
+
+                                       // Otherwise bail, disallowing action.
+                                       return false;
+                               }
+                       }
+
+               }
+       );
+
+       /**
+        * API Schema model. Contains meta information about the API.
+        */
+       wp.api.models.Schema = wp.api.WPApiBaseModel.extend(
+               /** @lends Schema.prototype  */
+               {
+                       defaults: {
+                               _links: {},
+                               namespace: null,
+                               routes: {}
+                       },
+
+                       initialize: function( attributes, options ) {
+                               var model = this;
+                               options = options || {};
+
+                               wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options );
+
+                               model.apiRoot = options.apiRoot || wpApiSettings.root;
+                               model.versionString = options.versionString || wpApiSettings.versionString;
+                       },
+
+                       url: function() {
+                               return this.apiRoot + this.versionString;
+                       }
+               }
+       );
+})();
+
+( function() {
+
+       'use strict';
+
+       var wpApiSettings = window.wpApiSettings || {};
+
+       /**
+        * Contains basic collection functionality such as pagination.
+        */
+       wp.api.WPApiBaseCollection = Backbone.Collection.extend(
+               /** @lends BaseCollection.prototype  */
+               {
+
+                       /**
+                        * Setup default state.
+                        */
+                       initialize: function( models, options ) {
+                               this.state = {
+                                       data: {},
+                                       currentPage: null,
+                                       totalPages: null,
+                                       totalObjects: null
+                               };
+                               if ( _.isUndefined( options ) ) {
+                                       this.parent = '';
+                               } else {
+                                       this.parent = options.parent;
+                               }
+                       },
+
+                       /**
+                        * Extend Backbone.Collection.sync to add nince and pagination support.
+                        *
+                        * Set nonce header before every Backbone sync.
+                        *
+                        * @param {string} method.
+                        * @param {Backbone.Model} model.
+                        * @param {{success}, *} options.
+                        * @returns {*}.
+                        */
+                       sync: function( method, model, options ) {
+                               var beforeSend, success,
+                                       self = this;
+
+                               options    = options || {};
+                               beforeSend = options.beforeSend;
+
+                               // If we have a localized nonce, pass that along with each sync.
+                               if ( 'undefined' !== typeof wpApiSettings.nonce ) {
+                                       options.beforeSend = function( xhr ) {
+                                               xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
+
+                                               if ( beforeSend ) {
+                                                       return beforeSend.apply( self, arguments );
+                                               }
+                                       };
+                               }
+
+                               // When reading, add pagination data.
+                               if ( 'read' === method ) {
+                                       if ( options.data ) {
+                                               self.state.data = _.clone( options.data );
+
+                                               delete self.state.data.page;
+                                       } else {
+                                               self.state.data = options.data = {};
+                                       }
+
+                                       if ( 'undefined' === typeof options.data.page ) {
+                                               self.state.currentPage  = null;
+                                               self.state.totalPages   = null;
+                                               self.state.totalObjects = null;
+                                       } else {
+                                               self.state.currentPage = options.data.page - 1;
+                                       }
+
+                                       success = options.success;
+                                       options.success = function( data, textStatus, request ) {
+                                               if ( ! _.isUndefined( request ) ) {
+                                                       self.state.totalPages   = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 );
+                                                       self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 );
+                                               }
+
+                                               if ( null === self.state.currentPage ) {
+                                                       self.state.currentPage = 1;
+                                               } else {
+                                                       self.state.currentPage++;
+                                               }
+
+                                               if ( success ) {
+                                                       return success.apply( this, arguments );
+                                               }
+                                       };
+                               }
+
+                               // Continue by calling Bacckbone's sync.
+                               return Backbone.sync( method, model, options );
+                       },
+
+                       /**
+                        * Fetches the next page of objects if a new page exists.
+                        *
+                        * @param {data: {page}} options.
+                        * @returns {*}.
+                        */
+                       more: function( options ) {
+                               options = options || {};
+                               options.data = options.data || {};
+
+                               _.extend( options.data, this.state.data );
+
+                               if ( 'undefined' === typeof options.data.page ) {
+                                       if ( ! this.hasMore() ) {
+                                               return false;
+                                       }
+
+                                       if ( null === this.state.currentPage || this.state.currentPage <= 1 ) {
+                                               options.data.page = 2;
+                                       } else {
+                                               options.data.page = this.state.currentPage + 1;
+                                       }
+                               }
+
+                               return this.fetch( options );
+                       },
+
+                       /**
+                        * Returns true if there are more pages of objects available.
+                        *
+                        * @returns null|boolean.
+                        */
+                       hasMore: function() {
+                               if ( null === this.state.totalPages ||
+                                        null === this.state.totalObjects ||
+                                        null === this.state.currentPage ) {
+                                       return null;
+                               } else {
+                                       return ( this.state.currentPage < this.state.totalPages );
+                               }
+                       }
+               }
+       );
+
+} )();
+
+( function() {
+
+       'use strict';
+
+       var Endpoint, initializedDeferreds = {},
+               wpApiSettings = window.wpApiSettings || {};
+       window.wp = window.wp || {};
+       wp.api    = wp.api || {};
+
+       // If wpApiSettings is unavailable, try the default.
+       if ( _.isEmpty( wpApiSettings ) ) {
+               wpApiSettings.root = window.location.origin + '/wp-json/';
+       }
+
+       Endpoint = Backbone.Model.extend( {
+               defaults: {
+                       apiRoot: wpApiSettings.root,
+                       versionString: wp.api.versionString,
+                       schema: null,
+                       models: {},
+                       collections: {}
+               },
+
+               /**
+                * Initialize the Endpoint model.
+                */
+               initialize: function() {
+                       var model = this, deferred;
+
+                       Backbone.Model.prototype.initialize.apply( model, arguments );
+
+                       deferred = jQuery.Deferred();
+                       model.schemaConstructed = deferred.promise();
+
+                       model.schemaModel = new wp.api.models.Schema( null, {
+                               apiRoot: model.get( 'apiRoot' ),
+                               versionString: model.get( 'versionString' )
+                       } );
+
+                       // When the model loads, resolve the promise.
+                       model.schemaModel.once( 'change', function() {
+                               model.constructFromSchema();
+                               deferred.resolve( model );
+                       } );
+
+                       if ( model.get( 'schema' ) ) {
+
+                               // Use schema supplied as model attribute.
+                               model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) );
+                       } else if (
+                               ! _.isUndefined( sessionStorage ) &&
+                               ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) &&
+                               sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) )
+                       ) {
+
+                               // Used a cached copy of the schema model if available.
+                               model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) );
+                       } else {
+                               model.schemaModel.fetch( {
+                                       /**
+                                        * When the server returns the schema model data, store the data in a sessionCache so we don't
+                                        * have to retrieve it again for this session. Then, construct the models and collections based
+                                        * on the schema model data.
+                                        */
+                                       success: function( newSchemaModel ) {
+
+                                               // Store a copy of the schema model in the session cache if available.
+                                               if ( ! _.isUndefined( sessionStorage ) && wpApiSettings.cacheSchema ) {
+                                                       try {
+                                                               sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) );
+                                                       } catch ( error ) {
+
+                                                               // Fail silently, fixes errors in safari private mode.
+                                                       }
+                                               }
+                                       },
+
+                                       // Log the error condition.
+                                       error: function( err ) {
+                                               window.console.log( err );
+                                       }
+                               } );
+                       }
+               },
+
+               constructFromSchema: function() {
+                       var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects,
+
+                       /**
+                        * Set up the model and collection name mapping options. As the schema is built, the
+                        * model and collection names will be adjusted if they are found in the mapping object.
+                        *
+                        * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options.
+                        *
+                        */
+                       mapping = wpApiSettings.mapping || {
+                               models: {
+                                       'Categories':      'Category',
+                                       'Comments':        'Comment',
+                                       'Pages':           'Page',
+                                       'PagesMeta':       'PageMeta',
+                                       'PagesRevisions':  'PageRevision',
+                                       'Posts':           'Post',
+                                       'PostsCategories': 'PostCategory',
+                                       'PostsRevisions':  'PostRevision',
+                                       'PostsTags':       'PostTag',
+                                       'Schema':          'Schema',
+                                       'Statuses':        'Status',
+                                       'Tags':            'Tag',
+                                       'Taxonomies':      'Taxonomy',
+                                       'Types':           'Type',
+                                       'Users':           'User'
+                               },
+                               collections: {
+                                       'PagesMeta':       'PageMeta',
+                                       'PagesRevisions':  'PageRevisions',
+                                       'PostsCategories': 'PostCategories',
+                                       'PostsMeta':       'PostMeta',
+                                       'PostsRevisions':  'PostRevisions',
+                                       'PostsTags':       'PostTags'
+                               }
+                       };
+
+                       /**
+                        * Iterate thru the routes, picking up models and collections to build. Builds two arrays,
+                        * one for models and one for collections.
+                        */
+                       modelRoutes      = [];
+                       collectionRoutes = [];
+                       schemaRoot       = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' );
+                       loadingObjects   = {};
+
+                       /**
+                        * Tracking objects for models and collections.
+                        */
+                       loadingObjects.models      = routeModel.get( 'models' );
+                       loadingObjects.collections = routeModel.get( 'collections' );
+
+                       _.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) {
+
+                               // Skip the schema root if included in the schema.
+                               if ( index !== routeModel.get( ' versionString' ) &&
+                                               index !== schemaRoot &&
+                                               index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) )
+                               ) {
+
+                                       // Single items end with a regex (or the special case 'me').
+                                       if ( /(?:.*[+)]|\/me)$/.test( index ) ) {
+                                               modelRoutes.push( { index: index, route: route } );
+                                       } else {
+
+                                               // Collections end in a name.
+                                               collectionRoutes.push( { index: index, route: route } );
+                                       }
+                               }
+                       } );
+
+                       /**
+                        * Construct the models.
+                        *
+                        * Base the class name on the route endpoint.
+                        */
+                       _.each( modelRoutes, function( modelRoute ) {
+
+                               // Extract the name and any parent from the route.
+                               var modelClassName,
+                                               routeName  = wp.api.utils.extractRoutePart( modelRoute.index, 2 ),
+                                               parentName = wp.api.utils.extractRoutePart( modelRoute.index, 4 ),
+                                               routeEnd   = wp.api.utils.extractRoutePart( modelRoute.index, 1 );
+
+                               // Handle the special case of the 'me' route.
+                               if ( 'me' === routeEnd ) {
+                                       routeName = 'me';
+                               }
+
+                               // If the model has a parent in its route, add that to its class name.
+                               if ( '' !== parentName && parentName !== routeName ) {
+                                       modelClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName );
+                                       modelClassName = mapping.models[ modelClassName ] || modelClassName;
+                                       loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
+
+                                               // Return a constructed url based on the parent and id.
+                                               url: function() {
+                                                       var url = routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
+                                                                       parentName +  '/' +
+                                                                       ( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ?
+                                                                               this.get( 'parent_post' ) :
+                                                                               this.get( 'parent' ) ) + '/' +
+                                                                       routeName;
+                                                       if ( ! _.isUndefined( this.get( 'id' ) ) ) {
+                                                               url +=  '/' + this.get( 'id' );
+                                                       }
+                                                       return url;
+                                               },
+
+                                               // Include a reference to the original route object.
+                                               route: modelRoute,
+
+                                               // Include a reference to the original class name.
+                                               name: modelClassName,
+
+                                               // Include the array of route methods for easy reference.
+                                               methods: modelRoute.route.methods,
+
+                                               initialize: function() {
+
+                                                       /**
+                                                        * Posts and pages support trashing, other types don't support a trash
+                                                        * and require that you pass ?force=true to actually delete them.
+                                                        *
+                                                        * @todo we should be getting trashability from the Schema, not hard coding types here.
+                                                        */
+                                                       if (
+                                                               'Posts' !== this.name &&
+                                                               'Pages' !== this.name &&
+                                                               _.includes( this.methods, 'DELETE' )
+                                                       ) {
+                                                               this.requireForceForDelete = true;
+                                                       }
+                                               }
+                                       } );
+                               } else {
+
+                                       // This is a model without a parent in its route
+                                       modelClassName = wp.api.utils.capitalize( routeName );
+                                       modelClassName = mapping.models[ modelClassName ] || modelClassName;
+                                       loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
+
+                                               // Function that returns a constructed url based on the id.
+                                               url: function() {
+                                                       var url = routeModel.get( 'apiRoot' ) +
+                                                               routeModel.get( 'versionString' ) +
+                                                               ( ( 'me' === routeName ) ? 'users/me' : routeName );
+
+                                                       if ( ! _.isUndefined( this.get( 'id' ) ) ) {
+                                                               url +=  '/' + this.get( 'id' );
+                                                       }
+                                                       return url;
+                                               },
+
+                                               // Include a reference to the original route object.
+                                               route: modelRoute,
+
+                                               // Include a reference to the original class name.
+                                               name: modelClassName,
+
+                                               // Include the array of route methods for easy reference.
+                                               methods: modelRoute.route.methods
+                                       } );
+                               }
+
+                               // Add defaults to the new model, pulled form the endpoint.
+                               wp.api.utils.decorateFromRoute( modelRoute.route.endpoints, loadingObjects.models[ modelClassName ] );
+
+                       } );
+
+                       /**
+                        * Construct the collections.
+                        *
+                        * Base the class name on the route endpoint.
+                        */
+                       _.each( collectionRoutes, function( collectionRoute ) {
+
+                               // Extract the name and any parent from the route.
+                               var collectionClassName, modelClassName,
+                                               routeName  = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ),
+                                               parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 3 );
+
+                               // If the collection has a parent in its route, add that to its class name.
+                               if ( '' !== parentName && parentName !== routeName ) {
+
+                                       collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName );
+                                       modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
+                                       collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
+                                       loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
+
+                                               // Function that returns a constructed url passed on the parent.
+                                               url: function() {
+                                                       return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
+                                                                       parentName + '/' + this.parent + '/' +
+                                                                       routeName;
+                                               },
+
+                                               // Specify the model that this collection contains.
+                                               model: loadingObjects.models[ modelClassName ],
+
+                                               // Include a reference to the original class name.
+                                               name: collectionClassName,
+
+                                               // Include a reference to the original route object.
+                                               route: collectionRoute,
+
+                                               // Include the array of route methods for easy reference.
+                                               methods: collectionRoute.route.methods
+                                       } );
+                               } else {
+
+                                       // This is a collection without a parent in its route.
+                                       collectionClassName = wp.api.utils.capitalize( routeName );
+                                       modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
+                                       collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
+                                       loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
+
+                                               // For the url of a root level collection, use a string.
+                                               url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName,
+
+                                               // Specify the model that this collection contains.
+                                               model: loadingObjects.models[ modelClassName ],
+
+                                               // Include a reference to the original class name.
+                                               name: collectionClassName,
+
+                                               // Include a reference to the original route object.
+                                               route: collectionRoute,
+
+                                               // Include the array of route methods for easy reference.
+                                               methods: collectionRoute.route.methods
+                                       } );
+                               }
+
+                               // Add defaults to the new model, pulled form the endpoint.
+                               wp.api.utils.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] );
+                       } );
+
+                       // Add mixins and helpers for each of the models.
+                       _.each( loadingObjects.models, function( model, index ) {
+                               loadingObjects.models[ index ] = wp.api.utils.addMixinsAndHelpers( model, index, loadingObjects );
+                       } );
+
+               }
+
+       } );
+
+       wp.api.endpoints = new Backbone.Collection( {
+               model: Endpoint
+       } );
+
+       /**
+        * Initialize the wp-api, optionally passing the API root.
+        *
+        * @param {object} [args]
+        * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root.
+        * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root.
+        * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided.
+        */
+       wp.api.init = function( args ) {
+               var endpoint, attributes = {}, deferred, promise;
+
+               args                     = args || {};
+               attributes.apiRoot       = args.apiRoot || wpApiSettings.root;
+               attributes.versionString = args.versionString || wpApiSettings.versionString;
+               attributes.schema        = args.schema || null;
+               if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) {
+                       attributes.schema = wpApiSettings.schema;
+               }
+
+               if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) {
+                       endpoint = wp.api.endpoints.findWhere( { apiRoot: attributes.apiRoot, versionString: attributes.versionString } );
+                       if ( ! endpoint ) {
+                               endpoint = new Endpoint( attributes );
+                               wp.api.endpoints.add( endpoint );
+                       }
+                       deferred = jQuery.Deferred();
+                       promise = deferred.promise();
+
+                       endpoint.schemaConstructed.done( function( endpoint ) {
+
+                               // Map the default endpoints, extending any already present items (including Schema model).
+                               wp.api.models      = _.extend( endpoint.get( 'models' ), wp.api.models );
+                               wp.api.collections = _.extend( endpoint.get( 'collections' ), wp.api.collections );
+                               deferred.resolveWith( wp.api, [ endpoint ] );
+                       } );
+                       initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise;
+               }
+               return initializedDeferreds[ attributes.apiRoot + attributes.versionString ];
+       };
+
+       /**
+        * Construct the default endpoints and add to an endpoints collection.
+        */
+
+       // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready.
+       wp.api.loadPromise = wp.api.init();
+
+} )();
</ins></span></pre></div>
<a id="trunksrcwpincludesoptionphp"></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/option.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/option.php  2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/option.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1708,6 +1708,115 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Register default settings available in WordPress.
+ *
+ * The settings registered here are primarily useful for the REST API, so this
+ * does not encompass all settings available in WordPress.
+ *
+ * @since 4.7.0
+ */
+function register_initial_settings() {
+       register_setting( 'general', 'blogname', array(
+               'show_in_rest' => array(
+                       'name' => 'title',
+               ),
+               'type'         => 'string',
+               'description'  => __( 'Site title.' ),
+       ) );
+
+       register_setting( 'general', 'blogdescription', array(
+               'show_in_rest' => array(
+                       'name' => 'description',
+               ),
+               'type'         => 'string',
+               'description'  => __( 'Site description.' ),
+       ) );
+
+       register_setting( 'general', 'siteurl', array(
+               'show_in_rest' => array(
+                       'name'    => 'url',
+                       'schema'  => array(
+                               'format' => 'uri',
+                       ),
+               ),
+               'type'         => 'string',
+               'description'  => __( 'Site URL.' ),
+       ) );
+
+       register_setting( 'general', 'admin_email', array(
+               'show_in_rest' => array(
+                       'name'    => 'email',
+                       'schema'  => array(
+                               'format' => 'email',
+                       ),
+               ),
+               'type'         => 'string',
+               'description'  => __( 'This address is used for admin purposes. If you change this we will send you an email at your new address to confirm it. The new address will not become active until confirmed.' ),
+       ) );
+
+       register_setting( 'general', 'timezone_string', array(
+               'show_in_rest' => array(
+                       'name' => 'timezone',
+               ),
+               'type'         => 'string',
+               'description'  => __( 'A city in the same timezone as you.' ),
+       ) );
+
+       register_setting( 'general', 'date_format', array(
+               'show_in_rest' => true,
+               'type'         => 'string',
+               'description'  => __( 'A date format for all date strings.' ),
+       ) );
+
+       register_setting( 'general', 'time_format', array(
+               'show_in_rest' => true,
+               'type'         => 'string',
+               'description'  => __( 'A time format for all time strings.' ),
+       ) );
+
+       register_setting( 'general', 'start_of_week', array(
+               'show_in_rest' => true,
+               'type'         => 'number',
+               'description'  => __( 'A day number of the week that the week should start on.' ),
+       ) );
+
+       register_setting( 'general', 'WPLANG', array(
+               'show_in_rest' => array(
+                       'name' => 'language',
+               ),
+               'type'         => 'string',
+               'description'  => __( 'WordPress locale code.' ),
+               'default'      => 'en_US',
+       ) );
+
+       register_setting( 'writing', 'use_smilies', array(
+               'show_in_rest' => true,
+               'type'         => 'boolean',
+               'description'  => __( 'Convert emoticons like :-) and :-P to graphics on display.' ),
+               'default'      => true,
+       ) );
+
+       register_setting( 'writing', 'default_category', array(
+               'show_in_rest' => true,
+               'type'         => 'number',
+               'description'  => __( 'Default category.' ),
+       ) );
+
+       register_setting( 'writing', 'default_post_format', array(
+               'show_in_rest' => true,
+               'type'         => 'string',
+               'description'  => __( 'Default post format.' ),
+       ) );
+
+       register_setting( 'reading', 'posts_per_page', array(
+               'show_in_rest' => true,
+               'type'         => 'number',
+               'description'  => __( 'Blog pages show at most.' ),
+               'default'      => 10,
+       ) );
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Register a setting and its data.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.7.0
</span></span></pre></div>
<a id="trunksrcwpincludespostphp"></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/post.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/post.php    2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/post.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -33,6 +33,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'query_var' => false,
</span><span class="cx" style="display: block; padding: 0 10px">                'delete_with_user' => true,
</span><span class="cx" style="display: block; padding: 0 10px">                'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'show_in_rest' => true,
+               'rest_base' => 'posts',
+               'rest_controller_class' => 'WP_REST_Posts_Controller',
</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">        register_post_type( 'page', array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -51,6 +54,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'query_var' => false,
</span><span class="cx" style="display: block; padding: 0 10px">                'delete_with_user' => true,
</span><span class="cx" style="display: block; padding: 0 10px">                'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'page-attributes', 'custom-fields', 'comments', 'revisions' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'show_in_rest' => true,
+               'rest_base' => 'pages',
+               'rest_controller_class' => 'WP_REST_Posts_Controller',
</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">        register_post_type( 'attachment', array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -76,6 +82,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'show_in_nav_menus' => false,
</span><span class="cx" style="display: block; padding: 0 10px">                'delete_with_user' => true,
</span><span class="cx" style="display: block; padding: 0 10px">                'supports' => array( 'title', 'author', 'comments' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'show_in_rest' => true,
+               'rest_base' => 'media',
+               'rest_controller_class' => 'WP_REST_Attachments_Controller',
</ins><span class="cx" style="display: block; padding: 0 10px">         ) );
</span><span class="cx" style="display: block; padding: 0 10px">        add_post_type_support( 'attachment:audio', 'thumbnail' );
</span><span class="cx" style="display: block; padding: 0 10px">        add_post_type_support( 'attachment:video', 'thumbnail' );
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestattachmentscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-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-attachments-controller.php                         (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,607 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
+
+       /**
+        * Determine the allowed query_vars for a get_items() response and
+        * prepare for WP_Query.
+        *
+        * @param array           $prepared_args Optional. Array of prepared arguments.
+        * @param WP_REST_Request $request       Optional. Request to prepare items for.
+        * @return array Array of query arguments.
+        */
+       protected function prepare_items_query( $prepared_args = array(), $request = null ) {
+               $query_args = parent::prepare_items_query( $prepared_args, $request );
+               if ( empty( $query_args['post_status'] ) || ! in_array( $query_args['post_status'], array( 'inherit', 'private', 'trash' ), true ) ) {
+                       $query_args['post_status'] = 'inherit';
+               }
+               $media_types = $this->get_media_types();
+               if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
+                       $query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
+               }
+               if ( ! empty( $request['mime_type'] ) ) {
+                       $parts = explode( '/', $request['mime_type'] );
+                       if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
+                               $query_args['post_mime_type'] = $request['mime_type'];
+                       }
+               }
+               return $query_args;
+       }
+
+       /**
+        * Check if a given request has access to create an attachment.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|true Boolean true if the attachment may be created, or a WP_Error if not.
+        */
+       public function create_item_permissions_check( $request ) {
+               $ret = parent::create_item_permissions_check( $request );
+               if ( ! $ret || is_wp_error( $ret ) ) {
+                       return $ret;
+               }
+
+               if ( ! current_user_can( 'upload_files' ) ) {
+                       return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to upload media on this site.' ), array( 'status' => 400 ) );
+               }
+
+               // Attaching media to a post requires ability to edit said post.
+               if ( ! empty( $request['post'] ) ) {
+                       $parent = $this->get_post( (int) $request['post'] );
+                       $post_parent_type = get_post_type_object( $parent->post_type );
+                       if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
+                               return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to upload media to this resource.' ), array( 'status' => rest_authorization_required_code() ) );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Create a single attachment.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
+        */
+       public function create_item( $request ) {
+
+               if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
+                       return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
+               }
+
+               // Get the file via $_FILES or raw data
+               $files = $request->get_file_params();
+               $headers = $request->get_headers();
+               if ( ! empty( $files ) ) {
+                       $file = $this->upload_from_file( $files, $headers );
+               } else {
+                       $file = $this->upload_from_data( $request->get_body(), $headers );
+               }
+
+               if ( is_wp_error( $file ) ) {
+                       return $file;
+               }
+
+               $name       = basename( $file['file'] );
+               $name_parts = pathinfo( $name );
+               $name       = trim( substr( $name, 0, -(1 + strlen( $name_parts['extension'] ) ) ) );
+
+               $url     = $file['url'];
+               $type    = $file['type'];
+               $file    = $file['file'];
+
+               // use image exif/iptc data for title and caption defaults if possible
+               // @codingStandardsIgnoreStart
+               $image_meta = @wp_read_image_metadata( $file );
+               // @codingStandardsIgnoreEnd
+               if ( ! empty( $image_meta ) ) {
+                       if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
+                               $request['title'] = $image_meta['title'];
+                       }
+
+                       if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
+                               $request['caption'] = $image_meta['caption'];
+                       }
+               }
+
+               $attachment = $this->prepare_item_for_database( $request );
+               $attachment->file = $file;
+               $attachment->post_mime_type = $type;
+               $attachment->guid = $url;
+
+               if ( empty( $attachment->post_title ) ) {
+                       $attachment->post_title = preg_replace( '/\.[^.]+$/', '', basename( $file ) );
+               }
+
+               $id = wp_insert_post( $attachment, true );
+               if ( is_wp_error( $id ) ) {
+                       if ( 'db_update_error' === $id->get_error_code() ) {
+                               $id->add_data( array( 'status' => 500 ) );
+                       } else {
+                               $id->add_data( array( 'status' => 400 ) );
+                       }
+                       return $id;
+               }
+               $attachment = $this->get_post( $id );
+
+               // Include admin functions to get access to wp_generate_attachment_metadata().
+               require_once ABSPATH . 'wp-admin/includes/admin.php';
+
+               wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
+
+               if ( isset( $request['alt_text'] ) ) {
+                       update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $attachment, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $attachment, $request );
+               $response = rest_ensure_response( $response );
+               $response->set_status( 201 );
+               $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );
+
+               /**
+                * Fires after a single attachment is created or updated via the REST API.
+                *
+                * @param object          $attachment Inserted attachment.
+                * @param WP_REST_Request $request    The request sent to the API.
+                * @param boolean         $creating   True when creating an attachment, false when updating.
+                */
+               do_action( 'rest_insert_attachment', $attachment, $request, true );
+
+               return $response;
+
+       }
+
+       /**
+        * Update a single post.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
+        */
+       public function update_item( $request ) {
+               if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
+                       return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
+               }
+               $response = parent::update_item( $request );
+               if ( is_wp_error( $response ) ) {
+                       return $response;
+               }
+
+               $response = rest_ensure_response( $response );
+               $data = $response->get_data();
+
+               if ( isset( $request['alt_text'] ) ) {
+                       update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
+               }
+
+               $attachment = $this->get_post( $request['id'] );
+
+               $fields_update = $this->update_additional_fields_for_object( $attachment, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $attachment, $request );
+               $response = rest_ensure_response( $response );
+
+               /* This action is documented in lib/endpoints/class-wp-rest-attachments-controller.php */
+               do_action( 'rest_insert_attachment', $data, $request, false );
+
+               return $response;
+       }
+
+       /**
+        * Prepare a single attachment for create or update.
+        *
+        * @param WP_REST_Request $request Request object.
+        * @return WP_Error|stdClass $prepared_attachment Post object.
+        */
+       protected function prepare_item_for_database( $request ) {
+               $prepared_attachment = parent::prepare_item_for_database( $request );
+
+               if ( isset( $request['caption'] ) ) {
+                       $prepared_attachment->post_excerpt = $request['caption'];
+               }
+
+               if ( isset( $request['description'] ) ) {
+                       $prepared_attachment->post_content = $request['description'];
+               }
+
+               if ( isset( $request['post'] ) ) {
+                       $prepared_attachment->post_parent = (int) $request['post'];
+               }
+
+               return $prepared_attachment;
+       }
+
+       /**
+        * Prepare a single attachment output for response.
+        *
+        * @param WP_Post         $post    Post object.
+        * @param WP_REST_Request $request Request object.
+        * @return WP_REST_Response Response object.
+        */
+       public function prepare_item_for_response( $post, $request ) {
+               $response = parent::prepare_item_for_response( $post, $request );
+               $data = $response->get_data();
+
+               $data['alt_text']      = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
+               $data['caption']       = $post->post_excerpt;
+               $data['description']   = $post->post_content;
+               $data['media_type']    = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
+               $data['mime_type']     = $post->post_mime_type;
+               $data['media_details'] = wp_get_attachment_metadata( $post->ID );
+               $data['post']          = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
+               $data['source_url']    = wp_get_attachment_url( $post->ID );
+
+               // Ensure empty details is an empty object.
+               if ( empty( $data['media_details'] ) ) {
+                       $data['media_details'] = new stdClass;
+               } elseif ( ! empty( $data['media_details']['sizes'] ) ) {
+
+                       foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
+
+                               if ( isset( $size_data['mime-type'] ) ) {
+                                       $size_data['mime_type'] = $size_data['mime-type'];
+                                       unset( $size_data['mime-type'] );
+                               }
+
+                               // Use the same method image_downsize() does.
+                               $image_src = wp_get_attachment_image_src( $post->ID, $size );
+                               if ( ! $image_src ) {
+                                       continue;
+                               }
+
+                               $size_data['source_url'] = $image_src[0];
+                       }
+
+                       $full_src = wp_get_attachment_image_src( $post->ID, 'full' );
+                       if ( ! empty( $full_src ) ) {
+                               $data['media_details']['sizes']['full'] = array(
+                                       'file'          => wp_basename( $full_src[0] ),
+                                       'width'         => $full_src[1],
+                                       'height'        => $full_src[2],
+                                       'mime_type'     => $post->post_mime_type,
+                                       'source_url'    => $full_src[0],
+                                       );
+                       }
+               } else {
+                       $data['media_details']['sizes'] = new stdClass;
+               }
+
+               $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+
+               $data = $this->filter_response_by_context( $data, $context );
+
+               // Wrap the data in a response object.
+               $response = rest_ensure_response( $data );
+
+               $response->add_links( $this->prepare_links( $post ) );
+
+               /**
+                * Filter an attachment returned from the API.
+                *
+                * Allows modification of the attachment right before it is returned.
+                *
+                * @param WP_REST_Response  $response   The response object.
+                * @param WP_Post           $post       The original attachment post.
+                * @param WP_REST_Request   $request    Request used to generate the response.
+                */
+               return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
+       }
+
+       /**
+        * Get the Attachment's schema, conforming to JSON Schema.
+        *
+        * @return array Item schema as an array.
+        */
+       public function get_item_schema() {
+
+               $schema = parent::get_item_schema();
+
+               $schema['properties']['alt_text'] = array(
+                       'description'     => __( 'Alternative text to display when resource is not displayed.' ),
+                       'type'            => 'string',
+                       'context'         => array( 'view', 'edit', 'embed' ),
+                       'arg_options'     => array(
+                               'sanitize_callback' => 'sanitize_text_field',
+                       ),
+               );
+               $schema['properties']['caption'] = array(
+                       'description'     => __( 'The caption for the resource.' ),
+                       'type'            => 'string',
+                       'context'         => array( 'view', 'edit' ),
+                       'arg_options'     => array(
+                               'sanitize_callback' => 'wp_filter_post_kses',
+                       ),
+               );
+               $schema['properties']['description'] = array(
+                       'description'     => __( 'The description for the resource.' ),
+                       'type'            => 'string',
+                       'context'         => array( 'view', 'edit' ),
+                       'arg_options'     => array(
+                               'sanitize_callback' => 'wp_filter_post_kses',
+                       ),
+               );
+               $schema['properties']['media_type'] = array(
+                       'description'     => __( 'Type of resource.' ),
+                       'type'            => 'string',
+                       'enum'            => array( 'image', 'file' ),
+                       'context'         => array( 'view', 'edit', 'embed' ),
+                       'readonly'        => true,
+               );
+               $schema['properties']['mime_type'] = array(
+                       'description'     => __( 'MIME type of resource.' ),
+                       'type'            => 'string',
+                       'context'         => array( 'view', 'edit', 'embed' ),
+                       'readonly'        => true,
+               );
+               $schema['properties']['media_details'] = array(
+                       'description'     => __( 'Details about the resource file, specific to its type.' ),
+                       'type'            => 'object',
+                       'context'         => array( 'view', 'edit', 'embed' ),
+                       'readonly'        => true,
+               );
+               $schema['properties']['post'] = array(
+                       'description'     => __( 'The id for the associated post of the resource.' ),
+                       'type'            => 'integer',
+                       'context'         => array( 'view', 'edit' ),
+               );
+               $schema['properties']['source_url'] = array(
+                       'description'     => __( 'URL to the original resource file.' ),
+                       'type'            => 'string',
+                       'format'          => 'uri',
+                       'context'         => array( 'view', 'edit', 'embed' ),
+                       'readonly'        => true,
+               );
+               return $schema;
+       }
+
+       /**
+        * Handle an upload via raw POST data.
+        *
+        * @param array $data    Supplied file data.
+        * @param array $headers HTTP headers from the request.
+        * @return array|WP_Error Data from {@see wp_handle_sideload()}.
+        */
+       protected function upload_from_data( $data, $headers ) {
+               if ( empty( $data ) ) {
+                       return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
+               }
+
+               if ( empty( $headers['content_type'] ) ) {
+                       return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) );
+               }
+
+               if ( empty( $headers['content_disposition'] ) ) {
+                       return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) );
+               }
+
+               $filename = self::get_filename_from_disposition( $headers['content_disposition'] );
+
+               if ( empty( $filename ) ) {
+                       return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) );
+               }
+
+               if ( ! empty( $headers['content_md5'] ) ) {
+                       $content_md5 = array_shift( $headers['content_md5'] );
+                       $expected = trim( $content_md5 );
+                       $actual   = md5( $data );
+
+                       if ( $expected !== $actual ) {
+                               return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
+                       }
+               }
+
+               // Get the content-type.
+               $type = array_shift( $headers['content_type'] );
+
+               /** Include admin functions to get access to wp_tempnam() and wp_handle_sideload() */
+               require_once ABSPATH . 'wp-admin/includes/admin.php';
+
+               // Save the file.
+               $tmpfname = wp_tempnam( $filename );
+
+               $fp = fopen( $tmpfname, 'w+' );
+
+               if ( ! $fp ) {
+                       return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) );
+               }
+
+               fwrite( $fp, $data );
+               fclose( $fp );
+
+               // Now, sideload it in.
+               $file_data = array(
+                       'error'    => null,
+                       'tmp_name' => $tmpfname,
+                       'name'     => $filename,
+                       'type'     => $type,
+               );
+               $overrides = array(
+                       'test_form' => false,
+               );
+               $sideloaded = wp_handle_sideload( $file_data, $overrides );
+
+               if ( isset( $sideloaded['error'] ) ) {
+                       // @codingStandardsIgnoreStart
+                       @unlink( $tmpfname );
+                       // @codingStandardsIgnoreEnd
+                       return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) );
+               }
+
+               return $sideloaded;
+       }
+
+       /**
+        * Parse filename from a Content-Disposition header value.
+        *
+        * As per RFC6266:
+        *
+        *     content-disposition = "Content-Disposition" ":"
+        *                            disposition-type *( ";" disposition-parm )
+        *
+        *     disposition-type    = "inline" | "attachment" | disp-ext-type
+        *                         ; case-insensitive
+        *     disp-ext-type       = token
+        *
+        *     disposition-parm    = filename-parm | disp-ext-parm
+        *
+        *     filename-parm       = "filename" "=" value
+        *                         | "filename*" "=" ext-value
+        *
+        *     disp-ext-parm       = token "=" value
+        *                         | ext-token "=" ext-value
+        *     ext-token           = <the characters in token, followed by "*">
+        *
+        * @see http://tools.ietf.org/html/rfc2388
+        * @see http://tools.ietf.org/html/rfc6266
+        *
+        * @param string[] $disposition_header List of Content-Disposition header values.
+        * @return string|null Filename if available, or null if not found.
+        */
+       public static function get_filename_from_disposition( $disposition_header ) {
+               // Get the filename.
+               $filename = null;
+
+               foreach ( $disposition_header as $value ) {
+                       $value = trim( $value );
+
+                       if ( strpos( $value, ';' ) === false ) {
+                               continue;
+                       }
+
+                       list( $type, $attr_parts ) = explode( ';', $value, 2 );
+                       $attr_parts = explode( ';', $attr_parts );
+                       $attributes = array();
+                       foreach ( $attr_parts as $part ) {
+                               if ( strpos( $part, '=' ) === false ) {
+                                       continue;
+                               }
+
+                               list( $key, $value ) = explode( '=', $part, 2 );
+                               $attributes[ trim( $key ) ] = trim( $value );
+                       }
+
+                       if ( empty( $attributes['filename'] ) ) {
+                               continue;
+                       }
+
+                       $filename = trim( $attributes['filename'] );
+
+                       // Unquote quoted filename, but after trimming.
+                       if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
+                               $filename = substr( $filename, 1, -1 );
+                       }
+               }
+
+               return $filename;
+       }
+
+       /**
+        * Get the query params for collections of attachments.
+        *
+        * @return array Query parameters for the attachment collection as an array.
+        */
+       public function get_collection_params() {
+               $params = parent::get_collection_params();
+               $params['status']['default'] = 'inherit';
+               $params['status']['enum'] = array( 'inherit', 'private', 'trash' );
+               $media_types = $this->get_media_types();
+               $params['media_type'] = array(
+                       'default'            => null,
+                       'description'        => __( 'Limit result set to attachments of a particular media type.' ),
+                       'type'               => 'string',
+                       'enum'               => array_keys( $media_types ),
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $params['mime_type'] = array(
+                       'default'            => null,
+                       'description'        => __( 'Limit result set to attachments of a particular MIME type.' ),
+                       'type'               => 'string',
+               );
+               return $params;
+       }
+
+       /**
+        * Validate whether the user can query private statuses
+        *
+        * @param  mixed           $value     Status value.
+        * @param  WP_REST_Request $request   Request object.
+        * @param  string          $parameter Additional parameter to pass to validation.
+        * @return WP_Error|boolean Boolean true if the user may query, WP_Error if not.
+        */
+       public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
+               if ( 'inherit' === $value ) {
+                       return true;
+               }
+               return parent::validate_user_can_query_private_statuses( $value, $request, $parameter );
+       }
+
+       /**
+        * Handle an upload via multipart/form-data ($_FILES).
+        *
+        * @param array $files   Data from $_FILES.
+        * @param array $headers HTTP headers from the request.
+        * @return array|WP_Error Data from {@see wp_handle_upload()}.
+        */
+       protected function upload_from_file( $files, $headers ) {
+               if ( empty( $files ) ) {
+                       return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
+               }
+
+               // Verify hash, if given.
+               if ( ! empty( $headers['content_md5'] ) ) {
+                       $content_md5 = array_shift( $headers['content_md5'] );
+                       $expected = trim( $content_md5 );
+                       $actual = md5_file( $files['file']['tmp_name'] );
+                       if ( $expected !== $actual ) {
+                               return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
+                       }
+               }
+
+               // Pass off to WP to handle the actual upload.
+               $overrides = array(
+                       'test_form'   => false,
+               );
+               // Bypasses is_uploaded_file() when running unit tests.
+               if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
+                       $overrides['action'] = 'wp_handle_mock_upload';
+               }
+
+               // Include admin functions to get access to wp_handle_upload().
+               require_once ABSPATH . 'wp-admin/includes/admin.php';
+               $file = wp_handle_upload( $files['file'], $overrides );
+
+               if ( isset( $file['error'] ) ) {
+                       return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
+               }
+
+               return $file;
+       }
+
+       /**
+        * Get the supported media types.
+        *
+        * Media types are considered the MIME type category.
+        *
+        * @return array
+        */
+       protected function get_media_types() {
+               $media_types = array();
+               foreach ( get_allowed_mime_types() as $mime_type ) {
+                       $parts = explode( '/', $mime_type );
+                       if ( ! isset( $media_types[ $parts[0] ] ) ) {
+                               $media_types[ $parts[0] ] = array();
+                       }
+                       $media_types[ $parts[0] ][] = $mime_type;
+               }
+               return $media_types;
+       }
+
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestcommentscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-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-comments-controller.php                            (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1316 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Access comments
+ */
+class WP_REST_Comments_Controller extends WP_REST_Controller {
+
+       /**
+        * Instance of a comment meta fields object.
+        *
+        * @access protected
+        * @var WP_REST_Comment_Meta_Fields
+        */
+       protected $meta;
+
+       public function __construct() {
+               $this->namespace = 'wp/v2';
+               $this->rest_base = 'comments';
+
+               $this->meta = new WP_REST_Comment_Meta_Fields();
+       }
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'   => WP_REST_Server::READABLE,
+                               'callback'  => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'      => $this->get_collection_params(),
+                       ),
+                       array(
+                               'methods'  => WP_REST_Server::CREATABLE,
+                               'callback' => array( $this, 'create_item' ),
+                               'permission_callback' => array( $this, 'create_item_permissions_check' ),
+                               'args'     => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+                       array(
+                               'methods'  => WP_REST_Server::READABLE,
+                               'callback' => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'     => array(
+                                       'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       array(
+                               'methods'  => WP_REST_Server::EDITABLE,
+                               'callback' => array( $this, 'update_item' ),
+                               'permission_callback' => array( $this, 'update_item_permissions_check' ),
+                               'args'     => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+                       ),
+                       array(
+                               'methods'  => WP_REST_Server::DELETABLE,
+                               'callback' => array( $this, 'delete_item' ),
+                               'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+                               'args'     => array(
+                                       'force'    => array(
+                                               'default'     => false,
+                                               'description' => __( 'Whether to bypass trash and force deletion.' ),
+                                       ),
+                               ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Check if a given request has access to read comments
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+
+               if ( ! empty( $request['post'] ) ) {
+                       foreach ( (array) $request['post'] as $post_id ) {
+                               $post = $this->get_post( $post_id );
+                               if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post ) ) {
+                                       return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
+                               } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
+                                       return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot read comments without a post.' ), array( 'status' => rest_authorization_required_code() ) );
+                               }
+                       }
+               }
+
+               if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view comments with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! current_user_can( 'edit_posts' ) ) {
+                       $protected_params = array( 'author', 'author_exclude', 'karma', 'author_email', 'type', 'status' );
+                       $forbidden_params = array();
+                       foreach ( $protected_params as $param ) {
+                               if ( 'status' === $param ) {
+                                       if ( 'approve' !== $request[ $param ] ) {
+                                               $forbidden_params[] = $param;
+                                       }
+                               } elseif ( 'type' === $param ) {
+                                       if ( 'comment' !== $request[ $param ] ) {
+                                               $forbidden_params[] = $param;
+                                       }
+                               } elseif ( ! empty( $request[ $param ] ) ) {
+                                       $forbidden_params[] = $param;
+                               }
+                       }
+                       if ( ! empty( $forbidden_params ) ) {
+                               return new WP_Error( 'rest_forbidden_param', sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ), array( 'status' => rest_authorization_required_code() ) );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Get a list of comments.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_items( $request ) {
+
+               // Retrieve the list of registered collection query parameters.
+               $registered = $this->get_collection_params();
+
+               // This array defines mappings between public API query parameters whose
+               // values are accepted as-passed, and their internal WP_Query parameter
+               // name equivalents (some are the same). Only values which are also
+               // present in $registered will be set.
+               $parameter_mappings = array(
+                       'author'         => 'author__in',
+                       'author_email'   => 'author_email',
+                       'author_exclude' => 'author__not_in',
+                       'exclude'        => 'comment__not_in',
+                       'include'        => 'comment__in',
+                       'karma'          => 'karma',
+                       'offset'         => 'offset',
+                       'order'          => 'order',
+                       'parent'         => 'parent__in',
+                       'parent_exclude' => 'parent__not_in',
+                       'per_page'       => 'number',
+                       'post'           => 'post__in',
+                       'search'         => 'search',
+                       'status'         => 'status',
+                       'type'           => 'type',
+               );
+
+               $prepared_args = array();
+
+               // For each known parameter which is both registered and present in the request,
+               // set the parameter's value on the query $prepared_args.
+               foreach ( $parameter_mappings as $api_param => $wp_param ) {
+                       if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
+                               $prepared_args[ $wp_param ] = $request[ $api_param ];
+                       }
+               }
+
+               // Ensure certain parameter values default to empty strings.
+               foreach ( array( 'author_email', 'karma', 'search' ) as $param ) {
+                       if ( ! isset( $prepared_args[ $param ] ) ) {
+                               $prepared_args[ $param ] = '';
+                       }
+               }
+
+               if ( isset( $registered['orderby'] ) ) {
+                       $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
+               }
+
+               $prepared_args['no_found_rows'] = false;
+
+               $prepared_args['date_query'] = array();
+               // Set before into date query. Date query must be specified as an array of an array.
+               if ( isset( $registered['before'], $request['before'] ) ) {
+                       $prepared_args['date_query'][0]['before'] = $request['before'];
+               }
+
+               // Set after into date query. Date query must be specified as an array of an array.
+               if ( isset( $registered['after'], $request['after'] ) ) {
+                       $prepared_args['date_query'][0]['after'] = $request['after'];
+               }
+
+               if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
+                       $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
+               }
+
+               /**
+                * Filter arguments, before passing to WP_Comment_Query, when querying comments via the REST API.
+                *
+                * @see https://developer.wordpress.org/reference/classes/wp_comment_query/
+                *
+                * @param array           $prepared_args Array of arguments for WP_Comment_Query.
+                * @param WP_REST_Request $request       The current request.
+                */
+               $prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request );
+
+               $query = new WP_Comment_Query;
+               $query_result = $query->query( $prepared_args );
+
+               $comments = array();
+               foreach ( $query_result as $comment ) {
+                       if ( ! $this->check_read_permission( $comment ) ) {
+                               continue;
+                       }
+
+                       $data = $this->prepare_item_for_response( $comment, $request );
+                       $comments[] = $this->prepare_response_for_collection( $data );
+               }
+
+               $total_comments = (int) $query->found_comments;
+               $max_pages = (int) $query->max_num_pages;
+               if ( $total_comments < 1 ) {
+                       // Out-of-bounds, run the query again without LIMIT for total count
+                       unset( $prepared_args['number'], $prepared_args['offset'] );
+                       $query = new WP_Comment_Query;
+                       $prepared_args['count'] = true;
+
+                       $total_comments = $query->query( $prepared_args );
+                       $max_pages = ceil( $total_comments / $request['per_page'] );
+               }
+
+               $response = rest_ensure_response( $comments );
+               $response->header( 'X-WP-Total', $total_comments );
+               $response->header( 'X-WP-TotalPages', $max_pages );
+
+               $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
+               if ( $request['page'] > 1 ) {
+                       $prev_page = $request['page'] - 1;
+                       if ( $prev_page > $max_pages ) {
+                               $prev_page = $max_pages;
+                       }
+                       $prev_link = add_query_arg( 'page', $prev_page, $base );
+                       $response->link_header( 'prev', $prev_link );
+               }
+               if ( $max_pages > $request['page'] ) {
+                       $next_page = $request['page'] + 1;
+                       $next_link = add_query_arg( 'page', $next_page, $base );
+                       $response->link_header( 'next', $next_link );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to read the comment
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+               $id = (int) $request['id'];
+
+               $comment = get_comment( $id );
+
+               if ( ! $comment ) {
+                       return true;
+               }
+
+               if ( ! $this->check_read_permission( $comment ) ) {
+                       return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot read this comment.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               $post = $this->get_post( $comment->comment_post_ID );
+
+               if ( $post && ! $this->check_read_post_permission( $post ) ) {
+                       return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this comment with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Get a comment.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_item( $request ) {
+               $id = (int) $request['id'];
+
+               $comment = get_comment( $id );
+               if ( empty( $comment ) ) {
+                       return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
+               }
+
+               if ( ! empty( $comment->comment_post_ID ) ) {
+                       $post = $this->get_post( $comment->comment_post_ID );
+                       if ( empty( $post ) ) {
+                               return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
+                       }
+               }
+
+               $data = $this->prepare_item_for_response( $comment, $request );
+               $response = rest_ensure_response( $data );
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to create a comment
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function create_item_permissions_check( $request ) {
+
+               if ( ! is_user_logged_in() && get_option( 'comment_registration' ) ) {
+                       return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) );
+               }
+
+               // Limit who can set comment `author`, `karma` or `status` to anything other than the default.
+               if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
+                       return new WP_Error( 'rest_comment_invalid_author', __( 'Comment author invalid.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               if ( isset( $request['karma'] ) && $request['karma'] > 0 && ! current_user_can( 'moderate_comments' ) ) {
+                       return new WP_Error( 'rest_comment_invalid_karma', __( 'Sorry, you cannot set karma for comments.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
+                       return new WP_Error( 'rest_comment_invalid_status', __( 'Sorry, you cannot set status for comments.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( empty( $request['post'] ) && ! current_user_can( 'moderate_comments' ) ) {
+                       return new WP_Error( 'rest_comment_invalid_post_id', __( 'Sorry, you cannot create this comment without a post.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! empty( $request['post'] ) && $post = $this->get_post( (int) $request['post'] ) ) {
+                       if ( 'draft' === $post->post_status ) {
+                               return new WP_Error( 'rest_comment_draft_post', __( 'Sorry, you cannot create a comment on this post.' ), array( 'status' => 403 ) );
+                       }
+
+                       if ( 'trash' === $post->post_status ) {
+                               return new WP_Error( 'rest_comment_trash_post', __( 'Sorry, you cannot create a comment on this post.' ), array( 'status' => 403 ) );
+                       }
+
+                       if ( ! $this->check_read_post_permission( $post ) ) {
+                               return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
+                       }
+
+                       if ( ! comments_open( $post->ID ) ) {
+                               return new WP_Error( 'rest_comment_closed', __( 'Sorry, comments are closed on this post.' ), array( 'status' => 403 ) );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Create a comment.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function create_item( $request ) {
+               if ( ! empty( $request['id'] ) ) {
+                       return new WP_Error( 'rest_comment_exists', __( 'Cannot create existing comment.' ), array( 'status' => 400 ) );
+               }
+
+               $prepared_comment = $this->prepare_item_for_database( $request );
+               if ( is_wp_error( $prepared_comment ) ) {
+                       return $prepared_comment;
+               }
+
+               /**
+                * Do not allow a comment to be created with an empty string for
+                * comment_content.
+                * See `wp_handle_comment_submission()`.
+                */
+               if ( '' === $prepared_comment['comment_content'] ) {
+                       return new WP_Error( 'rest_comment_content_invalid', __( 'Comment content is invalid.' ), array( 'status' => 400 ) );
+               }
+
+               // Setting remaining values before wp_insert_comment so we can
+               // use wp_allow_comment().
+               if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
+                       $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
+               }
+
+               // Set author data if the user's logged in
+               $missing_author = empty( $prepared_comment['user_id'] )
+                       && empty( $prepared_comment['comment_author'] )
+                       && empty( $prepared_comment['comment_author_email'] )
+                       && empty( $prepared_comment['comment_author_url'] );
+
+               if ( is_user_logged_in() && $missing_author ) {
+                       $user = wp_get_current_user();
+                       $prepared_comment['user_id'] = $user->ID;
+                       $prepared_comment['comment_author'] = $user->display_name;
+                       $prepared_comment['comment_author_email'] = $user->user_email;
+                       $prepared_comment['comment_author_url'] = $user->user_url;
+               }
+
+               // Honor the discussion setting that requires a name and email address
+               // of the comment author.
+               if ( get_option( 'require_name_email' ) ) {
+                       if ( ! isset( $prepared_comment['comment_author'] ) && ! isset( $prepared_comment['comment_author_email'] ) ) {
+                               return new WP_Error( 'rest_comment_author_data_required', __( 'Creating a comment requires valid author name and email values.' ), array( 'status' => 400 ) );
+                       }
+                       if ( ! isset( $prepared_comment['comment_author'] ) ) {
+                               return new WP_Error( 'rest_comment_author_required', __( 'Creating a comment requires a valid author name.' ), array( 'status' => 400 ) );
+                       }
+                       if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
+                               return new WP_Error( 'rest_comment_author_email_required', __( 'Creating a comment requires a valid author email.' ), array( 'status' => 400 ) );
+                       }
+               }
+
+               if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
+                       $prepared_comment['comment_author_email'] = '';
+               }
+               if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
+                       $prepared_comment['comment_author_url'] = '';
+               }
+
+               $prepared_comment['comment_agent'] = '';
+               $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true );
+
+               if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
+                       $error_code = $prepared_comment['comment_approved']->get_error_code();
+                       $error_message = $prepared_comment['comment_approved']->get_error_message();
+
+                       if ( 'comment_duplicate' === $error_code ) {
+                               return new WP_Error( $error_code, $error_message, array( 'status' => 409 ) );
+                       }
+
+                       if ( 'comment_flood' === $error_code ) {
+                               return new WP_Error( $error_code, $error_message, array( 'status' => 400 ) );
+                       }
+
+                       return $prepared_comment['comment_approved'];
+               }
+
+               /**
+                * Filter a comment before it is inserted via the REST API.
+                *
+                * Allows modification of the comment right before it is inserted via `wp_insert_comment`.
+                *
+                * @param array           $prepared_comment The prepared comment data for `wp_insert_comment`.
+                * @param WP_REST_Request $request          Request used to insert the comment.
+                */
+               $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
+
+               $comment_id = wp_insert_comment( $prepared_comment );
+               if ( ! $comment_id ) {
+                       return new WP_Error( 'rest_comment_failed_create', __( 'Creating comment failed.' ), array( 'status' => 500 ) );
+               }
+
+               if ( isset( $request['status'] ) ) {
+                       $comment = get_comment( $comment_id );
+                       $this->handle_status_param( $request['status'], $comment );
+               }
+
+               $schema = $this->get_item_schema();
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], $comment_id );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $comment = get_comment( $comment_id );
+               $fields_update = $this->update_additional_fields_for_object( $comment, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
+               $request->set_param( 'context', $context );
+               $response = $this->prepare_item_for_response( $comment, $request );
+               $response = rest_ensure_response( $response );
+               $response->set_status( 201 );
+               $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );
+
+               /**
+                * Fires after a comment is created or updated via the REST API.
+                *
+                * @param array           $comment  Comment as it exists in the database.
+                * @param WP_REST_Request $request  The request sent to the API.
+                * @param boolean         $creating True when creating a comment, false when updating.
+                */
+               do_action( 'rest_insert_comment', $comment, $request, true );
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to update a comment
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function update_item_permissions_check( $request ) {
+
+               $id = (int) $request['id'];
+
+               $comment = get_comment( $id );
+
+               if ( $comment && ! $this->check_edit_permission( $comment ) ) {
+                       return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you can not edit this comment.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Edit a comment
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function update_item( $request ) {
+               $id = (int) $request['id'];
+
+               $comment = get_comment( $id );
+               if ( empty( $comment ) ) {
+                       return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
+               }
+
+               if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) {
+                       return new WP_Error( 'rest_comment_invalid_type', __( 'Sorry, you cannot change the comment type.' ), array( 'status' => 404 ) );
+               }
+
+               $prepared_args = $this->prepare_item_for_database( $request );
+               if ( is_wp_error( $prepared_args ) ) {
+                       return $prepared_args;
+               }
+
+               if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
+                       // Only the comment status is being changed.
+                       $change = $this->handle_status_param( $request['status'], $comment );
+                       if ( ! $change ) {
+                               return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), array( 'status' => 500 ) );
+                       }
+               } else {
+                       if ( is_wp_error( $prepared_args ) ) {
+                               return $prepared_args;
+                       }
+
+                       $prepared_args['comment_ID'] = $id;
+
+                       $updated = wp_update_comment( $prepared_args );
+                       if ( 0 === $updated ) {
+                               return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment failed.' ), array( 'status' => 500 ) );
+                       }
+
+                       if ( isset( $request['status'] ) ) {
+                               $this->handle_status_param( $request['status'], $comment );
+                       }
+               }
+
+               $schema = $this->get_item_schema();
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], $id );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $comment = get_comment( $id );
+               $fields_update = $this->update_additional_fields_for_object( $comment, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $comment, $request );
+
+               /* This action is documented in lib/endpoints/class-wp-rest-comments-controller.php */
+               do_action( 'rest_insert_comment', $comment, $request, false );
+
+               return rest_ensure_response( $response );
+       }
+
+       /**
+        * Check if a given request has access to delete a comment
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function delete_item_permissions_check( $request ) {
+               $id = (int) $request['id'];
+               $comment = get_comment( $id );
+               if ( ! $comment ) {
+                       return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
+               }
+               if ( ! $this->check_edit_permission( $comment ) ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you can not delete this comment.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Delete a comment.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function delete_item( $request ) {
+               $id = (int) $request['id'];
+               $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+               $comment = get_comment( $id );
+               if ( empty( $comment ) ) {
+                       return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
+               }
+
+               /**
+                * Filter whether a comment is trashable.
+                *
+                * Return false to disable trash support for the post.
+                *
+                * @param boolean $supports_trash Whether the post type support trashing.
+                * @param WP_Post $comment        The comment object being considered for trashing support.
+                */
+               $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $comment, $request );
+
+               if ( $force ) {
+                       $result = wp_delete_comment( $comment->comment_ID, true );
+               } else {
+                       // If we don't support trashing for this type, error out
+                       if ( ! $supports_trash ) {
+                               return new WP_Error( 'rest_trash_not_supported', __( 'The comment does not support trashing.' ), array( 'status' => 501 ) );
+                       }
+
+                       if ( 'trash' === $comment->comment_approved ) {
+                               return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.' ), array( 'status' => 410 ) );
+                       }
+
+                       $result = wp_trash_comment( $comment->comment_ID );
+               }
+
+               if ( ! $result ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) );
+               }
+
+               /**
+                * Fires after a comment is deleted via the REST API.
+                *
+                * @param object           $comment  The deleted comment data.
+                * @param WP_REST_Response $response The response returned from the API.
+                * @param WP_REST_Request  $request  The request sent to the API.
+                */
+               do_action( 'rest_delete_comment', $comment, $response, $request );
+
+               return $response;
+       }
+
+       /**
+        * Prepare a single comment output for response.
+        *
+        * @param  object          $comment Comment object.
+        * @param  WP_REST_Request $request Request object.
+        * @return WP_REST_Response $response
+        */
+       public function prepare_item_for_response( $comment, $request ) {
+               $data = array(
+                       'id'                 => (int) $comment->comment_ID,
+                       'post'               => (int) $comment->comment_post_ID,
+                       'parent'             => (int) $comment->comment_parent,
+                       'author'             => (int) $comment->user_id,
+                       'author_name'        => $comment->comment_author,
+                       'author_email'       => $comment->comment_author_email,
+                       'author_url'         => $comment->comment_author_url,
+                       'author_ip'          => $comment->comment_author_IP,
+                       'author_user_agent'  => $comment->comment_agent,
+                       'date'               => mysql_to_rfc3339( $comment->comment_date ),
+                       'date_gmt'           => mysql_to_rfc3339( $comment->comment_date_gmt ),
+                       'content'            => array(
+                               'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment ),
+                               'raw'      => $comment->comment_content,
+                       ),
+                       'karma'              => (int) $comment->comment_karma,
+                       'link'               => get_comment_link( $comment ),
+                       'status'             => $this->prepare_status_response( $comment->comment_approved ),
+                       'type'               => get_comment_type( $comment->comment_ID ),
+               );
+
+               $schema = $this->get_item_schema();
+
+               if ( ! empty( $schema['properties']['author_avatar_urls'] ) ) {
+                       $data['author_avatar_urls'] = rest_get_avatar_urls( $comment->comment_author_email );
+               }
+
+               if ( ! empty( $schema['properties']['meta'] ) ) {
+                       $data['meta'] = $this->meta->get_value( $comment->comment_ID, $request );
+               }
+
+               $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 );
+
+               $response->add_links( $this->prepare_links( $comment ) );
+
+               /**
+                * Filter a comment returned from the API.
+                *
+                * Allows modification of the comment right before it is returned.
+                *
+                * @param WP_REST_Response  $response   The response object.
+                * @param object            $comment    The original comment object.
+                * @param WP_REST_Request   $request    Request used to generate the response.
+                */
+               return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
+       }
+
+       /**
+        * Prepare links for the request.
+        *
+        * @param object $comment Comment object.
+        * @return array Links for the given comment.
+        */
+       protected function prepare_links( $comment ) {
+               $links = array(
+                       'self' => array(
+                               'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ),
+                       ),
+                       'collection' => array(
+                               'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
+                       ),
+               );
+
+               if ( 0 !== (int) $comment->user_id ) {
+                       $links['author'] = array(
+                               'href'       => rest_url( 'wp/v2/users/' . $comment->user_id ),
+                               'embeddable' => true,
+                       );
+               }
+
+               if ( 0 !== (int) $comment->comment_post_ID ) {
+                       $post = $this->get_post( $comment->comment_post_ID );
+                       if ( ! empty( $post->ID ) ) {
+                               $obj = get_post_type_object( $post->post_type );
+                               $base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
+
+                               $links['up'] = array(
+                                       'href'       => rest_url( 'wp/v2/' . $base . '/' . $comment->comment_post_ID ),
+                                       'embeddable' => true,
+                                       'post_type'  => $post->post_type,
+                               );
+                       }
+               }
+
+               if ( 0 !== (int) $comment->comment_parent ) {
+                       $links['in-reply-to'] = array(
+                               'href'       => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ),
+                               'embeddable' => true,
+                       );
+               }
+
+               // Only grab one comment to verify the comment has children.
+               $comment_children = $comment->get_children( array( 'number' => 1, 'count' => true ) );
+               if ( ! empty( $comment_children ) ) {
+                       $args = array( 'parent' => $comment->comment_ID );
+                       $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
+
+                       $links['children'] = array(
+                               'href' => $rest_url,
+                       );
+               }
+
+               return $links;
+       }
+
+       /**
+        * Prepend internal property prefix to query parameters to match our response fields.
+        *
+        * @param  string $query_param
+        * @return string $normalized
+        */
+       protected function normalize_query_param( $query_param ) {
+               $prefix = 'comment_';
+
+               switch ( $query_param ) {
+                       case 'id':
+                               $normalized = $prefix . 'ID';
+                               break;
+                       case 'post':
+                               $normalized = $prefix . 'post_ID';
+                               break;
+                       case 'parent':
+                               $normalized = $prefix . 'parent';
+                               break;
+                       case 'include':
+                               $normalized = 'comment__in';
+                               break;
+                       default:
+                               $normalized = $prefix . $query_param;
+                               break;
+               }
+
+               return $normalized;
+       }
+
+       /**
+        * Check comment_approved to set comment status for single comment output.
+        *
+        * @param  string|int $comment_approved
+        * @return string     $status
+        */
+       protected function prepare_status_response( $comment_approved ) {
+
+               switch ( $comment_approved ) {
+                       case 'hold':
+                       case '0':
+                               $status = 'hold';
+                               break;
+
+                       case 'approve':
+                       case '1':
+                               $status = 'approved';
+                               break;
+
+                       case 'spam':
+                       case 'trash':
+                       default:
+                               $status = $comment_approved;
+                               break;
+               }
+
+               return $status;
+       }
+
+       /**
+        * Prepare a single comment to be inserted into the database.
+        *
+        * @param  WP_REST_Request $request Request object.
+        * @return array|WP_Error  $prepared_comment
+        */
+       protected function prepare_item_for_database( $request ) {
+               $prepared_comment = array();
+
+               /**
+                * Allow the comment_content to be set via the 'content' or
+                * the 'content.raw' properties of the Request object.
+                */
+               if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
+                       $prepared_comment['comment_content'] = wp_filter_kses( $request['content'] );
+               } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
+                       $prepared_comment['comment_content'] = wp_filter_kses( $request['content']['raw'] );
+               }
+
+               if ( isset( $request['post'] ) ) {
+                       $prepared_comment['comment_post_ID'] = (int) $request['post'];
+               }
+
+               if ( isset( $request['parent'] ) ) {
+                       $prepared_comment['comment_parent'] = $request['parent'];
+               }
+
+               if ( isset( $request['author'] ) ) {
+                       $user = new WP_User( $request['author'] );
+                       if ( $user->exists() ) {
+                               $prepared_comment['user_id'] = $user->ID;
+                               $prepared_comment['comment_author'] = $user->display_name;
+                               $prepared_comment['comment_author_email'] = $user->user_email;
+                               $prepared_comment['comment_author_url'] = $user->user_url;
+                       } else {
+                               return new WP_Error( 'rest_comment_author_invalid', __( 'Invalid comment author id.' ), array( 'status' => 400 ) );
+                       }
+               }
+
+               if ( isset( $request['author_name'] ) ) {
+                       $prepared_comment['comment_author'] = $request['author_name'];
+               }
+
+               if ( isset( $request['author_email'] ) ) {
+                       $prepared_comment['comment_author_email'] = $request['author_email'];
+               }
+
+               if ( isset( $request['author_url'] ) ) {
+                       $prepared_comment['comment_author_url'] = $request['author_url'];
+               }
+
+               if ( isset( $request['author_ip'] ) ) {
+                       $prepared_comment['comment_author_IP'] = $request['author_ip'];
+               }
+
+               if ( isset( $request['type'] ) ) {
+                       // Comment type "comment" needs to be created as an empty string.
+                       $prepared_comment['comment_type'] = 'comment' === $request['type'] ? '' : $request['type'];
+               }
+
+               if ( isset( $request['karma'] ) ) {
+                       $prepared_comment['comment_karma'] = $request['karma'] ;
+               }
+
+               if ( ! empty( $request['date'] ) ) {
+                       $date_data = rest_get_date_with_gmt( $request['date'] );
+
+                       if ( ! empty( $date_data ) ) {
+                               list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
+                       }
+               } elseif ( ! empty( $request['date_gmt'] ) ) {
+                       $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
+
+                       if ( ! empty( $date_data ) ) {
+                               list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
+                       }
+               }
+
+               // Require 'comment_content' unless only the 'comment_status' is being
+               // updated.
+               if ( ! empty( $prepared_comment ) && ! isset( $prepared_comment['comment_content'] ) ) {
+                       return new WP_Error( 'rest_comment_content_required', __( 'Missing comment content.' ), array( 'status' => 400 ) );
+               }
+
+               return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
+       }
+
+       /**
+        * Get the Comment's schema, conforming to JSON Schema
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'              => 'http://json-schema.org/draft-04/schema#',
+                       'title'                => 'comment',
+                       'type'                 => 'object',
+                       'properties'           => array(
+                               'id'               => array(
+                                       'description'  => __( 'Unique identifier for the object.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'readonly'     => true,
+                               ),
+                               'author'           => array(
+                                       'description'  => __( 'The id of the user object, if author was a user.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'author_email'     => array(
+                                       'description'  => __( 'Email address for the object author.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'email',
+                                       'context'      => array( 'edit' ),
+                               ),
+                               'author_ip'     => array(
+                                       'description'  => __( 'IP address for the object author.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'ipv4',
+                                       'context'      => array( 'edit' ),
+                                       'arg_options'  => array(
+                                               'default'           => '127.0.0.1',
+                                       ),
+                               ),
+                               'author_name'     => array(
+                                       'description'  => __( 'Display name for the object author.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'arg_options'  => array(
+                                               'sanitize_callback' => 'sanitize_text_field',
+                                       ),
+                               ),
+                               'author_url'       => array(
+                                       'description'  => __( 'URL for the object author.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'uri',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'author_user_agent'     => array(
+                                       'description'  => __( 'User agent for the object author.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'content'          => array(
+                                       'description'     => __( 'The content for the object.' ),
+                                       'type'            => 'object',
+                                       'context'         => array( 'view', 'edit', 'embed' ),
+                                       'properties'      => array(
+                                               'raw'         => array(
+                                                       'description'     => __( 'Content for the object, as it exists in the database.' ),
+                                                       'type'            => 'string',
+                                                       'context'         => array( 'edit' ),
+                                               ),
+                                               'rendered'    => array(
+                                                       'description'     => __( 'HTML content for the object, transformed for display.' ),
+                                                       'type'            => 'string',
+                                                       'context'         => array( 'view', 'edit', 'embed' ),
+                                               ),
+                                       ),
+                               ),
+                               'date'             => array(
+                                       'description'  => __( 'The date the object was published.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'date-time',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'date_gmt'         => array(
+                                       'description'  => __( 'The date the object was published as GMT.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'date-time',
+                                       'context'      => array( 'view', 'edit' ),
+                               ),
+                               'karma'             => array(
+                                       'description'  => __( 'Karma for the object.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'edit' ),
+                               ),
+                               'link'             => array(
+                                       'description'  => __( 'URL to the object.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'uri',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'readonly'     => true,
+                               ),
+                               'parent'           => array(
+                                       'description'  => __( 'The id for the parent of the object.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'arg_options'  => array(
+                                               'default'           => 0,
+                                       ),
+                               ),
+                               'post'             => array(
+                                       'description'  => __( 'The id of the associated post object.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'arg_options'  => array(
+                                               'default'           => 0,
+                                       ),
+                               ),
+                               'status'           => array(
+                                       'description'  => __( 'State of the object.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'arg_options'  => array(
+                                               'sanitize_callback' => 'sanitize_key',
+                                       ),
+                               ),
+                               'type'             => array(
+                                       'description'  => __( 'Type of Comment for the object.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'default'      => 'comment',
+                                       'arg_options'  => array(
+                                               'sanitize_callback' => 'sanitize_key',
+                                       ),
+                               ),
+                       ),
+               );
+
+               if ( get_option( 'show_avatars' ) ) {
+                       $avatar_properties = array();
+
+                       $avatar_sizes = rest_get_avatar_sizes();
+                       foreach ( $avatar_sizes as $size ) {
+                               $avatar_properties[ $size ] = array(
+                                       'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
+                                       'type'        => 'string',
+                                       'format'      => 'uri',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                               );
+                       }
+
+                       $schema['properties']['author_avatar_urls'] = array(
+                               'description'   => __( 'Avatar URLs for the object author.' ),
+                               'type'          => 'object',
+                               'context'       => array( 'view', 'edit', 'embed' ),
+                               'readonly'      => true,
+                               'properties'    => $avatar_properties,
+                       );
+               }
+
+               $schema['properties']['meta'] = $this->meta->get_field_schema();
+
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               $query_params = parent::get_collection_params();
+
+               $query_params['context']['default'] = 'view';
+
+               $query_params['after'] = array(
+                       'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.' ),
+                       'type'              => 'string',
+                       'format'            => 'date-time',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $query_params['author'] = array(
+                       'description'       => __( 'Limit result set to comments assigned to specific user ids. Requires authorization.' ),
+                       'sanitize_callback' => 'wp_parse_id_list',
+                       'type'              => 'array',
+               );
+               $query_params['author_exclude'] = array(
+                       'description'       => __( 'Ensure result set excludes comments assigned to specific user ids. Requires authorization.' ),
+                       'sanitize_callback' => 'wp_parse_id_list',
+                       'type'              => 'array',
+               );
+               $query_params['author_email'] = array(
+                       'default'           => null,
+                       'description'       => __( 'Limit result set to that from a specific author email. Requires authorization.' ),
+                       'format'            => 'email',
+                       'sanitize_callback' => 'sanitize_email',
+                       'type'              => 'string',
+               );
+               $query_params['before'] = array(
+                       'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.' ),
+                       'type'              => 'string',
+                       'format'            => 'date-time',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $query_params['exclude'] = array(
+                       'description'        => __( 'Ensure result set excludes specific ids.' ),
+                       'type'               => 'array',
+                       'default'            => array(),
+                       'sanitize_callback'  => 'wp_parse_id_list',
+               );
+               $query_params['include'] = array(
+                       'description'        => __( 'Limit result set to specific ids.' ),
+                       'type'               => 'array',
+                       'default'            => array(),
+                       'sanitize_callback'  => 'wp_parse_id_list',
+               );
+               $query_params['karma'] = array(
+                       'default'           => null,
+                       'description'       => __( 'Limit result set to that of a particular comment karma. Requires authorization.' ),
+                       'sanitize_callback' => 'absint',
+                       'type'              => 'integer',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $query_params['offset'] = array(
+                       'description'        => __( 'Offset the result set by a specific number of comments.' ),
+                       'type'               => 'integer',
+                       'sanitize_callback'  => 'absint',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $query_params['order']      = array(
+                       'description'           => __( 'Order sort attribute ascending or descending.' ),
+                       'type'                  => 'string',
+                       'sanitize_callback'     => 'sanitize_key',
+                       'validate_callback'     => 'rest_validate_request_arg',
+                       'default'               => 'desc',
+                       'enum'                  => array(
+                               'asc',
+                               'desc',
+                       ),
+               );
+               $query_params['orderby']    = array(
+                       'description'           => __( 'Sort collection by object attribute.' ),
+                       'type'                  => 'string',
+                       'sanitize_callback'     => 'sanitize_key',
+                       'validate_callback'     => 'rest_validate_request_arg',
+                       'default'               => 'date_gmt',
+                       'enum'                  => array(
+                               'date',
+                               'date_gmt',
+                               'id',
+                               'include',
+                               'post',
+                               'parent',
+                               'type',
+                       ),
+               );
+               $query_params['parent'] = array(
+                       'default'           => array(),
+                       'description'       => __( 'Limit result set to resources of specific parent ids.' ),
+                       'sanitize_callback' => 'wp_parse_id_list',
+                       'type'              => 'array',
+               );
+               $query_params['parent_exclude'] = array(
+                       'default'           => array(),
+                       'description'       => __( 'Ensure result set excludes specific parent ids.' ),
+                       'sanitize_callback' => 'wp_parse_id_list',
+                       'type'              => 'array',
+               );
+               $query_params['post']   = array(
+                       'default'           => array(),
+                       'description'       => __( 'Limit result set to resources assigned to specific post ids.' ),
+                       'type'              => 'array',
+                       'sanitize_callback' => 'wp_parse_id_list',
+               );
+               $query_params['status'] = array(
+                       'default'           => 'approve',
+                       'description'       => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ),
+                       'sanitize_callback' => 'sanitize_key',
+                       'type'              => 'string',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $query_params['type'] = array(
+                       'default'           => 'comment',
+                       'description'       => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ),
+                       'sanitize_callback' => 'sanitize_key',
+                       'type'              => 'string',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               return $query_params;
+       }
+
+       /**
+        * Set the comment_status of a given comment object when creating or updating a comment.
+        *
+        * @param string|int $new_status
+        * @param object     $comment
+        * @return boolean   $changed
+        */
+       protected function handle_status_param( $new_status, $comment ) {
+               $old_status = wp_get_comment_status( $comment->comment_ID );
+
+               if ( $new_status === $old_status ) {
+                       return false;
+               }
+
+               switch ( $new_status ) {
+                       case 'approved' :
+                       case 'approve':
+                       case '1':
+                               $changed = wp_set_comment_status( $comment->comment_ID, 'approve' );
+                               break;
+                       case 'hold':
+                       case '0':
+                               $changed = wp_set_comment_status( $comment->comment_ID, 'hold' );
+                               break;
+                       case 'spam' :
+                               $changed = wp_spam_comment( $comment->comment_ID );
+                               break;
+                       case 'unspam' :
+                               $changed = wp_unspam_comment( $comment->comment_ID );
+                               break;
+                       case 'trash' :
+                               $changed = wp_trash_comment( $comment->comment_ID );
+                               break;
+                       case 'untrash' :
+                               $changed = wp_untrash_comment( $comment->comment_ID );
+                               break;
+                       default :
+                               $changed = false;
+                               break;
+               }
+
+               return $changed;
+       }
+
+       /**
+        * Check if we can read a post.
+        *
+        * Correctly handles posts with the inherit status.
+        *
+        * @param  WP_Post $post Post Object.
+        * @return boolean Can we read it?
+        */
+       protected function check_read_post_permission( $post ) {
+               $posts_controller = new WP_REST_Posts_Controller( $post->post_type );
+
+               return $posts_controller->check_read_permission( $post );
+       }
+
+       /**
+        * Check if we can read a comment.
+        *
+        * @param  object  $comment Comment object.
+        * @return boolean Can we read it?
+        */
+       protected function check_read_permission( $comment ) {
+               if ( ! empty( $comment->comment_post_ID ) ) {
+                       $post = get_post( $comment->comment_post_ID );
+                       if ( $post ) {
+                               if ( $this->check_read_post_permission( $post ) && 1 === (int) $comment->comment_approved ) {
+                                       return true;
+                               }
+                       }
+               }
+
+               if ( 0 === get_current_user_id() ) {
+                       return false;
+               }
+
+               if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
+                       return false;
+               }
+
+               if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
+                       return true;
+               }
+
+               return current_user_can( 'edit_comment', $comment->comment_ID );
+       }
+
+       /**
+        * Check if we can edit or delete a comment.
+        *
+        * @param  object  $comment Comment object.
+        * @return boolean Can we edit or delete it?
+        */
+       protected function check_edit_permission( $comment ) {
+               if ( 0 === (int) get_current_user_id() ) {
+                       return false;
+               }
+
+               if ( ! current_user_can( 'moderate_comments' ) ) {
+                       return false;
+               }
+
+               return current_user_can( 'edit_comment', $comment->comment_ID );
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestcontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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                             (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php       2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,533 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+
+abstract class WP_REST_Controller {
+
+       /**
+        * The namespace of this controller's route.
+        *
+        * @var string
+        */
+       protected $namespace;
+
+       /**
+        * The base of this controller's route.
+        *
+        * @var string
+        */
+       protected $rest_base;
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+               _doing_it_wrong( 'WP_REST_Controller::register_routes', __( 'The register_routes() method must be overridden' ), 'WPAPI-2.0' );
+       }
+
+       /**
+        * Check if a given request has access to get items.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Get a collection of items.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_items( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Check if a given request has access to get a specific item.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Get one item from the collection.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_item( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Check if a given request has access to create items.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function create_item_permissions_check( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Create one item from the collection.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function create_item( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Check if a given request has access to update a specific item.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function update_item_permissions_check( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Update one item from the collection.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function update_item( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Check if a given request has access to delete a specific item.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function delete_item_permissions_check( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Delete one item from the collection.
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function delete_item( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Prepare the item for create or update operation.
+        *
+        * @param WP_REST_Request $request Request object.
+        * @return WP_Error|object $prepared_item
+        */
+       protected function prepare_item_for_database( $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Prepare the item for the REST response.
+        *
+        * @param mixed $item WordPress representation of the item.
+        * @param WP_REST_Request $request Request object.
+        * @return WP_Error|WP_REST_Response $response
+        */
+       public function prepare_item_for_response( $item, $request ) {
+               return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
+       }
+
+       /**
+        * Prepare a response for inserting into a collection.
+        *
+        * @param WP_REST_Response $response Response object.
+        * @return array Response data, ready for insertion into collection data.
+        */
+       public function prepare_response_for_collection( $response ) {
+               if ( ! ( $response instanceof WP_REST_Response ) ) {
+                       return $response;
+               }
+
+               $data = (array) $response->get_data();
+               $server = rest_get_server();
+
+               if ( method_exists( $server, 'get_compact_response_links' ) ) {
+                       $links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
+               } else {
+                       $links = call_user_func( array( $server, 'get_response_links' ), $response );
+               }
+
+               if ( ! empty( $links ) ) {
+                       $data['_links'] = $links;
+               }
+
+               return $data;
+       }
+
+       /**
+        * Filter a response based on the context defined in the schema.
+        *
+        * @param array $data
+        * @param string $context
+        * @return array
+        */
+       public function filter_response_by_context( $data, $context ) {
+
+               $schema = $this->get_item_schema();
+               foreach ( $data as $key => $value ) {
+                       if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
+                               continue;
+                       }
+
+                       if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) {
+                               unset( $data[ $key ] );
+                               continue;
+                       }
+
+                       if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
+                               foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
+                                       if ( empty( $details['context'] ) ) {
+                                               continue;
+                                       }
+                                       if ( ! in_array( $context, $details['context'], true ) ) {
+                                               if ( isset( $data[ $key ][ $attribute ] ) ) {
+                                                       unset( $data[ $key ][ $attribute ] );
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Get the item's schema, conforming to JSON Schema.
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               return $this->add_additional_fields_schema( array() );
+       }
+
+       /**
+        * Get the item's schema for display / public consumption purposes.
+        *
+        * @return array
+        */
+       public function get_public_item_schema() {
+
+               $schema = $this->get_item_schema();
+
+               foreach ( $schema['properties'] as &$property ) {
+                       unset( $property['arg_options'] );
+               }
+
+               return $schema;
+       }
+
+       /**
+        * Get the query params for collections.
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               return array(
+                       'context'                => $this->get_context_param(),
+                       'page'                   => array(
+                               'description'        => __( 'Current page of the collection.' ),
+                               'type'               => 'integer',
+                               'default'            => 1,
+                               'sanitize_callback'  => 'absint',
+                               'validate_callback'  => 'rest_validate_request_arg',
+                               'minimum'            => 1,
+                       ),
+                       'per_page'               => array(
+                               'description'        => __( 'Maximum number of items to be returned in result set.' ),
+                               'type'               => 'integer',
+                               'default'            => 10,
+                               'minimum'            => 1,
+                               'maximum'            => 100,
+                               'sanitize_callback'  => 'absint',
+                               'validate_callback'  => 'rest_validate_request_arg',
+                       ),
+                       'search'                 => array(
+                               'description'        => __( 'Limit results to those matching a string.' ),
+                               'type'               => 'string',
+                               'sanitize_callback'  => 'sanitize_text_field',
+                               'validate_callback'  => 'rest_validate_request_arg',
+                       ),
+               );
+       }
+
+       /**
+        * Get the magical context param.
+        *
+        * Ensures consistent description between endpoints, and populates enum from schema.
+        *
+        * @param array     $args
+        * @return array
+        */
+       public function get_context_param( $args = array() ) {
+               $param_details = array(
+                       'description'        => __( 'Scope under which the request is made; determines fields present in response.' ),
+                       'type'               => 'string',
+                       'sanitize_callback'  => 'sanitize_key',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $schema = $this->get_item_schema();
+               if ( empty( $schema['properties'] ) ) {
+                       return array_merge( $param_details, $args );
+               }
+               $contexts = array();
+               foreach ( $schema['properties'] as $attributes ) {
+                       if ( ! empty( $attributes['context'] ) ) {
+                               $contexts = array_merge( $contexts, $attributes['context'] );
+                       }
+               }
+               if ( ! empty( $contexts ) ) {
+                       $param_details['enum'] = array_unique( $contexts );
+                       rsort( $param_details['enum'] );
+               }
+               return array_merge( $param_details, $args );
+       }
+
+       /**
+        * Add the values from additional fields to a data object.
+        *
+        * @param array  $object
+        * @param WP_REST_Request $request
+        * @return array modified object with additional fields.
+        */
+       protected function add_additional_fields_to_object( $object, $request ) {
+
+               $additional_fields = $this->get_additional_fields();
+
+               foreach ( $additional_fields as $field_name => $field_options ) {
+
+                       if ( ! $field_options['get_callback'] ) {
+                               continue;
+                       }
+
+                       $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
+               }
+
+               return $object;
+       }
+
+       /**
+        * Update the values of additional fields added to a data object.
+        *
+        * @param array  $object
+        * @param WP_REST_Request $request
+        * @return bool|WP_Error True on success, WP_Error object if a field cannot be updated.
+        */
+       protected function update_additional_fields_for_object( $object, $request ) {
+               $additional_fields = $this->get_additional_fields();
+
+               foreach ( $additional_fields as $field_name => $field_options ) {
+                       if ( ! $field_options['update_callback'] ) {
+                               continue;
+                       }
+
+                       // Don't run the update callbacks if the data wasn't passed in the request.
+                       if ( ! isset( $request[ $field_name ] ) ) {
+                               continue;
+                       }
+
+                       $result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
+                       if ( is_wp_error( $result ) ) {
+                               return $result;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Add the schema from additional fields to an schema array.
+        *
+        * The type of object is inferred from the passed schema.
+        *
+        * @param array $schema Schema array.
+        * @return array Modified Schema array.
+        */
+       protected function add_additional_fields_schema( $schema ) {
+               if ( empty( $schema['title'] ) ) {
+                       return $schema;
+               }
+
+               /**
+                * Can't use $this->get_object_type otherwise we cause an inf loop.
+                */
+               $object_type = $schema['title'];
+
+               $additional_fields = $this->get_additional_fields( $object_type );
+
+               foreach ( $additional_fields as $field_name => $field_options ) {
+                       if ( ! $field_options['schema'] ) {
+                               continue;
+                       }
+
+                       $schema['properties'][ $field_name ] = $field_options['schema'];
+               }
+
+               return $schema;
+       }
+
+       /**
+        * Get all the registered additional fields for a given object-type.
+        *
+        * @param  string $object_type
+        * @return array
+        */
+       protected function get_additional_fields( $object_type = null ) {
+
+               if ( ! $object_type ) {
+                       $object_type = $this->get_object_type();
+               }
+
+               if ( ! $object_type ) {
+                       return array();
+               }
+
+               global $wp_rest_additional_fields;
+
+               if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
+                       return array();
+               }
+
+               return $wp_rest_additional_fields[ $object_type ];
+       }
+
+       /**
+        * Get the object type this controller is responsible for managing.
+        *
+        * @return string
+        */
+       protected function get_object_type() {
+               $schema = $this->get_item_schema();
+
+               if ( ! $schema || ! isset( $schema['title'] ) ) {
+                       return null;
+               }
+
+               return $schema['title'];
+       }
+
+       /**
+        * Get an array of endpoint arguments from the item schema for the controller.
+        *
+        * @param string $method HTTP method of the request. The arguments
+        *                       for `CREATABLE` requests are checked for required
+        *                       values and may fall-back to a given default, this
+        *                       is not done on `EDITABLE` requests. Default is
+        *                       WP_REST_Server::CREATABLE.
+        * @return array $endpoint_args
+        */
+       public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
+
+               $schema                = $this->get_item_schema();
+               $schema_properties     = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
+               $endpoint_args = array();
+
+               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 ( isset( $params['description'] ) ) {
+                               $endpoint_args[ $field_id ]['description'] = $params['description'];
+                       }
+
+                       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 ( array( 'type', 'format', 'enum' ) 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;
+       }
+
+       /**
+        * Retrieves post data given a post ID or post object.
+        *
+        * This is a subset of the functionality of the `get_post()` function, with
+        * the additional functionality of having `the_post` action done on the
+        * resultant post object. This is done so that plugins may manipulate the
+        * post that is used in the REST API.
+        *
+        * @see get_post()
+        * @global WP_Query $wp_query
+        *
+        * @param int|WP_Post $post Post ID or post object. Defaults to global $post.
+        * @return WP_Post|null A `WP_Post` object when successful.
+        */
+       public function get_post( $post ) {
+               $post_obj = get_post( $post );
+
+               /**
+                * Filter the post.
+                *
+                * Allows plugins to filter the post object as returned by `\WP_REST_Controller::get_post()`.
+                *
+                * @param WP_Post|null $post_obj  The post object as returned by `get_post()`.
+                * @param int|WP_Post  $post      The original value used to obtain the post object.
+                */
+               $post = apply_filters( 'rest_the_post', $post_obj, $post );
+
+               return $post;
+       }
+
+       /**
+        * Sanitize the slug value.
+        *
+        * @internal We can't use {@see sanitize_title} directly, as the second
+        * parameter is the fallback title, which would end up being set to the
+        * request object.
+        * @see https://github.com/WP-API/WP-API/issues/1585
+        *
+        * @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
+        *
+        * @param string $slug Slug value passed in request.
+        * @return string Sanitized value for the slug.
+        */
+       public function sanitize_slug( $slug ) {
+               return sanitize_title( $slug );
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestpoststatusescontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-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-post-statuses-controller.php                               (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,244 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
+
+       public function __construct() {
+               $this->namespace = 'wp/v2';
+               $this->rest_base = 'statuses';
+       }
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'            => $this->get_collection_params(),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<status>[\w-]+)', array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'            => array(
+                                       'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Check whether a given request has permission to read post statuses.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+               if ( 'edit' === $request['context'] ) {
+                       $types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
+                       foreach ( $types as $type ) {
+                               if ( current_user_can( $type->cap->edit_posts ) ) {
+                                       return true;
+                               }
+                       }
+                       return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Get all post statuses, depending on user context
+        *
+        * @param WP_REST_Request $request
+        * @return array|WP_Error
+        */
+       public function get_items( $request ) {
+               $data = array();
+               $statuses = get_post_stati( array( 'internal' => false ), 'object' );
+               $statuses['trash'] = get_post_status_object( 'trash' );
+               foreach ( $statuses as $slug => $obj ) {
+                       $ret = $this->check_read_permission( $obj );
+                       if ( ! $ret ) {
+                               continue;
+                       }
+                       $status = $this->prepare_item_for_response( $obj, $request );
+                       $data[ $obj->name ] = $this->prepare_response_for_collection( $status );
+               }
+               return rest_ensure_response( $data );
+       }
+
+       /**
+        * Check if a given request has access to read a post status.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+               $status = get_post_status_object( $request['status'] );
+               if ( empty( $status ) ) {
+                       return new WP_Error( 'rest_status_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
+               }
+               $check = $this->check_read_permission( $status );
+               if ( ! $check ) {
+                       return new WP_Error( 'rest_cannot_read_status', __( 'Cannot view resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Check whether a given post status should be visible
+        *
+        * @param object $status
+        * @return boolean
+        */
+       protected function check_read_permission( $status ) {
+               if ( true === $status->public ) {
+                       return true;
+               }
+               if ( false === $status->internal || 'trash' === $status->name ) {
+                       $types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
+                       foreach ( $types as $type ) {
+                               if ( current_user_can( $type->cap->edit_posts ) ) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Get a specific post status
+        *
+        * @param WP_REST_Request $request
+        * @return array|WP_Error
+        */
+       public function get_item( $request ) {
+               $obj = get_post_status_object( $request['status'] );
+               if ( empty( $obj ) ) {
+                       return new WP_Error( 'rest_status_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
+               }
+               $data = $this->prepare_item_for_response( $obj, $request );
+               return rest_ensure_response( $data );
+       }
+
+       /**
+        * Prepare a post status object for serialization
+        *
+        * @param stdClass $status Post status data
+        * @param WP_REST_Request $request
+        * @return WP_REST_Response Post status data
+        */
+       public function prepare_item_for_response( $status, $request ) {
+
+               $data = array(
+                       'name'         => $status->label,
+                       'private'      => (bool) $status->private,
+                       'protected'    => (bool) $status->protected,
+                       'public'       => (bool) $status->public,
+                       'queryable'    => (bool) $status->publicly_queryable,
+                       'show_in_list' => (bool) $status->show_in_admin_all_list,
+                       'slug'         => $status->name,
+               );
+
+               $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+               $data = $this->add_additional_fields_to_object( $data, $request );
+               $data = $this->filter_response_by_context( $data, $context );
+
+               $response = rest_ensure_response( $data );
+
+               if ( 'publish' === $status->name ) {
+                       $response->add_link( 'archives', rest_url( 'wp/v2/posts' ) );
+               } else {
+                       $response->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( 'wp/v2/posts' ) ) );
+               }
+
+               /**
+                * Filter a status returned from the API.
+                *
+                * Allows modification of the status data right before it is returned.
+                *
+                * @param WP_REST_Response  $response The response object.
+                * @param object            $status   The original status object.
+                * @param WP_REST_Request   $request  Request used to generate the response.
+                */
+               return apply_filters( 'rest_prepare_status', $response, $status, $request );
+       }
+
+       /**
+        * Get the Post status' schema, conforming to JSON Schema
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'              => 'http://json-schema.org/draft-04/schema#',
+                       'title'                => 'status',
+                       'type'                 => 'object',
+                       'properties'           => array(
+                               'name'             => array(
+                                       'description'  => __( 'The title for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'embed', 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'private'          => array(
+                                       'description'  => __( 'Whether posts with this resource should be private.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'protected'        => array(
+                                       'description'  => __( 'Whether posts with this resource should be protected.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'public'           => array(
+                                       'description'  => __( 'Whether posts of this resource should be shown in the front end of the site.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'queryable'        => array(
+                                       'description'  => __( 'Whether posts with this resource should be publicly-queryable.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'show_in_list'     => array(
+                                       'description'  => __( 'Whether to include posts in the edit listing for their post type.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'slug'             => array(
+                                       'description'  => __( 'An alphanumeric identifier for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'embed', 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                       ),
+               );
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               return array(
+                       'context'        => $this->get_context_param( array( 'default' => 'view' ) ),
+               );
+       }
+
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestposttypescontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-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-post-types-controller.php                          (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,202 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Post_Types_Controller extends WP_REST_Controller {
+
+       public function __construct() {
+               $this->namespace = 'wp/v2';
+               $this->rest_base = 'types';
+       }
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'            => $this->get_collection_params(),
+                       ),
+                       'schema'          => array( $this, 'get_public_item_schema' ),
+               ) );
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<type>[\w-]+)', array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_item' ),
+                               'args'            => array(
+                                       'context'     => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       'schema'          => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Check whether a given request has permission to read types.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+               if ( 'edit' === $request['context'] ) {
+                       foreach ( get_post_types( array(), 'object' ) as $post_type ) {
+                               if ( ! empty( $post_type->show_in_rest ) && current_user_can( $post_type->cap->edit_posts ) ) {
+                                       return true;
+                               }
+                       }
+                       return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Get all public post types
+        *
+        * @param WP_REST_Request $request
+        * @return array|WP_Error
+        */
+       public function get_items( $request ) {
+               $data = array();
+               foreach ( get_post_types( array(), 'object' ) as $obj ) {
+                       if ( empty( $obj->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) ) {
+                               continue;
+                       }
+                       $post_type = $this->prepare_item_for_response( $obj, $request );
+                       $data[ $obj->name ] = $this->prepare_response_for_collection( $post_type );
+               }
+               return rest_ensure_response( $data );
+       }
+
+       /**
+        * Get a specific post type
+        *
+        * @param WP_REST_Request $request
+        * @return array|WP_Error
+        */
+       public function get_item( $request ) {
+               $obj = get_post_type_object( $request['type'] );
+               if ( empty( $obj ) ) {
+                       return new WP_Error( 'rest_type_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
+               }
+               if ( empty( $obj->show_in_rest ) ) {
+                       return new WP_Error( 'rest_cannot_read_type', __( 'Cannot view resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               if ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               $data = $this->prepare_item_for_response( $obj, $request );
+               return rest_ensure_response( $data );
+       }
+
+       /**
+        * Prepare a post type object for serialization
+        *
+        * @param stdClass $post_type Post type data
+        * @param WP_REST_Request $request
+        * @return WP_REST_Response $response
+        */
+       public function prepare_item_for_response( $post_type, $request ) {
+               $data = array(
+                       'capabilities' => $post_type->cap,
+                       'description'  => $post_type->description,
+                       'hierarchical' => $post_type->hierarchical,
+                       'labels'       => $post_type->labels,
+                       'name'         => $post_type->label,
+                       'slug'         => $post_type->name,
+               );
+               $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 );
+
+               $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
+               $response->add_links( array(
+                       'collection'              => array(
+                               'href'                => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
+                       ),
+                       'https://api.w.org/items' => array(
+                               'href'                => rest_url( sprintf( 'wp/v2/%s', $base ) ),
+                       ),
+               ) );
+
+               /**
+                * Filter a post type returned from the API.
+                *
+                * Allows modification of the post type data right before it is returned.
+                *
+                * @param WP_REST_Response  $response   The response object.
+                * @param object            $item       The original post type object.
+                * @param WP_REST_Request   $request    Request used to generate the response.
+                */
+               return apply_filters( 'rest_prepare_post_type', $response, $post_type, $request );
+       }
+
+       /**
+        * Get the Post type's schema, conforming to JSON Schema
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'              => 'http://json-schema.org/draft-04/schema#',
+                       'title'                => 'type',
+                       'type'                 => 'object',
+                       'properties'           => array(
+                               'capabilities'     => array(
+                                       'description'  => __( 'All capabilities used by the resource.' ),
+                                       'type'         => 'array',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'description'      => array(
+                                       'description'  => __( 'A human-readable description of the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'hierarchical'     => array(
+                                       'description'  => __( 'Whether or not the resource should have children.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'labels'           => array(
+                                       'description'  => __( 'Human-readable labels for the resource for various contexts.' ),
+                                       'type'         => 'object',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'name'             => array(
+                                       'description'  => __( 'The title for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'readonly'     => true,
+                               ),
+                               'slug'             => array(
+                                       'description'  => __( 'An alphanumeric identifier for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'readonly'     => true,
+                               ),
+                       ),
+               );
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               return array(
+                       'context'      => $this->get_context_param( array( 'default' => 'view' ) ),
+               );
+       }
+
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestpostscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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                               (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1958 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Posts_Controller extends WP_REST_Controller {
+
+       /**
+        * Post type.
+        *
+        * @access protected
+        * @var string
+        */
+       protected $post_type;
+
+       /**
+        * Instance of a post meta fields object.
+        *
+        * @access protected
+        * @var WP_REST_Post_Meta_Fields
+        */
+       protected $meta;
+
+       /**
+        * Constructor.
+        *
+        * @param string $post_type Post type.
+        */
+       public function __construct( $post_type ) {
+               $this->post_type = $post_type;
+               $this->namespace = 'wp/v2';
+               $obj = get_post_type_object( $post_type );
+               $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
+
+               $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
+       }
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'            => $this->get_collection_params(),
+                       ),
+                       array(
+                               'methods'         => WP_REST_Server::CREATABLE,
+                               'callback'        => array( $this, 'create_item' ),
+                               'permission_callback' => array( $this, 'create_item_permissions_check' ),
+                               'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'            => array(
+                                       'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
+                                       'password' => array(
+                                               'description' => __( 'The password for the post if it is password protected.' ),
+                                       ),
+                               ),
+                       ),
+                       array(
+                               'methods'         => WP_REST_Server::EDITABLE,
+                               'callback'        => array( $this, 'update_item' ),
+                               'permission_callback' => array( $this, 'update_item_permissions_check' ),
+                               'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+                       ),
+                       array(
+                               'methods'  => WP_REST_Server::DELETABLE,
+                               'callback' => array( $this, 'delete_item' ),
+                               'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+                               'args'     => array(
+                                       'force'    => array(
+                                               'default'      => false,
+                                               'description'  => __( 'Whether to bypass trash and force deletion.' ),
+                                       ),
+                               ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Check if a given request has access to read /posts.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+
+               $post_type = get_post_type_object( $this->post_type );
+
+               if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit these posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Get a collection of posts.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_items( $request ) {
+
+               // Make sure a search string is set in case the orderby is set to 'relevance'.
+               if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) && empty( $request['filter']['s'] ) ) {
+                       return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.' ), array( 'status' => 400 ) );
+               }
+
+               // Retrieve the list of registered collection query parameters.
+               $registered = $this->get_collection_params();
+               $args = array();
+
+               // This array defines mappings between public API query parameters whose
+               // values are accepted as-passed, and their internal WP_Query parameter
+               // name equivalents (some are the same). Only values which are also
+               // present in $registered will be set.
+               $parameter_mappings = array(
+                       'author'         => 'author__in',
+                       'author_exclude' => 'author__not_in',
+                       'exclude'        => 'post__not_in',
+                       'include'        => 'post__in',
+                       'menu_order'     => 'menu_order',
+                       'offset'         => 'offset',
+                       'order'          => 'order',
+                       'orderby'        => 'orderby',
+                       'page'           => 'paged',
+                       'parent'         => 'post_parent__in',
+                       'parent_exclude' => 'post_parent__not_in',
+                       'search'         => 's',
+                       'slug'           => 'name',
+                       'status'         => 'post_status',
+               );
+
+               // For each known parameter which is both registered and present in the request,
+               // set the parameter's value on the query $args.
+               foreach ( $parameter_mappings as $api_param => $wp_param ) {
+                       if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
+                               $args[ $wp_param ] = $request[ $api_param ];
+                       }
+               }
+
+               // Check for & assign any parameters which require special handling or setting.
+
+               $args['date_query'] = array();
+               // Set before into date query. Date query must be specified as an array of an array.
+               if ( isset( $registered['before'], $request['before'] ) ) {
+                       $args['date_query'][0]['before'] = $request['before'];
+               }
+
+               // Set after into date query. Date query must be specified as an array of an array.
+               if ( isset( $registered['after'], $request['after'] ) ) {
+                       $args['date_query'][0]['after'] = $request['after'];
+               }
+
+               if ( isset( $registered['filter'] ) && is_array( $request['filter'] ) ) {
+                       $args = array_merge( $args, $request['filter'] );
+                       unset( $args['filter'] );
+               }
+
+               // Ensure our per_page parameter overrides any provided posts_per_page filter.
+               if ( isset( $registered['per_page'] ) ) {
+                       $args['posts_per_page'] = $request['per_page'];
+               }
+
+               if ( isset( $registered['sticky'], $request['sticky'] ) ) {
+                       $sticky_posts = get_option( 'sticky_posts', array() );
+                       if ( $sticky_posts && $request['sticky'] ) {
+                               // As post__in will be used to only get sticky posts,
+                               // we have to support the case where post__in was already
+                               // specified.
+                               $args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
+
+                               // If we intersected, but there are no post ids in common,
+                               // WP_Query won't return "no posts" for `post__in = array()`
+                               // so we have to fake it a bit.
+                               if ( ! $args['post__in'] ) {
+                                       $args['post__in'] = array( -1 );
+                               }
+                       } elseif ( $sticky_posts ) {
+                               // As post___not_in will be used to only get posts that
+                               // are not sticky, we have to support the case where post__not_in
+                               // was already specified.
+                               $args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
+                       }
+               }
+
+               // Force the post_type argument, since it's not a user input variable.
+               $args['post_type'] = $this->post_type;
+
+               /**
+                * Filter the query arguments for a request.
+                *
+                * Enables adding extra arguments or setting defaults for a post
+                * collection request.
+                *
+                * @see https://developer.wordpress.org/reference/classes/wp_query/
+                *
+                * @param array           $args    Key value array of query var to query value.
+                * @param WP_REST_Request $request The request used.
+                */
+               $args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
+               $query_args = $this->prepare_items_query( $args, $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;
+                       $tax_exclude = $base . '_exclude';
+
+                       if ( ! empty( $request[ $base ] ) ) {
+                               $query_args['tax_query'][] = array(
+                                       'taxonomy'         => $taxonomy->name,
+                                       'field'            => 'term_id',
+                                       'terms'            => $request[ $base ],
+                                       'include_children' => false,
+                               );
+                       }
+
+                       if ( ! empty( $request[ $tax_exclude ] ) ) {
+                               $query_args['tax_query'][] = array(
+                                       'taxonomy'         => $taxonomy->name,
+                                       'field'            => 'term_id',
+                                       'terms'            => $request[ $tax_exclude ],
+                                       'include_children' => false,
+                                       'operator'         => 'NOT IN',
+                               );
+                       }
+               }
+
+               $posts_query = new WP_Query();
+               $query_result = $posts_query->query( $query_args );
+
+               // Allow access to all password protected posts if the context is edit.
+               if ( 'edit' === $request['context'] ) {
+                       add_filter( 'post_password_required', '__return_false' );
+               }
+
+               $posts = array();
+               foreach ( $query_result as $post ) {
+                       if ( ! $this->check_read_permission( $post ) ) {
+                               continue;
+                       }
+
+                       $data = $this->prepare_item_for_response( $post, $request );
+                       $posts[] = $this->prepare_response_for_collection( $data );
+               }
+
+               // Reset filter.
+               if ( 'edit' === $request['context'] ) {
+                       remove_filter( 'post_password_required', '__return_false' );
+               }
+
+               $page = (int) $query_args['paged'];
+               $total_posts = $posts_query->found_posts;
+
+               if ( $total_posts < 1 ) {
+                       // Out-of-bounds, run the query again without LIMIT for total count.
+                       unset( $query_args['paged'] );
+                       $count_query = new WP_Query();
+                       $count_query->query( $query_args );
+                       $total_posts = $count_query->found_posts;
+               }
+
+               $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
+
+               $response = rest_ensure_response( $posts );
+               $response->header( 'X-WP-Total', (int) $total_posts );
+               $response->header( 'X-WP-TotalPages', (int) $max_pages );
+
+               $request_params = $request->get_query_params();
+               if ( ! empty( $request_params['filter'] ) ) {
+                       // Normalize the pagination params.
+                       unset( $request_params['filter']['posts_per_page'], $request_params['filter']['paged'] );
+               }
+               $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
+
+               if ( $page > 1 ) {
+                       $prev_page = $page - 1;
+                       if ( $prev_page > $max_pages ) {
+                               $prev_page = $max_pages;
+                       }
+                       $prev_link = add_query_arg( 'page', $prev_page, $base );
+                       $response->link_header( 'prev', $prev_link );
+               }
+               if ( $max_pages > $page ) {
+                       $next_page = $page + 1;
+                       $next_link = add_query_arg( 'page', $next_page, $base );
+                       $response->link_header( 'next', $next_link );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to read a post.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+
+               $post = $this->get_post( (int) $request['id'] );
+
+               if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( $post && ! empty( $request['password'] ) ) {
+                       // Check post password, and return error if invalid.
+                       if ( ! hash_equals( $post->post_password, $request['password'] ) ) {
+                               return new WP_Error( 'rest_post_incorrect_password', __( 'Incorrect post password.' ), array( 'status' => 403 ) );
+                       }
+               }
+
+               // Allow access to all password protected posts if the context is edit.
+               if ( 'edit' === $request['context'] ) {
+                       add_filter( 'post_password_required', '__return_false' );
+               }
+
+               if ( $post ) {
+                       return $this->check_read_permission( $post );
+               }
+
+               return true;
+       }
+
+       /**
+        * Can the user access password-protected content?
+        *
+        * This method determines whether we need to override the regular password
+        * check in core with a filter.
+        *
+        * @param WP_Post         $post    Post to check against.
+        * @param WP_REST_Request $request Request data to check.
+        * @return bool True if the user can access password-protected content, false otherwise.
+        */
+       protected function can_access_password_content( $post, $request ) {
+               if ( empty( $post->post_password ) ) {
+                       // No filter required.
+                       return false;
+               }
+
+               // Edit context always gets access to password-protected posts.
+               if ( 'edit' === $request['context'] ) {
+                       return true;
+               }
+
+               // No password, no auth.
+               if ( empty( $request['password'] ) ) {
+                       return false;
+               }
+
+               // Double-check the request password.
+               return hash_equals( $post->post_password, $request['password'] );
+       }
+
+       /**
+        * Get a single post.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_item( $request ) {
+               $id = (int) $request['id'];
+               $post = $this->get_post( $id );
+
+               if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
+                       return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
+               }
+
+               $data = $this->prepare_item_for_response( $post, $request );
+               $response = rest_ensure_response( $data );
+
+               if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
+                       $response->link_header( 'alternate',  get_permalink( $id ), array( 'type' => 'text/html' ) );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to create a post.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function create_item_permissions_check( $request ) {
+
+               $post_type = get_post_type_object( $this->post_type );
+
+               if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
+                       return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
+                       return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! current_user_can( $post_type->cap->create_posts ) ) {
+                       return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create new posts.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Create a single post.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function create_item( $request ) {
+               if ( ! empty( $request['id'] ) ) {
+                       return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
+               }
+
+               $post = $this->prepare_item_for_database( $request );
+               if ( is_wp_error( $post ) ) {
+                       return $post;
+               }
+
+               $post->post_type = $this->post_type;
+               $post_id = wp_insert_post( $post, true );
+
+               if ( is_wp_error( $post_id ) ) {
+
+                       if ( 'db_insert_error' === $post_id->get_error_code() ) {
+                               $post_id->add_data( array( 'status' => 500 ) );
+                       } else {
+                               $post_id->add_data( array( 'status' => 400 ) );
+                       }
+                       return $post_id;
+               }
+               $post->ID = $post_id;
+
+               $schema = $this->get_item_schema();
+
+               if ( ! empty( $schema['properties']['sticky'] ) ) {
+                       if ( ! empty( $request['sticky'] ) ) {
+                               stick_post( $post_id );
+                       } else {
+                               unstick_post( $post_id );
+                       }
+               }
+
+               if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
+                       $this->handle_featured_media( $request['featured_media'], $post->ID );
+               }
+
+               if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
+                       set_post_format( $post, $request['format'] );
+               }
+
+               if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
+                       $this->handle_template( $request['template'], $post->ID );
+               }
+               $terms_update = $this->handle_terms( $post->ID, $request );
+               if ( is_wp_error( $terms_update ) ) {
+                       return $terms_update;
+               }
+
+               $post = $this->get_post( $post_id );
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $post, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               /**
+                * Fires after a single post is created or updated via the REST API.
+                *
+                * @param object          $post      Inserted Post object (not a WP_Post object).
+                * @param WP_REST_Request $request   Request object.
+                * @param boolean         $creating  True when creating post, false when updating.
+                */
+               do_action( "rest_insert_{$this->post_type}", $post, $request, true );
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $post, $request );
+               $response = rest_ensure_response( $response );
+               $response->set_status( 201 );
+               $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to update a post.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function update_item_permissions_check( $request ) {
+
+               $post = $this->get_post( $request['id'] );
+               $post_type = get_post_type_object( $this->post_type );
+
+               if ( $post && ! $this->check_update_permission( $post ) ) {
+                       return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to update this post.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
+                       return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
+                       return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Update a single post.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function update_item( $request ) {
+               $id = (int) $request['id'];
+               $post = $this->get_post( $id );
+
+               if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
+                       return new WP_Error( 'rest_post_invalid_id', __( 'Post id is invalid.' ), array( 'status' => 404 ) );
+               }
+
+               $post = $this->prepare_item_for_database( $request );
+               if ( is_wp_error( $post ) ) {
+                       return $post;
+               }
+               // convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
+               $post_id = wp_update_post( (array) $post, true );
+               if ( is_wp_error( $post_id ) ) {
+                       if ( 'db_update_error' === $post_id->get_error_code() ) {
+                               $post_id->add_data( array( 'status' => 500 ) );
+                       } else {
+                               $post_id->add_data( array( 'status' => 400 ) );
+                       }
+                       return $post_id;
+               }
+
+               $schema = $this->get_item_schema();
+
+               if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
+                       set_post_format( $post, $request['format'] );
+               }
+
+               if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
+                       $this->handle_featured_media( $request['featured_media'], $post_id );
+               }
+
+               if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
+                       if ( ! empty( $request['sticky'] ) ) {
+                               stick_post( $post_id );
+                       } else {
+                               unstick_post( $post_id );
+                       }
+               }
+
+               if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
+                       $this->handle_template( $request['template'], $post->ID );
+               }
+
+               $terms_update = $this->handle_terms( $post->ID, $request );
+               if ( is_wp_error( $terms_update ) ) {
+                       return $terms_update;
+               }
+
+               $post = $this->get_post( $post_id );
+
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], $post->ID );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $post, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               /* This action is documented in lib/endpoints/class-wp-rest-controller.php */
+               do_action( "rest_insert_{$this->post_type}", $post, $request, false );
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $post, $request );
+               return rest_ensure_response( $response );
+       }
+
+       /**
+        * Check if a given request has access to delete a post.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return bool|WP_Error
+        */
+       public function delete_item_permissions_check( $request ) {
+
+               $post = $this->get_post( $request['id'] );
+
+               if ( $post && ! $this->check_delete_permission( $post ) ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete posts.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Delete a single post.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_REST_Response|WP_Error
+        */
+       public function delete_item( $request ) {
+               $id = (int) $request['id'];
+               $force = (bool) $request['force'];
+
+               $post = $this->get_post( $id );
+
+               if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
+                       return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
+               }
+
+               $supports_trash = ( EMPTY_TRASH_DAYS > 0 );
+               if ( 'attachment' === $post->post_type ) {
+                       $supports_trash = $supports_trash && MEDIA_TRASH;
+               }
+
+               /**
+                * Filter whether a post is trashable.
+                *
+                * Return false to disable trash support for the post.
+                *
+                * @param boolean $supports_trash Whether the post type support trashing.
+                * @param WP_Post $post           The Post object being considered for trashing support.
+                */
+               $supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
+
+               if ( ! $this->check_delete_permission( $post ) ) {
+                       return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $post, $request );
+
+               // If we're forcing, then delete permanently.
+               if ( $force ) {
+                       $result = wp_delete_post( $id, true );
+               } else {
+                       // If we don't support trashing for this type, error out.
+                       if ( ! $supports_trash ) {
+                               return new WP_Error( 'rest_trash_not_supported', __( 'The post does not support trashing.' ), array( 'status' => 501 ) );
+                       }
+
+                       // Otherwise, only trash if we haven't already.
+                       if ( 'trash' === $post->post_status ) {
+                               return new WP_Error( 'rest_already_trashed', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
+                       }
+
+                       // (Note that internally this falls through to `wp_delete_post` if
+                       // the trash is disabled.)
+                       $result = wp_trash_post( $id );
+               }
+
+               if ( ! $result ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
+               }
+
+               /**
+                * Fires after a single post is deleted or trashed via the REST API.
+                *
+                * @param object           $post     The deleted or trashed post.
+                * @param WP_REST_Response $response The response data.
+                * @param WP_REST_Request  $request  The request sent to the API.
+                */
+               do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
+
+               return $response;
+       }
+
+       /**
+        * Determine the allowed query_vars for a get_items() response and
+        * prepare for WP_Query.
+        *
+        * @param array           $prepared_args Prepared WP_Query arguments.
+        * @param WP_REST_Request $request       Full details about the request.
+        * @return array          $query_args
+        */
+       protected function prepare_items_query( $prepared_args = array(), $request = null ) {
+
+               $valid_vars = array_flip( $this->get_allowed_query_vars( $request ) );
+               $query_args = array();
+               foreach ( $valid_vars as $var => $index ) {
+                       if ( isset( $prepared_args[ $var ] ) ) {
+                               /**
+                                * Filter the query_vars used in `get_items` for the constructed query.
+                                *
+                                * The dynamic portion of the hook name, $var, refers to the query_var key.
+                                *
+                                * @param mixed $prepared_args[ $var ] The query_var value.
+                                */
+                               $query_args[ $var ] = apply_filters( "rest_query_var-{$var}", $prepared_args[ $var ] );
+                       }
+               }
+
+               if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
+                       $query_args['ignore_sticky_posts'] = true;
+               }
+
+               if ( 'include' === $query_args['orderby'] ) {
+                       $query_args['orderby'] = 'post__in';
+               }
+
+               return $query_args;
+       }
+
+       /**
+        * Get all the WP Query vars that are allowed for the API request.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return array
+        */
+       protected function get_allowed_query_vars( $request = null ) {
+               global $wp;
+
+               /**
+                * Filter the publicly allowed query vars.
+                *
+                * Allows adjusting of the default query vars that are made public.
+                *
+                * @param array  Array of allowed WP_Query query vars.
+                */
+               $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
+
+               $post_type_obj = get_post_type_object( $this->post_type );
+               if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
+                       /**
+                        * Filter the allowed 'private' query vars for authorized users.
+                        *
+                        * If the user has the `edit_posts` capability, we also allow use of
+                        * private query parameters, which are only undesirable on the
+                        * frontend, but are safe for use in query strings.
+                        *
+                        * To disable anyway, use
+                        * `add_filter( 'rest_private_query_vars', '__return_empty_array' );`
+                        *
+                        * @param array $private_query_vars Array of allowed query vars for authorized users.
+                        * }
+                        */
+                       $private = apply_filters( 'rest_private_query_vars', $wp->private_query_vars );
+                       $valid_vars = array_merge( $valid_vars, $private );
+               }
+               // Define our own in addition to WP's normal vars.
+               $rest_valid = array(
+                       'author__in',
+                       'author__not_in',
+                       'ignore_sticky_posts',
+                       'menu_order',
+                       'offset',
+                       'post__in',
+                       'post__not_in',
+                       'post_parent',
+                       'post_parent__in',
+                       'post_parent__not_in',
+                       'posts_per_page',
+                       'date_query',
+               );
+               $valid_vars = array_merge( $valid_vars, $rest_valid );
+
+               /**
+                * Filter allowed query vars for the REST API.
+                *
+                * This filter allows you to add or remove query vars from the final allowed
+                * list for all requests, including unauthenticated ones. To alter the
+                * vars for editors only, {@see rest_private_query_vars}.
+                *
+                * @param array {
+                *    Array of allowed WP_Query query vars.
+                *
+                *    @param string $allowed_query_var The query var to allow.
+                *    @param WP_REST_Request $request Request object.
+                * }
+                */
+               $valid_vars = apply_filters( 'rest_query_vars', $valid_vars, $request );
+
+               return $valid_vars;
+       }
+
+       /**
+        * Check the post_date_gmt or modified_gmt and prepare any post or
+        * modified date for single post output.
+        *
+        * @param string      $date_gmt GMT publication time.
+        * @param string|null $date     Optional, default is null. Local publication time.
+        * @return string|null ISO8601/RFC3339 formatted datetime.
+        */
+       protected function prepare_date_response( $date_gmt, $date = null ) {
+               // Use the date if passed.
+               if ( isset( $date ) ) {
+                       return mysql_to_rfc3339( $date );
+               }
+
+               // Return null if $date_gmt is empty/zeros.
+               if ( '0000-00-00 00:00:00' === $date_gmt ) {
+                       return null;
+               }
+
+               // Return the formatted datetime.
+               return mysql_to_rfc3339( $date_gmt );
+       }
+
+       /**
+        * Prepare a single post for create or update.
+        *
+        * @param WP_REST_Request $request Request object.
+        * @return WP_Error|stdClass $prepared_post Post object.
+        */
+       protected function prepare_item_for_database( $request ) {
+               $prepared_post = new stdClass;
+
+               // ID.
+               if ( isset( $request['id'] ) ) {
+                       $prepared_post->ID = absint( $request['id'] );
+               }
+
+               $schema = $this->get_item_schema();
+
+               // Post title.
+               if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
+                       if ( is_string( $request['title'] ) ) {
+                               $prepared_post->post_title = wp_filter_post_kses( $request['title'] );
+                       } elseif ( ! empty( $request['title']['raw'] ) ) {
+                               $prepared_post->post_title = wp_filter_post_kses( $request['title']['raw'] );
+                       }
+               }
+
+               // Post content.
+               if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
+                       if ( is_string( $request['content'] ) ) {
+                               $prepared_post->post_content = wp_filter_post_kses( $request['content'] );
+                       } elseif ( isset( $request['content']['raw'] ) ) {
+                               $prepared_post->post_content = wp_filter_post_kses( $request['content']['raw'] );
+                       }
+               }
+
+               // Post excerpt.
+               if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
+                       if ( is_string( $request['excerpt'] ) ) {
+                               $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt'] );
+                       } elseif ( isset( $request['excerpt']['raw'] ) ) {
+                               $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt']['raw'] );
+                       }
+               }
+
+               // Post type.
+               if ( empty( $request['id'] ) ) {
+                       // Creating new post, use default type for the controller.
+                       $prepared_post->post_type = $this->post_type;
+               } else {
+                       // Updating a post, use previous type.
+                       $prepared_post->post_type = get_post_type( $request['id'] );
+               }
+               $post_type = get_post_type_object( $prepared_post->post_type );
+
+               // Post status.
+               if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
+                       $status = $this->handle_status_param( $request['status'], $post_type );
+                       if ( is_wp_error( $status ) ) {
+                               return $status;
+                       }
+
+                       $prepared_post->post_status = $status;
+               }
+
+               // Post date.
+               if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
+                       $date_data = rest_get_date_with_gmt( $request['date'] );
+
+                       if ( ! empty( $date_data ) ) {
+                               list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
+                       }
+               } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
+                       $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
+
+                       if ( ! empty( $date_data ) ) {
+                               list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
+                       }
+               }
+               // Post slug.
+               if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
+                       $prepared_post->post_name = $request['slug'];
+               }
+
+               // Author.
+               if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
+                       $post_author = (int) $request['author'];
+                       if ( get_current_user_id() !== $post_author ) {
+                               $user_obj = get_userdata( $post_author );
+                               if ( ! $user_obj ) {
+                                       return new WP_Error( 'rest_invalid_author', __( 'Invalid author id.' ), array( 'status' => 400 ) );
+                               }
+                       }
+                       $prepared_post->post_author = $post_author;
+               }
+
+               // Post password.
+               if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) && '' !== $request['password'] ) {
+                       $prepared_post->post_password = $request['password'];
+
+                       if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
+                               return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) );
+                       }
+
+                       if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
+                               return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) );
+                       }
+               }
+
+               if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
+                       if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
+                               return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) );
+                       }
+               }
+
+               // Parent.
+               if ( ! empty( $schema['properties']['parent'] ) && ! empty( $request['parent'] ) ) {
+                       $parent = $this->get_post( (int) $request['parent'] );
+                       if ( empty( $parent ) ) {
+                               return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent id.' ), array( 'status' => 400 ) );
+                       }
+
+                       $prepared_post->post_parent = (int) $parent->ID;
+               }
+
+               // Menu order.
+               if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
+                       $prepared_post->menu_order = (int) $request['menu_order'];
+               }
+
+               // Comment status.
+               if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
+                       $prepared_post->comment_status = $request['comment_status'];
+               }
+
+               // Ping status.
+               if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
+                       $prepared_post->ping_status = $request['ping_status'];
+               }
+               /**
+                * Filter a post before it is inserted via the REST API.
+                *
+                * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
+                * prepared for insertion.
+                *
+                * @param stdClass        $prepared_post An object representing a single post prepared
+                *                                       for inserting or updating the database.
+                * @param WP_REST_Request $request       Request object.
+                */
+               return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
+
+       }
+
+       /**
+        * Determine validity and normalize provided status param.
+        *
+        * @param string $post_status Post status.
+        * @param object $post_type   Post type.
+        * @return WP_Error|string $post_status
+        */
+       protected function handle_status_param( $post_status, $post_type ) {
+
+               switch ( $post_status ) {
+                       case 'draft':
+                       case 'pending':
+                               break;
+                       case 'private':
+                               if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
+                                       return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create private posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
+                               }
+                               break;
+                       case 'publish':
+                       case 'future':
+                               if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
+                                       return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) );
+                               }
+                               break;
+                       default:
+                               if ( ! get_post_status_object( $post_status ) ) {
+                                       $post_status = 'draft';
+                               }
+                               break;
+               }
+
+               return $post_status;
+       }
+
+       /**
+        * Determine the featured media based on a request param.
+        *
+        * @param int $featured_media Featured Media ID.
+        * @param int $post_id        Post ID.
+        * @return bool|WP_Error
+        */
+       protected function handle_featured_media( $featured_media, $post_id ) {
+
+               $featured_media = (int) $featured_media;
+               if ( $featured_media ) {
+                       $result = set_post_thumbnail( $post_id, $featured_media );
+                       if ( $result ) {
+                               return true;
+                       } else {
+                               return new WP_Error( 'rest_invalid_featured_media', __( 'Invalid featured media id.' ), array( 'status' => 400 ) );
+                       }
+               } else {
+                       return delete_post_thumbnail( $post_id );
+               }
+
+       }
+
+       /**
+        * Set the template for a page.
+        *
+        * @param string  $template Page template filename.
+        * @param integer $post_id  Post ID.
+        */
+       public function handle_template( $template, $post_id ) {
+               if ( in_array( $template, array_keys( wp_get_theme()->get_page_templates( $this->get_post( $post_id ) ) ), true ) ) {
+                       update_post_meta( $post_id, '_wp_page_template', $template );
+               } else {
+                       update_post_meta( $post_id, '_wp_page_template', '' );
+               }
+       }
+
+       /**
+        * Update the post's terms from a REST request.
+        *
+        * @param  int             $post_id The post ID to update the terms form.
+        * @param  WP_REST_Request $request The request object with post and terms data.
+        * @return null|WP_Error   WP_Error on an error assigning any of the terms.
+        */
+       protected function handle_terms( $post_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 ( ! isset( $request[ $base ] ) ) {
+                               continue;
+                       }
+                       $terms = array_map( 'absint', $request[ $base ] );
+                       $result = wp_set_object_terms( $post_id, $terms, $taxonomy->name );
+                       if ( is_wp_error( $result ) ) {
+                               return $result;
+                       }
+               }
+       }
+
+       /**
+        * Check if a given post type should be viewed or managed.
+        *
+        * @param object|string $post_type Post type name or object.
+        * @return boolean Is post type allowed?
+        */
+       protected function check_is_post_type_allowed( $post_type ) {
+               if ( ! is_object( $post_type ) ) {
+                       $post_type = get_post_type_object( $post_type );
+               }
+
+               if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Check if we can read a post.
+        *
+        * Correctly handles posts with the inherit status.
+        *
+        * @param object $post Post object.
+        * @return boolean Can we read it?
+        */
+       public function check_read_permission( $post ) {
+               $post_type = get_post_type_object( $post->post_type );
+               if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
+                       return false;
+               }
+
+               // Can we read the post?
+               if ( 'publish' === $post->post_status || current_user_can( $post_type->cap->read_post, $post->ID ) ) {
+                       return true;
+               }
+
+               $post_status_obj = get_post_status_object( $post->post_status );
+               if ( $post_status_obj && $post_status_obj->public ) {
+                       return true;
+               }
+
+               // Can we read the parent if we're inheriting?
+               if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
+                       $parent = $this->get_post( $post->post_parent );
+                       return $this->check_read_permission( $parent );
+               }
+
+               // If we don't have a parent, but the status is set to inherit, assume
+               // it's published (as per get_post_status()).
+               if ( 'inherit' === $post->post_status ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Check if we can edit a post.
+        *
+        * @param object $post Post object.
+        * @return boolean Can we edit it?
+        */
+       protected function check_update_permission( $post ) {
+               $post_type = get_post_type_object( $post->post_type );
+
+               if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
+                       return false;
+               }
+
+               return current_user_can( $post_type->cap->edit_post, $post->ID );
+       }
+
+       /**
+        * Check if we can create a post.
+        *
+        * @param object $post Post object.
+        * @return boolean Can we create it?.
+        */
+       protected function check_create_permission( $post ) {
+               $post_type = get_post_type_object( $post->post_type );
+
+               if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
+                       return false;
+               }
+
+               return current_user_can( $post_type->cap->create_posts );
+       }
+
+       /**
+        * Check if we can delete a post.
+        *
+        * @param object $post Post object.
+        * @return boolean Can we delete it?
+        */
+       protected function check_delete_permission( $post ) {
+               $post_type = get_post_type_object( $post->post_type );
+
+               if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
+                       return false;
+               }
+
+               return current_user_can( $post_type->cap->delete_post, $post->ID );
+       }
+
+       /**
+        * Prepare a single post output for response.
+        *
+        * @param WP_Post         $post    Post object.
+        * @param WP_REST_Request $request Request object.
+        * @return WP_REST_Response $data
+        */
+       public function prepare_item_for_response( $post, $request ) {
+               $GLOBALS['post'] = $post;
+               setup_postdata( $post );
+
+               $schema = $this->get_item_schema();
+
+               // Base fields for every post.
+               $data = array();
+
+               if ( ! empty( $schema['properties']['id'] ) ) {
+                       $data['id'] = $post->ID;
+               }
+
+               if ( ! empty( $schema['properties']['date'] ) ) {
+                       $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
+               }
+
+               if ( ! empty( $schema['properties']['date_gmt'] ) ) {
+                       $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
+               }
+
+               if ( ! empty( $schema['properties']['guid'] ) ) {
+                       $data['guid'] = array(
+                               /** This filter is documented in wp-includes/post-template.php */
+                               'rendered' => apply_filters( 'get_the_guid', $post->guid ),
+                               'raw'      => $post->guid,
+                       );
+               }
+
+               if ( ! empty( $schema['properties']['modified'] ) ) {
+                       $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
+               }
+
+               if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
+                       $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
+               }
+
+               if ( ! empty( $schema['properties']['password'] ) ) {
+                       $data['password'] = $post->post_password;
+               }
+
+               if ( ! empty( $schema['properties']['slug'] ) ) {
+                       $data['slug'] = $post->post_name;
+               }
+
+               if ( ! empty( $schema['properties']['status'] ) ) {
+                       $data['status'] = $post->post_status;
+               }
+
+               if ( ! empty( $schema['properties']['type'] ) ) {
+                       $data['type'] = $post->post_type;
+               }
+
+               if ( ! empty( $schema['properties']['link'] ) ) {
+                       $data['link'] = get_permalink( $post->ID );
+               }
+
+               if ( ! empty( $schema['properties']['title'] ) ) {
+                       add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+                       $data['title'] = array(
+                               'raw'      => $post->post_title,
+                               'rendered' => get_the_title( $post->ID ),
+                       );
+                       remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+               }
+
+               $has_password_filter = false;
+               if ( $this->can_access_password_content( $post, $request ) ) {
+                       // Allow access to the post, permissions already checked before.
+                       add_filter( 'post_password_required', '__return_false' );
+                       $has_password_filter = true;
+               }
+
+               if ( ! empty( $schema['properties']['content'] ) ) {
+                       $data['content'] = array(
+                               'raw'       => $post->post_content,
+                               /** This filter is documented in wp-includes/post-template.php */
+                               'rendered'  => post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content ),
+                               'protected' => (bool) $post->post_password,
+                       );
+               }
+
+               if ( ! empty( $schema['properties']['excerpt'] ) ) {
+                       /** This filter is documented in wp-includes/post-template.php */
+                       $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
+                       $data['excerpt'] = array(
+                               'raw'       => $post->post_excerpt,
+                               'rendered'  => post_password_required( $post ) ? '' : $excerpt,
+                               'protected' => (bool) $post->post_password,
+                       );
+               }
+
+               if ( $has_password_filter ) {
+                       // Reset filter.
+                       remove_filter( 'post_password_required', '__return_false' );
+               }
+
+               if ( ! empty( $schema['properties']['author'] ) ) {
+                       $data['author'] = (int) $post->post_author;
+               }
+
+               if ( ! empty( $schema['properties']['featured_media'] ) ) {
+                       $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
+               }
+
+               if ( ! empty( $schema['properties']['parent'] ) ) {
+                       $data['parent'] = (int) $post->post_parent;
+               }
+
+               if ( ! empty( $schema['properties']['menu_order'] ) ) {
+                       $data['menu_order'] = (int) $post->menu_order;
+               }
+
+               if ( ! empty( $schema['properties']['comment_status'] ) ) {
+                       $data['comment_status'] = $post->comment_status;
+               }
+
+               if ( ! empty( $schema['properties']['ping_status'] ) ) {
+                       $data['ping_status'] = $post->ping_status;
+               }
+
+               if ( ! empty( $schema['properties']['sticky'] ) ) {
+                       $data['sticky'] = is_sticky( $post->ID );
+               }
+
+               if ( ! empty( $schema['properties']['template'] ) ) {
+                       if ( $template = get_page_template_slug( $post->ID ) ) {
+                               $data['template'] = $template;
+                       } else {
+                               $data['template'] = '';
+                       }
+               }
+
+               if ( ! empty( $schema['properties']['format'] ) ) {
+                       $data['format'] = get_post_format( $post->ID );
+                       // Fill in blank post format.
+                       if ( empty( $data['format'] ) ) {
+                               $data['format'] = 'standard';
+                       }
+               }
+
+               if ( ! empty( $schema['properties']['meta'] ) ) {
+                       $data['meta'] = $this->meta->get_value( $post->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 ( ! empty( $schema['properties'][ $base ] ) ) {
+                               $terms = get_the_terms( $post, $taxonomy->name );
+                               $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
+                       }
+               }
+
+               $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 );
+
+               $response->add_links( $this->prepare_links( $post ) );
+
+               /**
+                * Filter the post data for a response.
+                *
+                * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
+                * prepared for the response.
+                *
+                * @param WP_REST_Response   $response   The response object.
+                * @param WP_Post            $post       Post object.
+                * @param WP_REST_Request    $request    Request object.
+                */
+               return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
+       }
+
+       /**
+        * Overwrite the default protected title format.
+        *
+        * By default WordPress will show password protected posts with a title of
+        * "Protected: %s", as the REST API communicates the protected status of a post
+        * in a machine readable format, we remove the "Protected: " prefix.
+        *
+        * @return string
+        */
+       public function protected_title_format() {
+               return '%s';
+       }
+
+       /**
+        * Prepare links for the request.
+        *
+        * @param WP_Post $post Post object.
+        * @return array Links for the given post.
+        */
+       protected function prepare_links( $post ) {
+               $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
+
+               // Entity meta.
+               $links = array(
+                       'self' => array(
+                               'href'   => rest_url( trailingslashit( $base ) . $post->ID ),
+                       ),
+                       'collection' => array(
+                               'href'   => rest_url( $base ),
+                       ),
+                       'about'      => array(
+                               'href'   => rest_url( 'wp/v2/types/' . $this->post_type ),
+                       ),
+               );
+
+               if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
+                       && ! empty( $post->post_author ) ) {
+                       $links['author'] = array(
+                               'href'       => rest_url( 'wp/v2/users/' . $post->post_author ),
+                               'embeddable' => true,
+                       );
+               }
+
+               if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
+                       $replies_url = rest_url( 'wp/v2/comments' );
+                       $replies_url = add_query_arg( 'post', $post->ID, $replies_url );
+                       $links['replies'] = array(
+                               'href'         => $replies_url,
+                               'embeddable'   => true,
+                       );
+               }
+
+               if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
+                       $links['version-history'] = array(
+                               'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
+                       );
+               }
+               $post_type_obj = get_post_type_object( $post->post_type );
+               if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
+                       $links['up'] = array(
+                               'href'       => rest_url( trailingslashit( $base ) . (int) $post->post_parent ),
+                               'embeddable' => true,
+                       );
+               }
+
+               // If we have a featured media, add that.
+               if ( $featured_media = get_post_thumbnail_id( $post->ID ) ) {
+                       $image_url = rest_url( 'wp/v2/media/' . $featured_media );
+                       $links['https://api.w.org/featuredmedia'] = array(
+                               'href'       => $image_url,
+                               'embeddable' => true,
+                       );
+               }
+               if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
+                       $attachments_url = rest_url( 'wp/v2/media' );
+                       $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
+                       $links['https://api.w.org/attachment'] = array(
+                               'href'       => $attachments_url,
+                       );
+               }
+
+               $taxonomies = get_object_taxonomies( $post->post_type );
+               if ( ! empty( $taxonomies ) ) {
+                       $links['https://api.w.org/term'] = array();
+
+                       foreach ( $taxonomies as $tax ) {
+                               $taxonomy_obj = get_taxonomy( $tax );
+                               // Skip taxonomies that are not public.
+                               if ( empty( $taxonomy_obj->show_in_rest ) ) {
+                                       continue;
+                               }
+
+                               $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;
+                               $terms_url = add_query_arg(
+                                       'post',
+                                       $post->ID,
+                                       rest_url( 'wp/v2/' . $tax_base )
+                               );
+
+                               $links['https://api.w.org/term'][] = array(
+                                       'href'       => $terms_url,
+                                       'taxonomy'   => $tax,
+                                       'embeddable' => true,
+                               );
+                       }
+               }
+
+               return $links;
+       }
+
+       /**
+        * Get the Post's schema, conforming to JSON Schema.
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+
+               $schema = array(
+                       '$schema'    => 'http://json-schema.org/draft-04/schema#',
+                       'title'      => $this->post_type,
+                       'type'       => 'object',
+                       /*
+                        * Base properties for every Post.
+                        */
+                       'properties' => array(
+                               'date'            => array(
+                                       'description' => __( "The date the object was published, in the site's timezone." ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'date_gmt'        => array(
+                                       'description' => __( 'The date the object was published, as GMT.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit' ),
+                               ),
+                               'guid'            => array(
+                                       'description' => __( 'The globally unique identifier for the object.' ),
+                                       'type'        => 'object',
+                                       'context'     => array( 'view', 'edit' ),
+                                       'readonly'    => true,
+                                       'properties'  => array(
+                                               'raw'      => array(
+                                                       'description' => __( 'GUID for the object, as it exists in the database.' ),
+                                                       'type'        => 'string',
+                                                       'context'     => array( 'edit' ),
+                                                       'readonly'    => true,
+                                               ),
+                                               'rendered' => array(
+                                                       'description' => __( 'GUID for the object, transformed for display.' ),
+                                                       'type'        => 'string',
+                                                       'context'     => array( 'view', 'edit' ),
+                                                       'readonly'    => true,
+                                               ),
+                                       ),
+                               ),
+                               'id'              => array(
+                                       'description' => __( 'Unique identifier for the object.' ),
+                                       'type'        => 'integer',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                                       'readonly'    => true,
+                               ),
+                               'link'            => array(
+                                       'description' => __( 'URL to the object.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'uri',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                                       'readonly'    => true,
+                               ),
+                               'modified'        => array(
+                                       'description' => __( "The date the object was last modified, in the site's timezone." ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                               'modified_gmt'    => array(
+                                       'description' => __( 'The date the object was last modified, as GMT.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                               'slug'            => array(
+                                       'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => array( $this, 'sanitize_slug' ),
+                                       ),
+                               ),
+                               'status'          => array(
+                                       'description' => __( 'A named status for the object.' ),
+                                       'type'        => 'string',
+                                       'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
+                                       'context'     => array( 'edit' ),
+                               ),
+                               'type'            => array(
+                                       'description' => __( 'Type of Post for the object.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                                       'readonly'    => true,
+                               ),
+                       ),
+               );
+
+               $post_type_obj = get_post_type_object( $this->post_type );
+               if ( $post_type_obj->hierarchical ) {
+                       $schema['properties']['parent'] = array(
+                               'description' => __( 'The id for the parent of the object.' ),
+                               'type'        => 'integer',
+                               'context'     => array( 'view', 'edit' ),
+                       );
+               }
+
+               $post_type_attributes = array(
+                       'title',
+                       'editor',
+                       'author',
+                       'excerpt',
+                       'thumbnail',
+                       'comments',
+                       'revisions',
+                       'page-attributes',
+                       'post-formats',
+                       'custom-fields',
+               );
+               $fixed_schemas = array(
+                       'post' => array(
+                               'title',
+                               'editor',
+                               'author',
+                               'excerpt',
+                               'thumbnail',
+                               'comments',
+                               'revisions',
+                               'post-formats',
+                               'custom-fields',
+                       ),
+                       'page' => array(
+                               'title',
+                               'editor',
+                               'author',
+                               'excerpt',
+                               'thumbnail',
+                               'comments',
+                               'revisions',
+                               'page-attributes',
+                               'custom-fields',
+                       ),
+                       'attachment' => array(
+                               'title',
+                               'author',
+                               'comments',
+                               'revisions',
+                               'custom-fields',
+                       ),
+               );
+               foreach ( $post_type_attributes as $attribute ) {
+                       if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
+                               continue;
+                       } elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
+                               continue;
+                       }
+
+                       switch ( $attribute ) {
+
+                               case 'title':
+                                       $schema['properties']['title'] = array(
+                                               'description' => __( 'The title for the object.' ),
+                                               'type'        => '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,
+                                                       ),
+                                               ),
+                                       );
+                                       break;
+
+                               case 'editor':
+                                       $schema['properties']['content'] = array(
+                                               'description' => __( 'The content for the object.' ),
+                                               'type'        => 'object',
+                                               'context'     => array( 'view', 'edit' ),
+                                               'properties'  => array(
+                                                       'raw' => array(
+                                                               'description' => __( 'Content for the object, as it exists in the database.' ),
+                                                               'type'        => 'string',
+                                                               'context'     => array( 'edit' ),
+                                                       ),
+                                                       'rendered' => array(
+                                                               'description' => __( 'HTML content for the object, transformed for display.' ),
+                                                               'type'        => 'string',
+                                                               'context'     => array( 'view', 'edit' ),
+                                                               'readonly'    => true,
+                                                       ),
+                                                       'protected'       => array(
+                                                               'description' => __( 'Whether the content is protected with a password.' ),
+                                                               'type'        => 'boolean',
+                                                               'context'     => array( 'view', 'edit', 'embed' ),
+                                                               'readonly'    => true,
+                                                       ),
+                                               ),
+                                       );
+                                       break;
+
+                               case 'author':
+                                       $schema['properties']['author'] = array(
+                                               'description' => __( 'The id for the author of the object.' ),
+                                               'type'        => 'integer',
+                                               'context'     => array( 'view', 'edit', 'embed' ),
+                                       );
+                                       break;
+
+                               case 'excerpt':
+                                       $schema['properties']['excerpt'] = array(
+                                               'description' => __( 'The excerpt for the object.' ),
+                                               'type'        => 'object',
+                                               'context'     => array( 'view', 'edit', 'embed' ),
+                                               'properties'  => array(
+                                                       'raw' => array(
+                                                               'description' => __( 'Excerpt for the object, as it exists in the database.' ),
+                                                               'type'        => 'string',
+                                                               'context'     => array( 'edit' ),
+                                                       ),
+                                                       'rendered' => array(
+                                                               'description' => __( 'HTML excerpt for the object, transformed for display.' ),
+                                                               'type'        => 'string',
+                                                               'context'     => array( 'view', 'edit', 'embed' ),
+                                                               'readonly'    => true,
+                                                       ),
+                                                       'protected'       => array(
+                                                               'description' => __( 'Whether the excerpt is protected with a password.' ),
+                                                               'type'        => 'boolean',
+                                                               'context'     => array( 'view', 'edit', 'embed' ),
+                                                               'readonly'    => true,
+                                                       ),
+                                               ),
+                                       );
+                                       break;
+
+                               case 'thumbnail':
+                                       $schema['properties']['featured_media'] = array(
+                                               'description' => __( 'The id of the featured media for the object.' ),
+                                               'type'        => 'integer',
+                                               'context'     => array( 'view', 'edit' ),
+                                       );
+                                       break;
+
+                               case 'comments':
+                                       $schema['properties']['comment_status'] = array(
+                                               'description' => __( 'Whether or not comments are open on the object.' ),
+                                               'type'        => 'string',
+                                               'enum'        => array( 'open', 'closed' ),
+                                               'context'     => array( 'view', 'edit' ),
+                                       );
+                                       $schema['properties']['ping_status'] = array(
+                                               'description' => __( 'Whether or not the object can be pinged.' ),
+                                               'type'        => 'string',
+                                               'enum'        => array( 'open', 'closed' ),
+                                               'context'     => array( 'view', 'edit' ),
+                                       );
+                                       break;
+
+                               case 'page-attributes':
+                                       $schema['properties']['menu_order'] = array(
+                                               'description' => __( 'The order of the object in relation to other object of its type.' ),
+                                               'type'        => 'integer',
+                                               'context'     => array( 'view', 'edit' ),
+                                       );
+                                       break;
+
+                               case 'post-formats':
+                                       $schema['properties']['format'] = array(
+                                               'description' => __( 'The format for the object.' ),
+                                               'type'        => 'string',
+                                               'enum'        => array_values( get_post_format_slugs() ),
+                                               'context'     => array( 'view', 'edit' ),
+                                       );
+                                       break;
+
+                               case 'custom-fields':
+                                       $schema['properties']['meta'] = $this->meta->get_field_schema();
+                                       break;
+
+                       }
+               }
+
+               if ( 'post' === $this->post_type ) {
+                       $schema['properties']['sticky'] = array(
+                               'description' => __( 'Whether or not the object should be treated as sticky.' ),
+                               'type'        => 'boolean',
+                               'context'     => array( 'view', 'edit' ),
+                       );
+
+                       $schema['properties']['password'] = array(
+                               'description' => __( 'A password to protect access to the content and excerpt.' ),
+                               'type'        => 'string',
+                               'context'     => array( 'edit' ),
+                       );
+               }
+
+               if ( 'page' === $this->post_type ) {
+                       $schema['properties']['template'] = array(
+                               'description' => __( 'The theme file to use to display the object.' ),
+                               'type'        => 'string',
+                               'enum'        => array_keys( wp_get_theme()->get_page_templates() ),
+                               'context'     => array( 'view', 'edit' ),
+                       );
+               }
+
+               $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(
+                               'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
+                               'type'        => 'array',
+                               'context'     => array( 'view', 'edit' ),
+                       );
+                       $schema['properties'][ $base . '_exclude' ] = array(
+                               'description' => sprintf( __( 'The terms in the %s taxonomy that should not be assigned to the object.' ), $taxonomy->name ),
+                               'type'        => 'array',
+                               'context'     => array( 'view', 'edit' ),
+                       );
+               }
+
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections of attachments.
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               $params = parent::get_collection_params();
+
+               $params['context']['default'] = 'view';
+
+               $params['after'] = array(
+                       'description'        => __( 'Limit response to resources published after a given ISO8601 compliant date.' ),
+                       'type'               => 'string',
+                       'format'             => 'date-time',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               if ( post_type_supports( $this->post_type, 'author' ) ) {
+                       $params['author'] = array(
+                               'description'         => __( 'Limit result set to posts assigned to specific authors.' ),
+                               'type'                => 'array',
+                               'default'             => array(),
+                               'sanitize_callback'   => 'wp_parse_id_list',
+                       );
+                       $params['author_exclude'] = array(
+                               'description'         => __( 'Ensure result set excludes posts assigned to specific authors.' ),
+                               'type'                => 'array',
+                               'default'             => array(),
+                               'sanitize_callback'   => 'wp_parse_id_list',
+                       );
+               }
+               $params['before'] = array(
+                       'description'        => __( 'Limit response to resources published before a given ISO8601 compliant date.' ),
+                       'type'               => 'string',
+                       'format'             => 'date-time',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $params['exclude'] = array(
+                       'description'        => __( 'Ensure result set excludes specific ids.' ),
+                       'type'               => 'array',
+                       'default'            => array(),
+                       'sanitize_callback'  => 'wp_parse_id_list',
+               );
+               $params['include'] = array(
+                       'description'        => __( 'Limit result set to specific ids.' ),
+                       'type'               => 'array',
+                       'default'            => array(),
+                       'sanitize_callback'  => 'wp_parse_id_list',
+               );
+               if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
+                       $params['menu_order'] = array(
+                               'description'        => __( 'Limit result set to resources with a specific menu_order value.' ),
+                               'type'               => 'integer',
+                               'sanitize_callback'  => 'absint',
+                               'validate_callback'  => 'rest_validate_request_arg',
+                       );
+               }
+               $params['offset'] = array(
+                       'description'        => __( 'Offset the result set by a specific number of items.' ),
+                       'type'               => 'integer',
+                       'sanitize_callback'  => 'absint',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $params['order'] = array(
+                       'description'        => __( 'Order sort attribute ascending or descending.' ),
+                       'type'               => 'string',
+                       'default'            => 'desc',
+                       'enum'               => array( 'asc', 'desc' ),
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $params['orderby'] = array(
+                       'description'        => __( 'Sort collection by object attribute.' ),
+                       'type'               => 'string',
+                       'default'            => 'date',
+                       'enum'               => array(
+                               'date',
+                               'relevance',
+                               'id',
+                               'include',
+                               'title',
+                               'slug',
+                       ),
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
+                       $params['orderby']['enum'][] = 'menu_order';
+               }
+
+               $post_type_obj = get_post_type_object( $this->post_type );
+               if ( $post_type_obj->hierarchical || 'attachment' === $this->post_type ) {
+                       $params['parent'] = array(
+                               'description'       => __( 'Limit result set to those of particular parent ids.' ),
+                               'type'              => 'array',
+                               'sanitize_callback' => 'wp_parse_id_list',
+                               'default'           => array(),
+                       );
+                       $params['parent_exclude'] = array(
+                               'description'       => __( 'Limit result set to all items except those of a particular parent id.' ),
+                               'type'              => 'array',
+                               'sanitize_callback' => 'wp_parse_id_list',
+                               'default'           => array(),
+                       );
+               }
+
+               $params['slug'] = array(
+                       'description'       => __( 'Limit result set to posts with a specific slug.' ),
+                       'type'              => 'string',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $params['status'] = array(
+                       'default'           => 'publish',
+                       'description'       => __( 'Limit result set to posts assigned a specific status; can be comma-delimited list of status types.' ),
+                       'enum'              => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
+                       'sanitize_callback' => 'sanitize_key',
+                       'type'              => 'string',
+                       'validate_callback' => array( $this, 'validate_user_can_query_private_statuses' ),
+               );
+               $params['filter'] = array(
+                       'description'       => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.' ),
+               );
+
+               $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;
+
+                       $params[ $base ] = array(
+                               'description'       => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
+                               'type'              => 'array',
+                               'sanitize_callback' => 'wp_parse_id_list',
+                               'default'           => array(),
+                       );
+               }
+
+               if ( 'post' === $this->post_type ) {
+                       $params['sticky'] = array(
+                               'description'       => __( 'Limit result set to items that are sticky.' ),
+                               'type'              => 'boolean',
+                               'sanitize_callback' => 'rest_parse_request_arg',
+                       );
+               }
+
+               return $params;
+       }
+
+       /**
+        * Validate whether the user can query private statuses.
+        *
+        * @param  mixed           $value     Post status.
+        * @param  WP_REST_Request $request   Full details about the request.
+        * @param  string          $parameter
+        * @return WP_Error|boolean
+        */
+       public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
+               if ( 'publish' === $value ) {
+                       return true;
+               }
+               $post_type_obj = get_post_type_object( $this->post_type );
+               if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
+                       return true;
+               }
+               return new WP_Error( 'rest_forbidden_status', __( 'Status is forbidden.' ), array( 'status' => rest_authorization_required_code() ) );
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestrevisionscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-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-revisions-controller.php                           (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php     2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,428 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Revisions_Controller extends WP_REST_Controller {
+
+       private $parent_post_type;
+       private $parent_controller;
+       private $parent_base;
+
+       public function __construct( $parent_post_type ) {
+               $this->parent_post_type = $parent_post_type;
+               $this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
+               $this->namespace = 'wp/v2';
+               $this->rest_base = 'revisions';
+               $post_type_object = get_post_type_object( $parent_post_type );
+               $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
+       }
+
+       /**
+        * Register routes for revisions based on post types supporting revisions
+        *
+        * @access public
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base, array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'            => $this->get_collection_params(),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+
+               register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'            => array(
+                                       'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       array(
+                               'methods'         => WP_REST_Server::DELETABLE,
+                               'callback'        => array( $this, 'delete_item' ),
+                               'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ));
+
+       }
+
+       /**
+        * Check if a given request has access to get revisions
+        *
+        * @access public
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+
+               $parent = $this->get_post( $request['parent'] );
+               if ( ! $parent ) {
+                       return true;
+               }
+               $parent_post_type_obj = get_post_type_object( $parent->post_type );
+               if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
+                       return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Get a collection of revisions
+        *
+        * @access public
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_items( $request ) {
+
+               $parent = $this->get_post( $request['parent'] );
+               if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
+                       return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
+               }
+
+               $revisions = wp_get_post_revisions( $request['parent'] );
+
+               $response = array();
+               foreach ( $revisions as $revision ) {
+                       $data = $this->prepare_item_for_response( $revision, $request );
+                       $response[] = $this->prepare_response_for_collection( $data );
+               }
+               return rest_ensure_response( $response );
+       }
+
+       /**
+        * Check if a given request has access to get a specific revision
+        *
+        * @access public
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+               return $this->get_items_permissions_check( $request );
+       }
+
+       /**
+        * Get one revision from the collection
+        *
+        * @access public
+        *
+        * @param WP_REST_Request $request Full data about the request.
+        * @return WP_Error|array
+        */
+       public function get_item( $request ) {
+
+               $parent = $this->get_post( $request['parent'] );
+               if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
+                       return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
+               }
+
+               $revision = $this->get_post( $request['id'] );
+               if ( ! $revision || 'revision' !== $revision->post_type ) {
+                       return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
+               }
+
+               $response = $this->prepare_item_for_response( $revision, $request );
+               return rest_ensure_response( $response );
+       }
+
+       /**
+        * Check if a given request has access to delete a revision
+        *
+        * @access public
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function delete_item_permissions_check( $request ) {
+
+               $response = $this->get_items_permissions_check( $request );
+               if ( ! $response || is_wp_error( $response ) ) {
+                       return $response;
+               }
+
+               $post = $this->get_post( $request['id'] );
+               if ( ! $post ) {
+                       return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
+               }
+               $post_type = get_post_type_object( 'revision' );
+               return current_user_can( $post_type->cap->delete_post, $post->ID );
+       }
+
+       /**
+        * Delete a single revision
+        *
+        * @access public
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function delete_item( $request ) {
+               $result = wp_delete_post( $request['id'], true );
+
+               /**
+                * Fires after a revision is deleted via the REST API.
+                *
+                * @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
+                *                        or false (failure). If the revision was moved to to the trash, $result represents
+                *                        its new state; if it was deleted, $result represents its state before deletion.
+                * @param WP_REST_Request $request The request sent to the API.
+                */
+               do_action( 'rest_delete_revision', $result, $request );
+
+               if ( $result ) {
+                       return true;
+               } else {
+                       return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
+               }
+       }
+
+       /**
+        * Prepare the revision for the REST response
+        *
+        * @access public
+        *
+        * @param WP_Post         $post    Post revision object.
+        * @param WP_REST_Request $request Request object.
+        * @return WP_REST_Response $response
+        */
+       public function prepare_item_for_response( $post, $request ) {
+
+               $schema = $this->get_item_schema();
+
+               $data = array();
+
+               if ( ! empty( $schema['properties']['author'] ) ) {
+                       $data['author'] = $post->post_author;
+               }
+
+               if ( ! empty( $schema['properties']['date'] ) ) {
+                       $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
+               }
+
+               if ( ! empty( $schema['properties']['date_gmt'] ) ) {
+                       $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
+               }
+
+               if ( ! empty( $schema['properties']['id'] ) ) {
+                       $data['id'] = $post->ID;
+               }
+
+               if ( ! empty( $schema['properties']['modified'] ) ) {
+                       $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
+               }
+
+               if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
+                       $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
+               }
+
+               if ( ! empty( $schema['properties']['parent'] ) ) {
+                       $data['parent'] = (int) $post->post_parent;
+               }
+
+               if ( ! empty( $schema['properties']['slug'] ) ) {
+                       $data['slug'] = $post->post_name;
+               }
+
+               if ( ! empty( $schema['properties']['guid'] ) ) {
+                       $data['guid'] = array(
+                               /** This filter is documented in wp-includes/post-template.php */
+                               'rendered' => apply_filters( 'get_the_guid', $post->guid ),
+                               'raw'      => $post->guid,
+                       );
+               }
+
+               if ( ! empty( $schema['properties']['title'] ) ) {
+                       $data['title'] = array(
+                               'raw'      => $post->post_title,
+                               'rendered' => get_the_title( $post->ID ),
+                       );
+               }
+
+               if ( ! empty( $schema['properties']['content'] ) ) {
+
+                       $data['content'] = array(
+                               'raw'      => $post->post_content,
+                               /** This filter is documented in wp-includes/post-template.php */
+                               'rendered' => apply_filters( 'the_content', $post->post_content ),
+                       );
+               }
+
+               if ( ! empty( $schema['properties']['excerpt'] ) ) {
+                       $data['excerpt'] = array(
+                               'raw'      => $post->post_excerpt,
+                               'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
+                       );
+               }
+
+               $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+               $data = $this->add_additional_fields_to_object( $data, $request );
+               $data = $this->filter_response_by_context( $data, $context );
+               $response = rest_ensure_response( $data );
+
+               if ( ! empty( $data['parent'] ) ) {
+                       $response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
+               }
+
+               /**
+                * Filter a revision returned from the API.
+                *
+                * Allows modification of the revision right before it is returned.
+                *
+                * @param WP_REST_Response  $response   The response object.
+                * @param WP_Post           $post       The original revision object.
+                * @param WP_REST_Request   $request    Request used to generate the response.
+                */
+               return apply_filters( 'rest_prepare_revision', $response, $post, $request );
+       }
+
+       /**
+        * Check the post_date_gmt or modified_gmt and prepare any post or
+        * modified date for single post output.
+        *
+        * @access protected
+        *
+        * @param string      $date_gmt GMT publication time.
+        * @param string|null $date     Optional, default is null. Local publication time.
+        * @return string|null ISO8601/RFC3339 formatted datetime.
+        */
+       protected function prepare_date_response( $date_gmt, $date = null ) {
+               if ( '0000-00-00 00:00:00' === $date_gmt ) {
+                       return null;
+               }
+
+               if ( isset( $date ) ) {
+                       return mysql_to_rfc3339( $date );
+               }
+
+               return mysql_to_rfc3339( $date_gmt );
+       }
+
+       /**
+        * Get the revision's schema, conforming to JSON Schema
+        *
+        * @access public
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'    => 'http://json-schema.org/draft-04/schema#',
+                       'title'      => "{$this->parent_post_type}-revision",
+                       'type'       => 'object',
+                       /*
+                        * Base properties for every Revision
+                        */
+                       'properties' => array(
+                               'author'          => array(
+                                       'description' => __( 'The id for the author of the object.' ),
+                                       'type'        => 'integer',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'date'            => array(
+                                       'description' => __( 'The date the object was published.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'date_gmt'        => array(
+                                       'description' => __( 'The date the object was published, as GMT.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit' ),
+                               ),
+                               'guid'            => array(
+                                       'description' => __( 'GUID for the object, as it exists in the database.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'view', 'edit' ),
+                               ),
+                               'id'              => array(
+                                       'description' => __( 'Unique identifier for the object.' ),
+                                       'type'        => 'integer',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                               ),
+                               'modified'        => array(
+                                       'description' => __( 'The date the object was last modified.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit' ),
+                               ),
+                               'modified_gmt'    => array(
+                                       'description' => __( 'The date the object was last modified, as GMT.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'view', 'edit' ),
+                               ),
+                               'parent'          => array(
+                                       'description' => __( 'The id for the parent of the object.' ),
+                                       'type'        => 'integer',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                                       ),
+                               'slug'            => array(
+                                       'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                               ),
+                       ),
+               );
+
+               $parent_schema = $this->parent_controller->get_item_schema();
+
+               if ( ! empty( $parent_schema['properties']['title'] ) ) {
+                       $schema['properties']['title'] = $parent_schema['properties']['title'];
+               }
+               if ( ! empty( $parent_schema['properties']['content'] ) ) {
+                       $schema['properties']['content'] = $parent_schema['properties']['content'];
+               }
+               if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
+                       $schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
+               }
+               if ( ! empty( $parent_schema['properties']['guid'] ) ) {
+                       $schema['properties']['guid'] = $parent_schema['properties']['guid'];
+               }
+
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections
+        *
+        * @access public
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               return array(
+                       'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+               );
+       }
+
+       /**
+        * Check the post excerpt and prepare it for single post output.
+        *
+        * @access protected
+        *
+        * @param string  $excerpt The post excerpt.
+        * @param WP_Post $post    Post revision object.
+        * @return string|null $excerpt
+        */
+       protected function prepare_excerpt_response( $excerpt, $post ) {
+
+               /** This filter is documented in wp-includes/post-template.php */
+               $excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
+
+               if ( empty( $excerpt ) ) {
+                       return '';
+               }
+
+               return $excerpt;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestsettingscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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                            (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,220 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Manage a WordPress site's settings.
+ */
+
+class WP_REST_Settings_Controller extends WP_REST_Controller {
+
+       protected $rest_base = 'settings';
+       protected $namespace = 'wp/v2';
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'             => WP_REST_Server::READABLE,
+                               'callback'            => array( $this, 'get_item' ),
+                               'args'                => array(),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                       ),
+                       array(
+                               'methods'             => WP_REST_Server::EDITABLE,
+                               'callback'            => array( $this, 'update_item' ),
+                               'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Check if a given request has access to read and manage settings.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return boolean
+        */
+       public function get_item_permissions_check( $request ) {
+               return current_user_can( 'manage_options' );
+       }
+
+       /**
+        * Get the settings.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|array
+        */
+       public function get_item( $request ) {
+               $options  = $this->get_registered_options();
+               $response = array();
+
+               foreach ( $options as $name => $args ) {
+                       /**
+                        * Filters the value of a setting recognized by the REST API.
+                        *
+                        * Allow hijacking the setting value and overriding the built-in behavior by returning a
+                        * non-null value.  The returned value will be presented as the setting value instead.
+                        *
+                        * @since 4.7.0
+                        *
+                        * @param mixed  $result  Value to use for the requested setting. Can be a scalar
+                        *                        matching the registered schema for the setting, or null to
+                        *                        follow the default `get_option` behavior.
+                        * @param string $name    Setting name (as shown in REST API responses).
+                        * @param array  $args    Arguments passed to `register_setting()` for this setting.
+                        */
+                       $response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
+
+                       if ( is_null( $response[ $name ] ) ) {
+                               // Default to a null value as "null" in the response means "not set".
+                               $response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
+                       }
+
+                       // Because get_option() is lossy, we have to
+                       // cast values to the type they are registered with.
+                       $response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Prepare a value for output based off a schema array.
+        *
+        * @param  mixed $value
+        * @param  array $schema
+        * @return mixed
+        */
+       protected function prepare_value( $value, $schema ) {
+               switch ( $schema['type'] ) {
+                       case 'string':
+                               return (string) $value;
+                       case 'number':
+                               return (float) $value;
+                       case 'boolean':
+                               return (bool) $value;
+                       default:
+                               return null;
+               }
+       }
+
+       /**
+        * Update settings for the settings object.
+        *
+        * @param  WP_REST_Request $request Full detail about the request.
+        * @return WP_Error|array
+        */
+       public function update_item( $request ) {
+               $options = $this->get_registered_options();
+               $params = $request->get_params();
+
+               foreach ( $options as $name => $args ) {
+                       if ( ! array_key_exists( $name, $params ) ) {
+                               continue;
+                       }
+
+                       /**
+                        * Filters whether to preempt a setting value update.
+                        *
+                        * Allow hijacking the setting update logic and overriding the built-in behavior by
+                        * returning true.
+                        *
+                        * @since 4.7.0
+                        *
+                        * @param boolean $result Whether to override the default behavior for updating the
+                        *                        value of a setting.
+                        * @param string  $name   Setting name (as shown in REST API responses).
+                        * @param mixed   $value  Updated setting value.
+                        * @param array   $args   Arguments passed to `register_setting()` for this setting.
+                        */
+                       $updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
+                       if ( $updated ) {
+                               continue;
+                       }
+
+                       // A null value means reset the option, which is essentially deleting it
+                       // from the database and then relying on the default value.
+                       if ( is_null( $request[ $name ] ) ) {
+                               delete_option( $args['option_name'] );
+                       } else {
+                               update_option( $args['option_name'], $request[ $name ] );
+                       }
+               }
+
+               return $this->get_item( $request );
+       }
+
+       /**
+        * Get all the registered options for the Settings API
+        *
+        * @return array
+        */
+       protected function get_registered_options() {
+               $rest_options = array();
+
+               foreach ( get_registered_settings() as $name => $args ) {
+                       if ( empty( $args['show_in_rest'] ) ) {
+                               continue;
+                       }
+
+                       $rest_args = array();
+                       if ( is_array( $args['show_in_rest'] ) ) {
+                               $rest_args = $args['show_in_rest'];
+                       }
+
+                       $defaults = array(
+                               'name'   => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
+                               'schema' => array(),
+                       );
+                       $rest_args = array_merge( $defaults, $rest_args );
+
+                       $default_schema = array(
+                               'type'        => empty( $args['type'] ) ? null : $args['type'],
+                               'description' => empty( $args['description'] ) ? '' : $args['description'],
+                               'default'     => isset( $args['default'] ) ? $args['default'] : null,
+                       );
+
+                       $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
+                       $rest_args['option_name'] = $name;
+
+                       // Skip over settings that don't have a defined type in the schema.
+                       if ( empty( $rest_args['schema']['type'] ) ) {
+                               continue;
+                       }
+
+                       // Whitelist the supported types for settings, as we don't want invalid types
+                       // to be updated with arbitrary values that we can't do decent sanitizing for.
+                       if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'string', 'boolean' ), true ) ) {
+                               continue;
+                       }
+
+                       $rest_options[ $rest_args['name'] ] = $rest_args;
+               }
+
+               return $rest_options;
+       }
+
+       /**
+        * Get the site setting schema, conforming to JSON Schema.
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $options = $this->get_registered_options();
+
+               $schema = array(
+                       '$schema'    => 'http://json-schema.org/draft-04/schema#',
+                       'title'      => 'settings',
+                       'type'       => 'object',
+                       'properties' => array(),
+               );
+
+               foreach ( $options as $option_name => $option ) {
+                       $schema['properties'][ $option_name ] = $option['schema'];
+               }
+
+               return $this->add_additional_fields_schema( $schema );
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswpresttaxonomiescontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-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-taxonomies-controller.php                          (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,261 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
+
+       public function __construct() {
+               $this->namespace = 'wp/v2';
+               $this->rest_base = 'taxonomies';
+       }
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'            => $this->get_collection_params(),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<taxonomy>[\w-]+)', array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'            => array(
+                                       'context'     => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Check whether a given request has permission to read taxonomies.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+               if ( 'edit' === $request['context'] ) {
+                       if ( ! empty( $request['type'] ) ) {
+                               $taxonomies = get_object_taxonomies( $request['type'], 'objects' );
+                       } else {
+                               $taxonomies = get_taxonomies( '', 'objects' );
+                       }
+                       foreach ( $taxonomies as $taxonomy ) {
+                               if ( ! empty( $taxonomy->show_in_rest ) && current_user_can( $taxonomy->cap->manage_terms ) ) {
+                                       return true;
+                               }
+                       }
+                       return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Get all public taxonomies
+        *
+        * @param WP_REST_Request $request
+        * @return array
+        */
+       public function get_items( $request ) {
+
+               // Retrieve the list of registered collection query parameters.
+               $registered = $this->get_collection_params();
+
+               if ( isset( $registered['type'] ) && ! empty( $request['type'] ) ) {
+                       $taxonomies = get_object_taxonomies( $request['type'], 'objects' );
+               } else {
+                       $taxonomies = get_taxonomies( '', 'objects' );
+               }
+               $data = array();
+               foreach ( $taxonomies as $tax_type => $value ) {
+                       if ( empty( $value->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $value->cap->manage_terms ) ) ) {
+                               continue;
+                       }
+                       $tax = $this->prepare_item_for_response( $value, $request );
+                       $tax = $this->prepare_response_for_collection( $tax );
+                       $data[ $tax_type ] = $tax;
+               }
+
+               if ( empty( $data ) ) {
+                       // Response should still be returned as a JSON object when it is empty.
+                       $data = (object) $data;
+               }
+
+               return rest_ensure_response( $data );
+       }
+
+       /**
+        * Check if a given request has access a taxonomy
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+
+               $tax_obj = get_taxonomy( $request['taxonomy'] );
+
+               if ( $tax_obj ) {
+                       if ( empty( $tax_obj->show_in_rest ) ) {
+                               return false;
+                       }
+                       if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->manage_terms ) ) {
+                               return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this resource.' ), array( 'status' => rest_authorization_required_code() ) );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Get a specific taxonomy
+        *
+        * @param WP_REST_Request $request
+        * @return array|WP_Error
+        */
+       public function get_item( $request ) {
+               $tax_obj = get_taxonomy( $request['taxonomy'] );
+               if ( empty( $tax_obj ) ) {
+                       return new WP_Error( 'rest_taxonomy_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
+               }
+               $data = $this->prepare_item_for_response( $tax_obj, $request );
+               return rest_ensure_response( $data );
+       }
+
+       /**
+        * Prepare a taxonomy object for serialization
+        *
+        * @param stdClass $taxonomy Taxonomy data
+        * @param WP_REST_Request $request
+        * @return WP_REST_Response $response
+        */
+       public function prepare_item_for_response( $taxonomy, $request ) {
+
+               $data = array(
+                       'name'         => $taxonomy->label,
+                       'slug'         => $taxonomy->name,
+                       'capabilities' => $taxonomy->cap,
+                       'description'  => $taxonomy->description,
+                       'labels'       => $taxonomy->labels,
+                       'types'        => $taxonomy->object_type,
+                       'show_cloud'   => $taxonomy->show_tagcloud,
+                       'hierarchical' => $taxonomy->hierarchical,
+               );
+
+               $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 );
+
+               $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
+               $response->add_links( array(
+                       'collection'                => array(
+                               'href'                  => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
+                       ),
+                       'https://api.w.org/items'   => array(
+                               'href'                  => rest_url( sprintf( 'wp/v2/%s', $base ) ),
+                       ),
+               ) );
+
+               /**
+                * Filter a taxonomy returned from the API.
+                *
+                * Allows modification of the taxonomy data right before it is returned.
+                *
+                * @param WP_REST_Response  $response   The response object.
+                * @param object            $item       The original taxonomy object.
+                * @param WP_REST_Request   $request    Request used to generate the response.
+                */
+               return apply_filters( 'rest_prepare_taxonomy', $response, $taxonomy, $request );
+       }
+
+       /**
+        * Get the taxonomy's schema, conforming to JSON Schema
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'              => 'http://json-schema.org/draft-04/schema#',
+                       'title'                => 'taxonomy',
+                       'type'                 => 'object',
+                       'properties'           => array(
+                               'capabilities'     => array(
+                                       'description'  => __( 'All capabilities used by the resource.' ),
+                                       'type'         => 'array',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'description'      => array(
+                                       'description'  => __( 'A human-readable description of the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'hierarchical'     => array(
+                                       'description'  => __( 'Whether or not the resource should have children.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'labels'           => array(
+                                       'description'  => __( 'Human-readable labels for the resource for various contexts.' ),
+                                       'type'         => 'object',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'name'             => array(
+                                       'description'  => __( 'The title for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'readonly'     => true,
+                               ),
+                               'slug'             => array(
+                                       'description'  => __( 'An alphanumeric identifier for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit', 'embed' ),
+                                       'readonly'     => true,
+                               ),
+                               'show_cloud'       => array(
+                                       'description'  => __( 'Whether or not the term cloud should be displayed.' ),
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'types'            => array(
+                                       'description'  => __( 'Types associated with resource.' ),
+                                       'type'         => 'array',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                       ),
+               );
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               $new_params = array();
+               $new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
+               $new_params['type'] = array(
+                       'description'  => __( 'Limit results to resources associated with a specific post type.' ),
+                       'type'         => 'string',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               return $new_params;
+       }
+
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswpresttermscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-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-terms-controller.php                               (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,907 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Access terms associated with a taxonomy.
+ */
+class WP_REST_Terms_Controller extends WP_REST_Controller {
+
+       /**
+        * Taxonomy key.
+        *
+        * @access protected
+        * @var string
+        */
+       protected $taxonomy;
+
+       /**
+        * Instance of a term meta fields object.
+        *
+        * @access protected
+        * @var WP_REST_Term_Meta_Fields
+        */
+       protected $meta;
+
+       /**
+        * Column to have the terms be sorted by.
+        *
+        * @access protected
+        * @var string
+        */
+       protected $sort_column;
+
+       /**
+        * Number of terms that were found.
+        *
+        * @access protected
+        * @var int
+        */
+       protected $total_terms;
+
+       /**
+        * Constructor.
+        *
+        * @param string $taxonomy Taxonomy key.
+        */
+       public function __construct( $taxonomy ) {
+               $this->taxonomy = $taxonomy;
+               $this->namespace = 'wp/v2';
+               $tax_obj = get_taxonomy( $taxonomy );
+               $this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name;
+
+               $this->meta = new WP_REST_Term_Meta_Fields( $taxonomy );
+       }
+
+       /**
+        * Registers the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'             => WP_REST_Server::READABLE,
+                               'callback'            => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'                => $this->get_collection_params(),
+                       ),
+                       array(
+                               'methods'             => WP_REST_Server::CREATABLE,
+                               'callback'            => array( $this, 'create_item' ),
+                               'permission_callback' => array( $this, 'create_item_permissions_check' ),
+                               'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+                       array(
+                               'methods'             => WP_REST_Server::READABLE,
+                               'callback'            => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'                => array(
+                                       'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       array(
+                               'methods'             => WP_REST_Server::EDITABLE,
+                               'callback'            => array( $this, 'update_item' ),
+                               'permission_callback' => array( $this, 'update_item_permissions_check' ),
+                               'args'                 => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+                       ),
+                       array(
+                               'methods'             => WP_REST_Server::DELETABLE,
+                               'callback'            => array( $this, 'delete_item' ),
+                               'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+                               'args'                => array(
+                                       'force' => array(
+                                               'default'     => false,
+                                               'description' => __( 'Required to be true, as resource does not support trashing.' ),
+                                       ),
+                               ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+       }
+
+       /**
+        * Checks if a request has access to read terms in the specified taxonomy.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+               $tax_obj = get_taxonomy( $this->taxonomy );
+               if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
+                       return false;
+               }
+               if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Gets terms associated with a taxonomy.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_REST_Response|WP_Error
+        */
+       public function get_items( $request ) {
+
+               // Retrieve the list of registered collection query parameters.
+               $registered = $this->get_collection_params();
+
+               // This array defines mappings between public API query parameters whose
+               // values are accepted as-passed, and their internal WP_Query parameter
+               // name equivalents (some are the same). Only values which are also
+               // present in $registered will be set.
+               $parameter_mappings = array(
+                       'exclude'    => 'exclude',
+                       'include'    => 'include',
+                       'order'      => 'order',
+                       'orderby'    => 'orderby',
+                       'post'       => 'post',
+                       'hide_empty' => 'hide_empty',
+                       'per_page'   => 'number',
+                       'search'     => 'search',
+                       'slug'       => 'slug',
+               );
+
+               $prepared_args = array();
+
+               // For each known parameter which is both registered and present in the request,
+               // set the parameter's value on the query $prepared_args.
+               foreach ( $parameter_mappings as $api_param => $wp_param ) {
+                       if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
+                               $prepared_args[ $wp_param ] = $request[ $api_param ];
+                       }
+               }
+
+               if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
+                       $prepared_args['offset'] = $request['offset'];
+               } else {
+                       $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
+               }
+
+               $taxonomy_obj = get_taxonomy( $this->taxonomy );
+
+               if ( $taxonomy_obj->hierarchical && isset( $registered['parent'], $request['parent'] ) ) {
+                       if ( 0 === $request['parent'] ) {
+                               // Only query top-level terms.
+                               $prepared_args['parent'] = 0;
+                       } else {
+                               if ( $request['parent'] ) {
+                                       $prepared_args['parent'] = $request['parent'];
+                               }
+                       }
+               }
+
+               /**
+                * Filters the query arguments before passing them to get_terms().
+                *
+                * Enables adding extra arguments or setting defaults for a terms
+                * collection request.
+                *
+                * @see https://developer.wordpress.org/reference/functions/get_terms/
+                *
+                * @param array           $prepared_args Array of arguments to be
+                *                                       passed to get_terms().
+                * @param WP_REST_Request $request       The current request.
+                */
+               $prepared_args = apply_filters( "rest_{$this->taxonomy}_query", $prepared_args, $request );
+
+               if ( ! empty( $prepared_args['post'] )  ) {
+                       $query_result = $this->get_terms_for_post( $prepared_args );
+                       $total_terms = $this->total_terms;
+               } else {
+                       $query_result = get_terms( $this->taxonomy, $prepared_args );
+
+                       $count_args = $prepared_args;
+                       unset( $count_args['number'], $count_args['offset'] );
+                       $total_terms = wp_count_terms( $this->taxonomy, $count_args );
+
+                       // wp_count_terms can return a falsy value when the term has no children
+                       if ( ! $total_terms ) {
+                               $total_terms = 0;
+                       }
+               }
+               $response = array();
+               foreach ( $query_result as $term ) {
+                       $data = $this->prepare_item_for_response( $term, $request );
+                       $response[] = $this->prepare_response_for_collection( $data );
+               }
+
+               $response = rest_ensure_response( $response );
+
+               // Store pagination values for headers.
+               $per_page = (int) $prepared_args['number'];
+               $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
+
+               $response->header( 'X-WP-Total', (int) $total_terms );
+               $max_pages = ceil( $total_terms / $per_page );
+               $response->header( 'X-WP-TotalPages', (int) $max_pages );
+
+               $base = add_query_arg( $request->get_query_params(), rest_url( $this->namespace . '/' . $this->rest_base ) );
+               if ( $page > 1 ) {
+                       $prev_page = $page - 1;
+                       if ( $prev_page > $max_pages ) {
+                               $prev_page = $max_pages;
+                       }
+                       $prev_link = add_query_arg( 'page', $prev_page, $base );
+                       $response->link_header( 'prev', $prev_link );
+               }
+               if ( $max_pages > $page ) {
+                       $next_page = $page + 1;
+                       $next_link = add_query_arg( 'page', $next_page, $base );
+                       $response->link_header( 'next', $next_link );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Gets the terms attached to a post.
+        *
+        * This is an alternative to get_terms() that uses get_the_terms()
+        * instead, which hits the object cache. There are a few things not
+        * supported, notably `include`, `exclude`. In `self::get_items()` these
+        * are instead treated as a full query.
+        *
+        * @param array $prepared_args Arguments for get_terms().
+        * @return array List of term objects. (Total count in `$this->total_terms`)
+        */
+       protected function get_terms_for_post( $prepared_args ) {
+               $query_result = get_the_terms( $prepared_args['post'], $this->taxonomy );
+               if ( empty( $query_result ) ) {
+                       $this->total_terms = 0;
+                       return array();
+               }
+
+               /*
+                * get_items() verifies that we don't have `include` set, and default
+                * ordering is by `name`.
+                */
+               if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) {
+                       switch ( $prepared_args['orderby'] ) {
+                               case 'id':
+                                       $this->sort_column = 'term_id';
+                                       break;
+
+                               case 'slug':
+                               case 'term_group':
+                               case 'description':
+                               case 'count':
+                                       $this->sort_column = $prepared_args['orderby'];
+                                       break;
+                       }
+                       usort( $query_result, array( $this, 'compare_terms' ) );
+               }
+               if ( strtolower( $prepared_args['order'] ) !== 'asc' ) {
+                       $query_result = array_reverse( $query_result );
+               }
+
+               // Pagination.
+               $this->total_terms = count( $query_result );
+               $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] );
+
+               return $query_result;
+       }
+
+       /**
+        * Comparison function for sorting terms by a column.
+        *
+        * Uses `$this->sort_column` to determine field to sort by.
+        *
+        * @access protected
+        *
+        * @param stdClass $left Term object.
+        * @param stdClass $right Term object.
+        * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left.
+        */
+       protected function compare_terms( $left, $right ) {
+               $col = $this->sort_column;
+               $left_val = $left->$col;
+               $right_val = $right->$col;
+
+               if ( is_int( $left_val ) && is_int( $right_val ) ) {
+                       return $left_val - $right_val;
+               }
+
+               return strcmp( $left_val, $right_val );
+       }
+
+       /**
+        * Checks if a request has access to read the specified term.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+               $tax_obj = get_taxonomy( $this->taxonomy );
+               if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
+                       return false;
+               }
+               if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Gets a single term from a taxonomy.
+        *
+        * @param WP_REST_Request $request Full details about the request
+        * @return WP_REST_Request|WP_Error
+        */
+       public function get_item( $request ) {
+
+               $term = get_term( (int) $request['id'], $this->taxonomy );
+               if ( ! $term || $term->taxonomy !== $this->taxonomy ) {
+                       return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
+               }
+               if ( is_wp_error( $term ) ) {
+                       return $term;
+               }
+
+               $response = $this->prepare_item_for_response( $term, $request );
+
+               return rest_ensure_response( $response );
+       }
+
+       /**
+        * Checks if a request has access to create a term.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function create_item_permissions_check( $request ) {
+
+               if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
+                       return false;
+               }
+
+               $taxonomy_obj = get_taxonomy( $this->taxonomy );
+               if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) {
+                       return new WP_Error( 'rest_cannot_create', __( 'Sorry, you cannot create new resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Creates a single term in a taxonomy.
+        *
+        * @param WP_REST_Request $request Full details about the request
+        * @return WP_REST_Request|WP_Error
+        */
+       public function create_item( $request ) {
+               if ( isset( $request['parent'] ) ) {
+                       if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
+                               return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
+                       }
+
+                       $parent = get_term( (int) $request['parent'], $this->taxonomy );
+
+                       if ( ! $parent ) {
+                               return new WP_Error( 'rest_term_invalid', __( "Parent resource doesn't exist." ), array( 'status' => 400 ) );
+                       }
+               }
+
+               $prepared_term = $this->prepare_item_for_database( $request );
+
+               $term = wp_insert_term( $prepared_term->name, $this->taxonomy, $prepared_term );
+               if ( is_wp_error( $term ) ) {
+
+                       /*
+                        * If we're going to inform the client that the term already exists,
+                        * give them the identifier for future use.
+                        */
+                       if ( $term_id = $term->get_error_data( 'term_exists' ) ) {
+                               $existing_term = get_term( $term_id, $this->taxonomy );
+                               $term->add_data( $existing_term->term_id, 'term_exists' );
+                       }
+
+                       return $term;
+               }
+
+               $term = get_term( $term['term_id'], $this->taxonomy );
+
+               /**
+                * Fires after a single term is created or updated via the REST API.
+                *
+                * @param WP_Term         $term     Inserted Term object.
+                * @param WP_REST_Request $request  Request object.
+                * @param boolean         $creating True when creating term, false when updating.
+                */
+               do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
+
+               $schema = $this->get_item_schema();
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $term, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               $request->set_param( 'context', 'view' );
+               $response = $this->prepare_item_for_response( $term, $request );
+               $response = rest_ensure_response( $response );
+               $response->set_status( 201 );
+               $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
+               return $response;
+       }
+
+       /**
+        * Checks if a request has access to update the specified term.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function update_item_permissions_check( $request ) {
+
+               if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
+                       return false;
+               }
+
+               $term = get_term( (int) $request['id'], $this->taxonomy );
+               if ( ! $term ) {
+                       return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
+               }
+
+               $taxonomy_obj = get_taxonomy( $this->taxonomy );
+               if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) {
+                       return new WP_Error( 'rest_cannot_update', __( 'Sorry, you cannot update resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Updates a single term from a taxonomy.
+        *
+        * @param WP_REST_Request $request Full details about the request
+        * @return WP_REST_Request|WP_Error
+        */
+       public function update_item( $request ) {
+               if ( isset( $request['parent'] ) ) {
+                       if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
+                               return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
+                       }
+
+                       $parent = get_term( (int) $request['parent'], $this->taxonomy );
+
+                       if ( ! $parent ) {
+                               return new WP_Error( 'rest_term_invalid', __( "Parent resource doesn't exist." ), array( 'status' => 400 ) );
+                       }
+               }
+
+               $prepared_term = $this->prepare_item_for_database( $request );
+
+               $term = get_term( (int) $request['id'], $this->taxonomy );
+
+               // Only update the term if we haz something to update.
+               if ( ! empty( $prepared_term ) ) {
+                       $update = wp_update_term( $term->term_id, $term->taxonomy, (array) $prepared_term );
+                       if ( is_wp_error( $update ) ) {
+                               return $update;
+                       }
+               }
+
+               $term = get_term( (int) $request['id'], $this->taxonomy );
+
+               /* This action is documented in lib/endpoints/class-wp-rest-terms-controller.php */
+               do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
+
+               $schema = $this->get_item_schema();
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $term, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               $request->set_param( 'context', 'view' );
+               $response = $this->prepare_item_for_response( $term, $request );
+               return rest_ensure_response( $response );
+       }
+
+       /**
+        * Checks if a request has access to delete the specified term.
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function delete_item_permissions_check( $request ) {
+               if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
+                       return false;
+               }
+               $term = get_term( (int) $request['id'], $this->taxonomy );
+               if ( ! $term ) {
+                       return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
+               }
+               $taxonomy_obj = get_taxonomy( $this->taxonomy );
+               if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you cannot delete resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+               return true;
+       }
+
+       /**
+        * Deletes a single term from a taxonomy.
+        *
+        * @param WP_REST_Request $request Full details about the request
+        * @return WP_REST_Response|WP_Error
+        */
+       public function delete_item( $request ) {
+
+               $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+               // We don't support trashing for this resource type.
+               if ( ! $force ) {
+                       return new WP_Error( 'rest_trash_not_supported', __( 'Resource does not support trashing.' ), array( 'status' => 501 ) );
+               }
+
+               $term = get_term( (int) $request['id'], $this->taxonomy );
+               $request->set_param( 'context', 'view' );
+               $response = $this->prepare_item_for_response( $term, $request );
+
+               $retval = wp_delete_term( $term->term_id, $term->taxonomy );
+               if ( ! $retval ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
+               }
+
+               /**
+                * Fires after a single term is deleted via the REST API.
+                *
+                * @param WP_Term          $term     The deleted term.
+                * @param WP_REST_Response $response The response data.
+                * @param WP_REST_Request  $request  The request sent to the API.
+                */
+               do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
+
+               return $response;
+       }
+
+       /**
+        * Prepares a single term for create or update.
+        *
+        * @param WP_REST_Request $request Request object.
+        * @return object $prepared_term Term object.
+        */
+       public function prepare_item_for_database( $request ) {
+               $prepared_term = new stdClass;
+
+               $schema = $this->get_item_schema();
+               if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
+                       $prepared_term->name = $request['name'];
+               }
+
+               if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
+                       $prepared_term->slug = $request['slug'];
+               }
+
+               if ( isset( $request['taxonomy'] ) && ! empty( $schema['properties']['taxonomy'] ) ) {
+                       $prepared_term->taxonomy = $request['taxonomy'];
+               }
+
+               if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
+                       $prepared_term->description = $request['description'];
+               }
+
+               if ( isset( $request['parent'] ) && ! empty( $schema['properties']['parent'] ) ) {
+                       $parent_term_id = 0;
+                       $parent_term = get_term( (int) $request['parent'], $this->taxonomy );
+
+                       if ( $parent_term ) {
+                               $parent_term_id = $parent_term->term_id;
+                       }
+
+                       $prepared_term->parent = $parent_term_id;
+               }
+
+               /**
+                * Filters term data before inserting term via the REST API.
+                *
+                * @param object          $prepared_term Term object.
+                * @param WP_REST_Request $request       Request object.
+                */
+               return apply_filters( "rest_pre_insert_{$this->taxonomy}", $prepared_term, $request );
+       }
+
+       /**
+        * Prepares a single term output for response.
+        *
+        * @param obj             $item    Term object.
+        * @param WP_REST_Request $request Request object.
+        * @return WP_REST_Response $response
+        */
+       public function prepare_item_for_response( $item, $request ) {
+
+               $schema = $this->get_item_schema();
+               $data = array();
+               if ( ! empty( $schema['properties']['id'] ) ) {
+                       $data['id'] = (int) $item->term_id;
+               }
+               if ( ! empty( $schema['properties']['count'] ) ) {
+                       $data['count'] = (int) $item->count;
+               }
+               if ( ! empty( $schema['properties']['description'] ) ) {
+                       $data['description'] = $item->description;
+               }
+               if ( ! empty( $schema['properties']['link'] ) ) {
+                       $data['link'] = get_term_link( $item );
+               }
+               if ( ! empty( $schema['properties']['name'] ) ) {
+                       $data['name'] = $item->name;
+               }
+               if ( ! empty( $schema['properties']['slug'] ) ) {
+                       $data['slug'] = $item->slug;
+               }
+               if ( ! empty( $schema['properties']['taxonomy'] ) ) {
+                       $data['taxonomy'] = $item->taxonomy;
+               }
+               if ( ! empty( $schema['properties']['parent'] ) ) {
+                       $data['parent'] = (int) $item->parent;
+               }
+               if ( ! empty( $schema['properties']['meta'] ) ) {
+                       $data['meta'] = $this->meta->get_value( $item->term_id, $request );
+               }
+
+               $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+               $data = $this->add_additional_fields_to_object( $data, $request );
+               $data = $this->filter_response_by_context( $data, $context );
+
+               $response = rest_ensure_response( $data );
+
+               $response->add_links( $this->prepare_links( $item ) );
+
+               /**
+                * Filters a term item returned from the API.
+                *
+                * Allows modification of the term data right before it is returned.
+                *
+                * @param WP_REST_Response  $response  The response object.
+                * @param object            $item      The original term object.
+                * @param WP_REST_Request   $request   Request used to generate the response.
+                */
+               return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $item, $request );
+       }
+
+       /**
+        * Prepares links for the request.
+        *
+        * @param object $term Term object.
+        * @return array Links for the given term.
+        */
+       protected function prepare_links( $term ) {
+               $base = $this->namespace . '/' . $this->rest_base;
+               $links = array(
+                       'self'       => array(
+                               'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
+                       ),
+                       'collection' => array(
+                               'href' => rest_url( $base ),
+                       ),
+                       'about'      => array(
+                               'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
+                       ),
+               );
+
+               if ( $term->parent ) {
+                       $parent_term = get_term( (int) $term->parent, $term->taxonomy );
+                       if ( $parent_term ) {
+                               $links['up'] = array(
+                                       'href'       => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
+                                       'embeddable' => true,
+                               );
+                       }
+               }
+
+               $taxonomy_obj = get_taxonomy( $term->taxonomy );
+               if ( empty( $taxonomy_obj->object_type ) ) {
+                       return $links;
+               }
+
+               $post_type_links = array();
+               foreach ( $taxonomy_obj->object_type as $type ) {
+                       $post_type_object = get_post_type_object( $type );
+                       if ( empty( $post_type_object->show_in_rest ) ) {
+                               continue;
+                       }
+                       $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
+                       $post_type_links[] = array(
+                               'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( sprintf( 'wp/v2/%s', $rest_base ) ) ),
+                       );
+               }
+               if ( ! empty( $post_type_links ) ) {
+                       $links['https://api.w.org/post_type'] = $post_type_links;
+               }
+
+               return $links;
+       }
+
+       /**
+        * Gets the term's schema, conforming to JSON Schema.
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'    => 'http://json-schema.org/draft-04/schema#',
+                       'title'      => 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy,
+                       'type'       => 'object',
+                       'properties' => array(
+                               'id'          => array(
+                                       'description'  => __( 'Unique identifier for the resource.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view', 'embed', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'count'       => array(
+                                       'description'  => __( 'Number of published posts for the resource.' ),
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'description' => array(
+                                       'description'  => __( 'HTML description of the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'edit' ),
+                                       'arg_options'  => array(
+                                               'sanitize_callback' => 'wp_filter_post_kses',
+                                       ),
+                               ),
+                               'link'        => array(
+                                       'description'  => __( 'URL to the resource.' ),
+                                       'type'         => 'string',
+                                       'format'       => 'uri',
+                                       'context'      => array( 'view', 'embed', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                               'name'        => array(
+                                       'description'  => __( 'HTML title for the resource.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'embed', 'edit' ),
+                                       'arg_options'  => array(
+                                               'sanitize_callback' => 'sanitize_text_field',
+                                       ),
+                                       'required'     => true,
+                               ),
+                               'slug'        => array(
+                                       'description'  => __( 'An alphanumeric identifier for the resource unique to its type.' ),
+                                       'type'         => 'string',
+                                       'context'      => array( 'view', 'embed', 'edit' ),
+                                       'arg_options'  => array(
+                                               'sanitize_callback' => array( $this, 'sanitize_slug' ),
+                                       ),
+                               ),
+                               'taxonomy'    => array(
+                                       'description'  => __( 'Type attribution for the resource.' ),
+                                       'type'         => 'string',
+                                       'enum'         => array_keys( get_taxonomies() ),
+                                       'context'      => array( 'view', 'embed', 'edit' ),
+                                       'readonly'     => true,
+                               ),
+                       ),
+               );
+               $taxonomy = get_taxonomy( $this->taxonomy );
+               if ( $taxonomy->hierarchical ) {
+                       $schema['properties']['parent'] = array(
+                               'description'  => __( 'The id for the parent of the resource.' ),
+                               'type'         => 'integer',
+                               'context'      => array( 'view', 'edit' ),
+                       );
+               }
+
+               $schema['properties']['meta'] = $this->meta->get_field_schema();
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Gets the query params for collections.
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               $query_params = parent::get_collection_params();
+               $taxonomy = get_taxonomy( $this->taxonomy );
+
+               $query_params['context']['default'] = 'view';
+
+               $query_params['exclude'] = array(
+                       'description'       => __( 'Ensure result set excludes specific ids.' ),
+                       'type'              => 'array',
+                       'default'           => array(),
+                       'sanitize_callback' => 'wp_parse_id_list',
+               );
+               $query_params['include'] = array(
+                       'description'       => __( 'Limit result set to specific ids.' ),
+                       'type'              => 'array',
+                       'default'           => array(),
+                       'sanitize_callback' => 'wp_parse_id_list',
+               );
+               if ( ! $taxonomy->hierarchical ) {
+                       $query_params['offset'] = array(
+                               'description'       => __( 'Offset the result set by a specific number of items.' ),
+                               'type'              => 'integer',
+                               'sanitize_callback' => 'absint',
+                               'validate_callback' => 'rest_validate_request_arg',
+                       );
+               }
+               $query_params['order'] = array(
+                       'description'       => __( 'Order sort attribute ascending or descending.' ),
+                       'type'              => 'string',
+                       'sanitize_callback' => 'sanitize_key',
+                       'default'           => 'asc',
+                       'enum'              => array(
+                               'asc',
+                               'desc',
+                       ),
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $query_params['orderby'] = array(
+                       'description'       => __( 'Sort collection by resource attribute.' ),
+                       'type'              => 'string',
+                       'sanitize_callback' => 'sanitize_key',
+                       'default'           => 'name',
+                       'enum'              => array(
+                               'id',
+                               'include',
+                               'name',
+                               'slug',
+                               'term_group',
+                               'description',
+                               'count',
+                       ),
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $query_params['hide_empty'] = array(
+                       'description'       => __( 'Whether to hide resources not assigned to any posts.' ),
+                       'type'              => 'boolean',
+                       'default'           => false,
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               if ( $taxonomy->hierarchical ) {
+                       $query_params['parent'] = array(
+                               'description'       => __( 'Limit result set to resources assigned to a specific parent.' ),
+                               'type'              => 'integer',
+                               'sanitize_callback' => 'absint',
+                               'validate_callback' => 'rest_validate_request_arg',
+                       );
+               }
+               $query_params['post'] = array(
+                       'description'       => __( 'Limit result set to resources assigned to a specific post.' ),
+                       'type'              => 'integer',
+                       'default'           => null,
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               $query_params['slug'] = array(
+                       'description'       => __( 'Limit result set to resources with a specific slug.' ),
+                       'type'              => 'string',
+                       'validate_callback' => 'rest_validate_request_arg',
+               );
+               return $query_params;
+       }
+
+       /**
+        * Checks that the taxonomy is valid.
+        *
+        * @param string $taxonomy Taxonomy to check.
+        * @return WP_Error|boolean
+        */
+       protected function check_is_taxonomy_allowed( $taxonomy ) {
+               $taxonomy_obj = get_taxonomy( $taxonomy );
+               if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
+                       return true;
+               }
+               return false;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestuserscontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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                               (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,990 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Access users
+ */
+class WP_REST_Users_Controller extends WP_REST_Controller {
+
+       /**
+        * Instance of a user meta fields object.
+        *
+        * @access protected
+        * @var WP_REST_User_Meta_Fields
+        */
+       protected $meta;
+
+       public function __construct() {
+               $this->namespace = 'wp/v2';
+               $this->rest_base = 'users';
+
+               $this->meta = new WP_REST_User_Meta_Fields();
+       }
+
+       /**
+        * Register the routes for the objects of the controller.
+        */
+       public function register_routes() {
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base, array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_items' ),
+                               'permission_callback' => array( $this, 'get_items_permissions_check' ),
+                               'args'            => $this->get_collection_params(),
+                       ),
+                       array(
+                               'methods'         => WP_REST_Server::CREATABLE,
+                               'callback'        => array( $this, 'create_item' ),
+                               'permission_callback' => array( $this, 'create_item_permissions_check' ),
+                               'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+                       array(
+                               'methods'         => WP_REST_Server::READABLE,
+                               'callback'        => array( $this, 'get_item' ),
+                               'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                               'args'            => array(
+                                       'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
+                               ),
+                       ),
+                       array(
+                               'methods'         => WP_REST_Server::EDITABLE,
+                               'callback'        => array( $this, 'update_item' ),
+                               'permission_callback' => array( $this, 'update_item_permissions_check' ),
+                               'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+                       ),
+                       array(
+                               'methods' => WP_REST_Server::DELETABLE,
+                               'callback' => array( $this, 'delete_item' ),
+                               'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+                               'args' => array(
+                                       'force'    => array(
+                                               'default'     => false,
+                                               'description' => __( 'Required to be true, as resource does not support trashing.' ),
+                                       ),
+                                       'reassign' => array(),
+                               ),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ) );
+
+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array(
+                       'methods'         => WP_REST_Server::READABLE,
+                       'callback'        => array( $this, 'get_current_item' ),
+                       'args'            => array(
+                               'context'          => array(),
+                       ),
+                       'schema' => array( $this, 'get_public_item_schema' ),
+               ));
+       }
+
+       /**
+        * Permissions check for getting all users.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_items_permissions_check( $request ) {
+               // Check if roles is specified in GET request and if user can list users.
+               if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
+                       return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot filter by role.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
+                       return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
+                       return new WP_Error( 'rest_forbidden_orderby', __( 'Sorry, you cannot order by this parameter.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Get all users
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_items( $request ) {
+
+               // Retrieve the list of registered collection query parameters.
+               $registered = $this->get_collection_params();
+
+               // This array defines mappings between public API query parameters whose
+               // values are accepted as-passed, and their internal WP_Query parameter
+               // name equivalents (some are the same). Only values which are also
+               // present in $registered will be set.
+               $parameter_mappings = array(
+                       'exclude'  => 'exclude',
+                       'include'  => 'include',
+                       'order'    => 'order',
+                       'per_page' => 'number',
+                       'search'   => 'search',
+                       'roles'    => 'role__in',
+               );
+
+               $prepared_args = array();
+
+               // For each known parameter which is both registered and present in the request,
+               // set the parameter's value on the query $prepared_args.
+               foreach ( $parameter_mappings as $api_param => $wp_param ) {
+                       if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
+                               $prepared_args[ $wp_param ] = $request[ $api_param ];
+                       }
+               }
+
+               if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
+                       $prepared_args['offset'] = $request['offset'];
+               } else {
+                       $prepared_args['offset']  = ( $request['page'] - 1 ) * $prepared_args['number'];
+               }
+
+               if ( isset( $registered['orderby'] ) ) {
+                       $orderby_possibles = array(
+                               'id'              => 'ID',
+                               'include'         => 'include',
+                               'name'            => 'display_name',
+                               'registered_date' => 'registered',
+                               'slug'            => 'user_nicename',
+                               'email'           => 'user_email',
+                               'url'             => 'user_url',
+                       );
+                       $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
+               }
+
+               if ( ! current_user_can( 'list_users' ) ) {
+                       $prepared_args['has_published_posts'] = true;
+               }
+
+               if ( ! empty( $prepared_args['search'] ) ) {
+                       $prepared_args['search'] = '*' . $prepared_args['search'] . '*';
+               }
+
+               if ( isset( $registered['slug'] ) && ! empty( $request['slug'] ) ) {
+                       $prepared_args['search'] = $request['slug'];
+                       $prepared_args['search_columns'] = array( 'user_nicename' );
+               }
+
+               /**
+                * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
+                *
+                * @see https://developer.wordpress.org/reference/classes/wp_user_query/
+                *
+                * @param array           $prepared_args Array of arguments for WP_User_Query.
+                * @param WP_REST_Request $request       The current request.
+                */
+               $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
+
+               $query = new WP_User_Query( $prepared_args );
+
+               $users = array();
+               foreach ( $query->results as $user ) {
+                       $data = $this->prepare_item_for_response( $user, $request );
+                       $users[] = $this->prepare_response_for_collection( $data );
+               }
+
+               $response = rest_ensure_response( $users );
+
+               // Store pagination values for headers then unset for count query.
+               $per_page = (int) $prepared_args['number'];
+               $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
+
+               $prepared_args['fields'] = 'ID';
+
+               $total_users = $query->get_total();
+               if ( $total_users < 1 ) {
+                       // Out-of-bounds, run the query again without LIMIT for total count
+                       unset( $prepared_args['number'], $prepared_args['offset'] );
+                       $count_query = new WP_User_Query( $prepared_args );
+                       $total_users = $count_query->get_total();
+               }
+               $response->header( 'X-WP-Total', (int) $total_users );
+               $max_pages = ceil( $total_users / $per_page );
+               $response->header( 'X-WP-TotalPages', (int) $max_pages );
+
+               $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
+               if ( $page > 1 ) {
+                       $prev_page = $page - 1;
+                       if ( $prev_page > $max_pages ) {
+                               $prev_page = $max_pages;
+                       }
+                       $prev_link = add_query_arg( 'page', $prev_page, $base );
+                       $response->link_header( 'prev', $prev_link );
+               }
+               if ( $max_pages > $page ) {
+                       $next_page = $page + 1;
+                       $next_link = add_query_arg( 'page', $next_page, $base );
+                       $response->link_header( 'next', $next_link );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access to read a user
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function get_item_permissions_check( $request ) {
+
+               $id = (int) $request['id'];
+               $user = get_userdata( $id );
+               $types = get_post_types( array( 'show_in_rest' => true ), 'names' );
+
+               if ( empty( $id ) || empty( $user->ID ) ) {
+                       return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
+               }
+
+               if ( get_current_user_id() === $id ) {
+                       return true;
+               }
+
+               if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
+                       return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
+               } elseif ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) {
+                       return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Get a single user
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_item( $request ) {
+               $id = (int) $request['id'];
+               $user = get_userdata( $id );
+
+               if ( empty( $id ) || empty( $user->ID ) ) {
+                       return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
+               }
+
+               $user = $this->prepare_item_for_response( $user, $request );
+               $response = rest_ensure_response( $user );
+
+               return $response;
+       }
+
+       /**
+        * Get the current user
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function get_current_item( $request ) {
+               $current_user_id = get_current_user_id();
+               if ( empty( $current_user_id ) ) {
+                       return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
+               }
+
+               $user = wp_get_current_user();
+               $response = $this->prepare_item_for_response( $user, $request );
+               $response = rest_ensure_response( $response );
+               $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $current_user_id ) ) );
+               $response->set_status( 302 );
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access create users
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function create_item_permissions_check( $request ) {
+
+               if ( ! current_user_can( 'create_users' ) ) {
+                       return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Create a single user
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function create_item( $request ) {
+               if ( ! empty( $request['id'] ) ) {
+                       return new WP_Error( 'rest_user_exists', __( 'Cannot create existing resource.' ), array( 'status' => 400 ) );
+               }
+
+               $schema = $this->get_item_schema();
+
+               if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
+                       $check_permission = $this->check_role_update( $request['id'], $request['roles'] );
+                       if ( is_wp_error( $check_permission ) ) {
+                               return $check_permission;
+                       }
+               }
+
+               $user = $this->prepare_item_for_database( $request );
+
+               if ( is_multisite() ) {
+                       $ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );
+                       if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) {
+                               return $ret['errors'];
+                       }
+               }
+
+               if ( is_multisite() ) {
+                       $user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
+                       if ( ! $user_id ) {
+                               return new WP_Error( 'rest_user_create', __( 'Error creating new resource.' ), array( 'status' => 500 ) );
+                       }
+                       $user->ID = $user_id;
+                       $user_id = wp_update_user( $user );
+                       if ( is_wp_error( $user_id ) ) {
+                               return $user_id;
+                       }
+               } else {
+                       $user_id = wp_insert_user( $user );
+                       if ( is_wp_error( $user_id ) ) {
+                               return $user_id;
+                       }
+               }
+
+               $user = get_user_by( 'id', $user_id );
+               if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
+                       array_map( array( $user, 'add_role' ), $request['roles'] );
+               }
+
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], $user_id );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $user, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               /**
+                * Fires after a user is created or updated via the REST API.
+                *
+                * @param WP_User         $user      Data used to create the user.
+                * @param WP_REST_Request $request   Request object.
+                * @param boolean         $creating  True when creating user, false when updating user.
+                */
+               do_action( 'rest_insert_user', $user, $request, true );
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $user, $request );
+               $response = rest_ensure_response( $response );
+               $response->set_status( 201 );
+               $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) );
+
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access update a user
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function update_item_permissions_check( $request ) {
+
+               $id = (int) $request['id'];
+
+               if ( ! current_user_can( 'edit_user', $id ) ) {
+                       return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               if ( ! empty( $request['roles'] ) && ! current_user_can( 'edit_users' ) ) {
+                       return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of this resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Update a single user
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function update_item( $request ) {
+               $id = (int) $request['id'];
+
+               $user = get_userdata( $id );
+               if ( ! $user ) {
+                       return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
+               }
+
+               if ( email_exists( $request['email'] ) && $request['email'] !== $user->user_email ) {
+                       return new WP_Error( 'rest_user_invalid_email', __( 'Email address is invalid.' ), array( 'status' => 400 ) );
+               }
+
+               if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
+                       return new WP_Error( 'rest_user_invalid_argument', __( "Username isn't editable." ), array( 'status' => 400 ) );
+               }
+
+               if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
+                       return new WP_Error( 'rest_user_invalid_slug', __( 'Slug is invalid.' ), array( 'status' => 400 ) );
+               }
+
+               if ( ! empty( $request['roles'] ) ) {
+                       $check_permission = $this->check_role_update( $id, $request['roles'] );
+                       if ( is_wp_error( $check_permission ) ) {
+                               return $check_permission;
+                       }
+               }
+
+               $user = $this->prepare_item_for_database( $request );
+
+               // Ensure we're operating on the same user we already checked
+               $user->ID = $id;
+
+               $user_id = wp_update_user( $user );
+               if ( is_wp_error( $user_id ) ) {
+                       return $user_id;
+               }
+
+               $user = get_user_by( 'id', $id );
+               if ( ! empty( $request['roles'] ) ) {
+                       array_map( array( $user, 'add_role' ), $request['roles'] );
+               }
+
+               $schema = $this->get_item_schema();
+               if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+                       $meta_update = $this->meta->update_value( $request['meta'], $id );
+                       if ( is_wp_error( $meta_update ) ) {
+                               return $meta_update;
+                       }
+               }
+
+               $fields_update = $this->update_additional_fields_for_object( $user, $request );
+               if ( is_wp_error( $fields_update ) ) {
+                       return $fields_update;
+               }
+
+               /* This action is documented in lib/endpoints/class-wp-rest-users-controller.php */
+               do_action( 'rest_insert_user', $user, $request, false );
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $user, $request );
+               $response = rest_ensure_response( $response );
+               return $response;
+       }
+
+       /**
+        * Check if a given request has access delete a user
+        *
+        * @param  WP_REST_Request $request Full details about the request.
+        * @return WP_Error|boolean
+        */
+       public function delete_item_permissions_check( $request ) {
+
+               $id = (int) $request['id'];
+
+               if ( ! current_user_can( 'delete_user', $id ) ) {
+                       return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.' ), array( 'status' => rest_authorization_required_code() ) );
+               }
+
+               return true;
+       }
+
+       /**
+        * Delete a single user
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_Error|WP_REST_Response
+        */
+       public function delete_item( $request ) {
+               $id = (int) $request['id'];
+               $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
+               $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+               // We don't support trashing for this type, error out
+               if ( ! $force ) {
+                       return new WP_Error( 'rest_trash_not_supported', __( 'Users do not support trashing.' ), array( 'status' => 501 ) );
+               }
+
+               $user = get_userdata( $id );
+               if ( ! $user ) {
+                       return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
+               }
+
+               if ( ! empty( $reassign ) ) {
+                       if ( $reassign === $id || ! get_userdata( $reassign ) ) {
+                               return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid resource id for reassignment.' ), array( 'status' => 400 ) );
+                       }
+               }
+
+               $request->set_param( 'context', 'edit' );
+               $response = $this->prepare_item_for_response( $user, $request );
+
+               /** Include admin user functions to get access to wp_delete_user() */
+               require_once ABSPATH . 'wp-admin/includes/user.php';
+
+               $result = wp_delete_user( $id, $reassign );
+
+               if ( ! $result ) {
+                       return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
+               }
+
+               /**
+                * Fires after a user is deleted via the REST API.
+                *
+                * @param WP_User          $user     The user data.
+                * @param WP_REST_Response $response The response returned from the API.
+                * @param WP_REST_Request  $request  The request sent to the API.
+                */
+               do_action( 'rest_delete_user', $user, $response, $request );
+
+               return $response;
+       }
+
+       /**
+        * Prepare a single user output for response
+        *
+        * @param object $user User object.
+        * @param WP_REST_Request $request Request object.
+        * @return WP_REST_Response $response Response data.
+        */
+       public function prepare_item_for_response( $user, $request ) {
+
+               $data = array();
+               $schema = $this->get_item_schema();
+               if ( ! empty( $schema['properties']['id'] ) ) {
+                       $data['id'] = $user->ID;
+               }
+
+               if ( ! empty( $schema['properties']['username'] ) ) {
+                       $data['username'] = $user->user_login;
+               }
+
+               if ( ! empty( $schema['properties']['name'] ) ) {
+                       $data['name'] = $user->display_name;
+               }
+
+               if ( ! empty( $schema['properties']['first_name'] ) ) {
+                       $data['first_name'] = $user->first_name;
+               }
+
+               if ( ! empty( $schema['properties']['last_name'] ) ) {
+                       $data['last_name'] = $user->last_name;
+               }
+
+               if ( ! empty( $schema['properties']['email'] ) ) {
+                       $data['email'] = $user->user_email;
+               }
+
+               if ( ! empty( $schema['properties']['url'] ) ) {
+                       $data['url'] = $user->user_url;
+               }
+
+               if ( ! empty( $schema['properties']['description'] ) ) {
+                       $data['description'] = $user->description;
+               }
+
+               if ( ! empty( $schema['properties']['link'] ) ) {
+                       $data['link'] = get_author_posts_url( $user->ID, $user->user_nicename );
+               }
+
+               if ( ! empty( $schema['properties']['nickname'] ) ) {
+                       $data['nickname'] = $user->nickname;
+               }
+
+               if ( ! empty( $schema['properties']['slug'] ) ) {
+                       $data['slug'] = $user->user_nicename;
+               }
+
+               if ( ! empty( $schema['properties']['roles'] ) ) {
+                       // Defensively call array_values() to ensure an array is returned.
+                       $data['roles'] = array_values( $user->roles );
+               }
+
+               if ( ! empty( $schema['properties']['registered_date'] ) ) {
+                       $data['registered_date'] = date( 'c', strtotime( $user->user_registered ) );
+               }
+
+               if ( ! empty( $schema['properties']['capabilities'] ) ) {
+                       $data['capabilities'] = (object) $user->allcaps;
+               }
+
+               if ( ! empty( $schema['properties']['extra_capabilities'] ) ) {
+                       $data['extra_capabilities'] = (object) $user->caps;
+               }
+
+               if ( ! empty( $schema['properties']['avatar_urls'] ) ) {
+                       $data['avatar_urls'] = rest_get_avatar_urls( $user->user_email );
+               }
+
+               if ( ! empty( $schema['properties']['meta'] ) ) {
+                       $data['meta'] = $this->meta->get_value( $user->ID, $request );
+               }
+
+               $context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
+
+               $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 );
+
+               $response->add_links( $this->prepare_links( $user ) );
+
+               /**
+                * Filter user data returned from the REST API.
+                *
+                * @param WP_REST_Response $response  The response object.
+                * @param object           $user      User object used to create response.
+                * @param WP_REST_Request  $request   Request object.
+                */
+               return apply_filters( 'rest_prepare_user', $response, $user, $request );
+       }
+
+       /**
+        * Prepare links for the request.
+        *
+        * @param WP_Post $user User object.
+        * @return array Links for the given user.
+        */
+       protected function prepare_links( $user ) {
+               $links = array(
+                       'self' => array(
+                               'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ),
+                       ),
+                       'collection' => array(
+                               'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
+                       ),
+               );
+
+               return $links;
+       }
+
+       /**
+        * Prepare a single user for create or update
+        *
+        * @param WP_REST_Request $request Request object.
+        * @return object $prepared_user User object.
+        */
+       protected function prepare_item_for_database( $request ) {
+               $prepared_user = new stdClass;
+
+               $schema = $this->get_item_schema();
+
+               // required arguments.
+               if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) {
+                       $prepared_user->user_email = $request['email'];
+               }
+               if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) {
+                       $prepared_user->user_login = $request['username'];
+               }
+               if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) {
+                       $prepared_user->user_pass = $request['password'];
+               }
+
+               // optional arguments.
+               if ( isset( $request['id'] ) ) {
+                       $prepared_user->ID = absint( $request['id'] );
+               }
+               if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
+                       $prepared_user->display_name = $request['name'];
+               }
+               if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) {
+                       $prepared_user->first_name = $request['first_name'];
+               }
+               if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) {
+                       $prepared_user->last_name = $request['last_name'];
+               }
+               if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) {
+                       $prepared_user->nickname = $request['nickname'];
+               }
+               if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
+                       $prepared_user->user_nicename = $request['slug'];
+               }
+               if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
+                       $prepared_user->description = $request['description'];
+               }
+
+               if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) {
+                       $prepared_user->user_url = $request['url'];
+               }
+
+               // setting roles will be handled outside of this function.
+               if ( isset( $request['roles'] ) ) {
+                       $prepared_user->role = false;
+               }
+
+               /**
+                * Filter user data before inserting user via the REST API.
+                *
+                * @param object          $prepared_user User object.
+                * @param WP_REST_Request $request       Request object.
+                */
+               return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
+       }
+
+       /**
+        * Determine if the current user is allowed to make the desired roles change.
+        *
+        * @param integer $user_id User ID.
+        * @param array   $roles   New user roles.
+        * @return WP_Error|boolean
+        */
+       protected function check_role_update( $user_id, $roles ) {
+               global $wp_roles;
+
+               foreach ( $roles as $role ) {
+
+                       if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
+                               return new WP_Error( 'rest_user_invalid_role', sprintf( __( 'The role %s does not exist.' ), $role ), array( 'status' => 400 ) );
+                       }
+
+                       $potential_role = $wp_roles->role_objects[ $role ];
+                       // Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
+                       // Multisite super admins can freely edit their blog roles -- they possess all caps.
+                       if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $user_id && ! $potential_role->has_cap( 'edit_users' ) ) {
+                               return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => rest_authorization_required_code() ) );
+                       }
+
+                       // The new role must be editable by the logged-in user.
+
+                       /** Include admin functions to get access to get_editable_roles() */
+                       require_once ABSPATH . 'wp-admin/includes/admin.php';
+
+                       $editable_roles = get_editable_roles();
+                       if ( empty( $editable_roles[ $role ] ) ) {
+                               return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => 403 ) );
+                       }
+               }
+
+               return true;
+
+       }
+
+       /**
+        * Get the User's schema, conforming to JSON Schema
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'    => 'http://json-schema.org/draft-04/schema#',
+                       'title'      => 'user',
+                       'type'       => 'object',
+                       'properties' => array(
+                               'id'          => array(
+                                       'description' => __( 'Unique identifier for the resource.' ),
+                                       'type'        => 'integer',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                               'username'    => array(
+                                       'description' => __( 'Login name for the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'edit' ),
+                                       'required'    => true,
+                                       'arg_options' => array(
+                                               'sanitize_callback' => 'sanitize_user',
+                                       ),
+                               ),
+                               'name'        => array(
+                                       'description' => __( 'Display name for the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => 'sanitize_text_field',
+                                       ),
+                               ),
+                               'first_name'  => array(
+                                       'description' => __( 'First name for the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'edit' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => 'sanitize_text_field',
+                                       ),
+                               ),
+                               'last_name'   => array(
+                                       'description' => __( 'Last name for the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'edit' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => 'sanitize_text_field',
+                                       ),
+                               ),
+                               'email'       => array(
+                                       'description' => __( 'The email address for the resource.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'email',
+                                       'context'     => array( 'edit' ),
+                                       'required'    => true,
+                               ),
+                               'url'         => array(
+                                       'description' => __( 'URL of the resource.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'uri',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                               ),
+                               'description' => array(
+                                       'description' => __( 'Description of the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => 'wp_filter_post_kses',
+                                       ),
+                               ),
+                               'link'        => array(
+                                       'description' => __( 'Author URL to the resource.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'uri',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                               'nickname'    => array(
+                                       'description' => __( 'The nickname for the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'edit' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => 'sanitize_text_field',
+                                       ),
+                               ),
+                               'slug'        => array(
+                                       'description' => __( 'An alphanumeric identifier for the resource.' ),
+                                       'type'        => 'string',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                                       'arg_options' => array(
+                                               'sanitize_callback' => array( $this, 'sanitize_slug' ),
+                                       ),
+                               ),
+                               'registered_date' => array(
+                                       'description' => __( 'Registration date for the resource.' ),
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                                       'context'     => array( 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                               'roles'           => array(
+                                       'description' => __( 'Roles assigned to the resource.' ),
+                                       'type'        => 'array',
+                                       'context'     => array( 'edit' ),
+                               ),
+                               'password'        => array(
+                                       'description' => __( 'Password for the resource (never included).' ),
+                                       'type'        => 'string',
+                                       'context'     => array(), // Password is never displayed
+                                       'required'    => true,
+                               ),
+                               'capabilities'    => array(
+                                       'description' => __( 'All capabilities assigned to the resource.' ),
+                                       'type'        => 'object',
+                                       'context'     => array( 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                               'extra_capabilities' => array(
+                                       'description' => __( 'Any extra capabilities assigned to the resource.' ),
+                                       'type'        => 'object',
+                                       'context'     => array( 'edit' ),
+                                       'readonly'    => true,
+                               ),
+                       ),
+               );
+
+               if ( get_option( 'show_avatars' ) ) {
+                       $avatar_properties = array();
+
+                       $avatar_sizes = rest_get_avatar_sizes();
+                       foreach ( $avatar_sizes as $size ) {
+                               $avatar_properties[ $size ] = array(
+                                       'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
+                                       'type'        => 'string',
+                                       'format'      => 'uri',
+                                       'context'     => array( 'embed', 'view', 'edit' ),
+                               );
+                       }
+
+                       $schema['properties']['avatar_urls']  = array(
+                               'description' => __( 'Avatar URLs for the resource.' ),
+                               'type'        => 'object',
+                               'context'     => array( 'embed', 'view', 'edit' ),
+                               'readonly'    => true,
+                               'properties'  => $avatar_properties,
+                       );
+               }
+
+               $schema['properties']['meta'] = $this->meta->get_field_schema();
+
+               return $this->add_additional_fields_schema( $schema );
+       }
+
+       /**
+        * Get the query params for collections
+        *
+        * @return array
+        */
+       public function get_collection_params() {
+               $query_params = parent::get_collection_params();
+
+               $query_params['context']['default'] = 'view';
+
+               $query_params['exclude'] = array(
+                       'description'        => __( 'Ensure result set excludes specific ids.' ),
+                       'type'               => 'array',
+                       'default'            => array(),
+                       'sanitize_callback'  => 'wp_parse_id_list',
+               );
+               $query_params['include'] = array(
+                       'description'        => __( 'Limit result set to specific ids.' ),
+                       'type'               => 'array',
+                       'default'            => array(),
+                       'sanitize_callback'  => 'wp_parse_id_list',
+               );
+               $query_params['offset'] = array(
+                       'description'        => __( 'Offset the result set by a specific number of items.' ),
+                       'type'               => 'integer',
+                       'sanitize_callback'  => 'absint',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $query_params['order'] = array(
+                       'default'            => 'asc',
+                       'description'        => __( 'Order sort attribute ascending or descending.' ),
+                       'enum'               => array( 'asc', 'desc' ),
+                       'sanitize_callback'  => 'sanitize_key',
+                       'type'               => 'string',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $query_params['orderby'] = array(
+                       'default'            => 'name',
+                       'description'        => __( 'Sort collection by object attribute.' ),
+                       'enum'               => array(
+                               'id',
+                               'include',
+                               'name',
+                               'registered_date',
+                               'slug',
+                               'email',
+                               'url',
+                       ),
+                       'sanitize_callback'  => 'sanitize_key',
+                       'type'               => 'string',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $query_params['slug']    = array(
+                       'description'        => __( 'Limit result set to resources with a specific slug.' ),
+                       'type'               => 'string',
+                       'validate_callback'  => 'rest_validate_request_arg',
+               );
+               $query_params['roles']   = array(
+                       'description'        => __( 'Limit result set to resources matching at least one specific role provided. Accepts csv list or single role.' ),
+                       'type'               => 'array',
+                       'sanitize_callback'  => 'wp_parse_slug_list',
+               );
+               return $query_params;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapifieldsclasswprestcommentmetafieldsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php                               (rev 0)
+++ trunk/src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,21 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields {
+       /**
+        * Get the object type for meta.
+        *
+        * @return string
+        */
+       protected function get_meta_type() {
+               return 'comment';
+       }
+
+       /**
+        * Get the type for `register_rest_field`.
+        *
+        * @return string
+        */
+       public function get_rest_field_type() {
+               return 'comment';
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapifieldsclasswprestmetafieldsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php                               (rev 0)
+++ trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,365 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Manage meta values for an object.
+ */
+abstract class WP_REST_Meta_Fields {
+
+       /**
+        * Get the object type for meta.
+        *
+        * @return string One of 'post', 'comment', 'term', 'user', or anything
+        *                else supported by `_get_meta_table()`.
+        */
+       abstract protected function get_meta_type();
+
+       /**
+        * Get the object type for `register_rest_field`.
+        *
+        * @return string Custom post type, 'taxonomy', 'comment', or `user`.
+        */
+       abstract protected function get_rest_field_type();
+
+       /**
+        * Register the meta field.
+        */
+       public function register_field() {
+               register_rest_field( $this->get_rest_field_type(), 'meta', array(
+                       'get_callback' => array( $this, 'get_value' ),
+                       'update_callback' => array( $this, 'update_value' ),
+                       'schema' => $this->get_field_schema(),
+               ));
+       }
+
+       /**
+        * Get the `meta` field value.
+        *
+        * @param int             $object_id Object ID to fetch meta for.
+        * @param WP_REST_Request $request   Full details about the request.
+        * @return WP_Error|object
+        */
+       public function get_value( $object_id, $request ) {
+               $fields   = $this->get_registered_fields();
+               $response = array();
+
+               foreach ( $fields as $name => $args ) {
+                       $all_values = get_metadata( $this->get_meta_type(), $object_id, $name, false );
+                       if ( $args['single'] ) {
+                               if ( empty( $all_values ) ) {
+                                       $value = $args['schema']['default'];
+                               } else {
+                                       $value = $all_values[0];
+                               }
+                               $value = $this->prepare_value_for_response( $value, $request, $args );
+                       } else {
+                               $value = array();
+                               foreach ( $all_values as $row ) {
+                                       $value[] = $this->prepare_value_for_response( $row, $request, $args );
+                               }
+                       }
+
+                       $response[ $name ] = $value;
+               }
+
+               return (object) $response;
+       }
+
+       /**
+        * Prepare value for response.
+        *
+        * This is required because some native types cannot be stored correctly in
+        * the database, such as booleans. We need to cast back to the relevant
+        * type before passing back to JSON.
+        *
+        * @param mixed           $value   Value to prepare.
+        * @param WP_REST_Request $request Current request object.
+        * @param array           $args    Options for the field.
+        * @return mixed Prepared value.
+        */
+       protected function prepare_value_for_response( $value, $request, $args ) {
+               if ( ! empty( $args['prepare_callback'] ) ) {
+                       $value = call_user_func( $args['prepare_callback'], $value, $request, $args );
+               }
+
+               return $value;
+       }
+
+       /**
+        * Update meta values.
+        *
+        * @param WP_REST_Request $request    Full details about the request.
+        * @param int             $object_id  Object ID to fetch meta for.
+        * @return WP_Error|null Error if one occurs, null on success.
+        */
+       public function update_value( $request, $object_id ) {
+               $fields = $this->get_registered_fields();
+
+               foreach ( $fields as $name => $args ) {
+                       if ( ! array_key_exists( $name, $request ) ) {
+                               continue;
+                       }
+
+                       // A null value means reset the field, which is essentially deleting it
+                       // from the database and then relying on the default value.
+                       if ( is_null( $request[ $name ] ) ) {
+                               $result = $this->delete_meta_value( $object_id, $name );
+                       } elseif ( $args['single'] ) {
+                               $result = $this->update_meta_value( $object_id, $name, $request[ $name ] );
+                       } else {
+                               $result = $this->update_multi_meta_value( $object_id, $name, $request[ $name ] );
+                       }
+
+                       if ( is_wp_error( $result ) ) {
+                               return $result;
+                       }
+               }
+
+               return null;
+       }
+
+       /**
+        * Delete meta value for an object.
+        *
+        * @param int    $object_id Object ID the field belongs to.
+        * @param string $name      Key for the field.
+        * @return bool|WP_Error True if meta field is deleted, error otherwise.
+        */
+       protected function delete_meta_value( $object_id, $name ) {
+               if ( ! current_user_can( 'delete_post_meta', $object_id, $name ) ) {
+                       return new WP_Error(
+                               'rest_cannot_delete',
+                               sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
+                               array( 'key' => $name, 'status' => rest_authorization_required_code() )
+                       );
+               }
+
+               if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ) ) ) {
+                       return new WP_Error(
+                               'rest_meta_database_error',
+                               __( 'Could not delete meta value from database.' ),
+                               array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
+                       );
+               }
+
+               return true;
+       }
+
+       /**
+        * Update multiple meta values for an object.
+        *
+        * Alters the list of values in the database to match the list of provided values.
+        *
+        * @param int    $object_id Object ID to update.
+        * @param string $name      Key for the custom field.
+        * @param array  $values    List of values to update to.
+        * @return bool|WP_Error True if meta fields are updated, error otherwise.
+        */
+       protected function update_multi_meta_value( $object_id, $name, $values ) {
+               if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
+                       return new WP_Error(
+                               'rest_cannot_update',
+                               sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
+                               array( 'key' => $name, 'status' => rest_authorization_required_code() )
+                       );
+               }
+
+               $current = get_metadata( $this->get_meta_type(), $object_id, $name, false );
+
+               $to_remove = $current;
+               $to_add = $values;
+               foreach ( $to_add as $add_key => $value ) {
+                       $remove_keys = array_keys( $to_remove, $value, true );
+                       if ( empty( $remove_keys ) ) {
+                               continue;
+                       }
+
+                       if ( count( $remove_keys ) > 1 ) {
+                               // To remove, we need to remove first, then add, so don't touch.
+                               continue;
+                       }
+
+                       $remove_key = $remove_keys[0];
+                       unset( $to_remove[ $remove_key ] );
+                       unset( $to_add[ $add_key ] );
+               }
+
+               // `delete_metadata` removes _all_ instances of the value, so only call
+               // once.
+               $to_remove = array_unique( $to_remove );
+               foreach ( $to_remove as $value ) {
+                       if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
+                               return new WP_Error(
+                                       'rest_meta_database_error',
+                                       __( 'Could not update meta value in database.' ),
+                                       array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
+                               );
+                       }
+               }
+               foreach ( $to_add as $value ) {
+                       if ( ! add_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
+                               return new WP_Error(
+                                       'rest_meta_database_error',
+                                       __( 'Could not update meta value in database.' ),
+                                       array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
+                               );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Update meta value for an object.
+        *
+        * @param int    $object_id Object ID to update.
+        * @param string $name      Key for the custom field.
+        * @param mixed  $value     Updated value.
+        * @return bool|WP_Error True if meta field is updated, error otherwise.
+        */
+       protected function update_meta_value( $object_id, $name, $value ) {
+               if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
+                       return new WP_Error(
+                               'rest_cannot_update',
+                               sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
+                               array( 'key' => $name, 'status' => rest_authorization_required_code() )
+                       );
+               }
+
+               $meta_type  = $this->get_meta_type();
+               $meta_key   = wp_slash( $name );
+               $meta_value = wp_slash( $value );
+
+               // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
+               $old_value = get_metadata( $meta_type, $object_id, $meta_key );
+               if ( 1 === count( $old_value ) ) {
+                       if ( $old_value[0] === $meta_value ) {
+                               return true;
+                       }
+               }
+
+               if ( ! update_metadata( $meta_type, $object_id, $meta_key, $meta_value ) ) {
+                       return new WP_Error(
+                               'rest_meta_database_error',
+                               __( 'Could not update meta value in database.' ),
+                               array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
+                       );
+               }
+
+               return true;
+       }
+
+       /**
+        * Get all the registered meta fields.
+        *
+        * @return array
+        */
+       protected function get_registered_fields() {
+               $registered = array();
+
+               foreach ( get_registered_meta_keys( $this->get_meta_type() ) as $name => $args ) {
+                       if ( empty( $args['show_in_rest'] ) ) {
+                               continue;
+                       }
+
+                       $rest_args = array();
+                       if ( is_array( $args['show_in_rest'] ) ) {
+                               $rest_args = $args['show_in_rest'];
+                       }
+
+                       $default_args = array(
+                               'name'             => $name,
+                               'single'           => $args['single'],
+                               'schema'           => array(),
+                               'prepare_callback' => array( $this, 'prepare_value' ),
+                       );
+                       $default_schema = array(
+                               'type'        => null,
+                               'description' => empty( $args['description'] ) ? '' : $args['description'],
+                               'default'     => isset( $args['default'] ) ? $args['default'] : null,
+                       );
+                       $rest_args = array_merge( $default_args, $rest_args );
+                       $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
+
+                       if ( empty( $rest_args['schema']['type'] ) ) {
+                               // Skip over meta fields that don't have a defined type.
+                               if ( empty( $args['type'] ) ) {
+                                       continue;
+                               }
+
+                               if ( $rest_args['single'] ) {
+                                       $rest_args['schema']['type'] = $args['type'];
+                               } else {
+                                       $rest_args['schema']['type'] = 'array';
+                                       $rest_args['schema']['items'] = array(
+                                               'type' => $args['type'],
+                                       );
+                               }
+                       }
+
+                       $registered[ $rest_args['name'] ] = $rest_args;
+               } // End foreach().
+
+               return $registered;
+       }
+
+       /**
+        * Get the object's `meta` schema, conforming to JSON Schema.
+        *
+        * @return array
+        */
+       public function get_field_schema() {
+               $fields = $this->get_registered_fields();
+
+               $schema = array(
+                       'description' => __( 'Meta fields.' ),
+                       'type'        => 'object',
+                       'context'     => array( 'view', 'edit' ),
+                       'properties'  => array(),
+               );
+
+               foreach ( $fields as $key => $args ) {
+                       $schema['properties'][ $key ] = $args['schema'];
+               }
+
+               return $schema;
+       }
+
+       /**
+        * Prepare a meta value for output.
+        *
+        * Default preparation for meta fields. Override by passing the
+        * `prepare_callback` in your `show_in_rest` options.
+        *
+        * @param mixed           $value   Meta value from the database.
+        * @param WP_REST_Request $request Request object.
+        * @param array           $args    REST-specific options for the meta key.
+        * @return mixed Value prepared for output.
+        */
+       public static function prepare_value( $value, $request, $args ) {
+               $type = $args['schema']['type'];
+
+               // For multi-value fields, check the item type instead.
+               if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) {
+                       $type = $args['schema']['items']['type'];
+               }
+
+               switch ( $type ) {
+                       case 'string':
+                               $value = (string) $value;
+                               break;
+                       case 'number':
+                               $value = (float) $value;
+                               break;
+                       case 'boolean':
+                               $value = (bool) $value;
+                               break;
+               }
+
+               // Don't allow objects to be output.
+               if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) {
+                       return null;
+               }
+
+               return $value;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapifieldsclasswprestpostmetafieldsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php                          (rev 0)
+++ trunk/src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,37 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields {
+       /**
+        * Post type to register fields for.
+        *
+        * @var string
+        */
+       protected $post_type;
+
+       /**
+        * Constructor.
+        *
+        * @param string $post_type Post type to register fields for.
+        */
+       public function __construct( $post_type ) {
+               $this->post_type = $post_type;
+       }
+
+       /**
+        * Get the object type for meta.
+        *
+        * @return string
+        */
+       protected function get_meta_type() {
+               return 'post';
+       }
+
+       /**
+        * Get the type for `register_rest_field`.
+        *
+        * @return string Custom post type slug.
+        */
+       public function get_rest_field_type() {
+               return $this->post_type;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapifieldsclasswpresttermmetafieldsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php                          (rev 0)
+++ trunk/src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,39 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * Manage meta values for terms.
+ */
+class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields {
+       /**
+        * Taxonomy to register fields for.
+        *
+        * @var string
+        */
+       protected $taxonomy;
+       /**
+        * Constructor.
+        *
+        * @param string $taxonomy Taxonomy to register fields for.
+        */
+       public function __construct( $taxonomy ) {
+               $this->taxonomy = $taxonomy;
+       }
+
+       /**
+        * Get the object type for meta.
+        *
+        * @return string
+        */
+       protected function get_meta_type() {
+               return 'term';
+       }
+
+       /**
+        * Get the type for `register_rest_field`.
+        *
+        * @return string
+        */
+       public function get_rest_field_type() {
+               return 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy;
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesrestapifieldsclasswprestusermetafieldsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php                          (rev 0)
+++ trunk/src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,21 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields {
+       /**
+        * Get the object type for meta.
+        *
+        * @return string
+        */
+       protected function get_meta_type() {
+               return 'user';
+       }
+
+       /**
+        * Get the type for `register_rest_field`.
+        *
+        * @return string
+        */
+       public function get_rest_field_type() {
+               return 'user';
+       }
+}
</ins></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-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/rest-api.php  2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -71,6 +71,48 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Registers a new field on an existing WordPress object type.
+ *
+ * @since 4.7.0
+ *
+ * @global array $wp_rest_additional_fields Holds registered fields, organized
+ *                                          by object type.
+ *
+ * @param string|array $object_type Object(s) the field is being registered
+ *                                  to, "post"|"term"|"comment" etc.
+ * @param string $attribute         The attribute name.
+ * @param array  $args {
+ *     Optional. An array of arguments used to handle the registered field.
+ *
+ *     @type string|array|null $get_callback    Optional. The callback function used to retrieve the field
+ *                                              value. Default is 'null', the field will not be returned in
+ *                                              the response.
+ *     @type string|array|null $update_callback Optional. The callback function used to set and update the
+ *                                              field value. Default is 'null', the value cannot be set or
+ *                                              updated.
+ *     @type string|array|null $schema          Optional. The callback function used to create the schema for
+ *                                              this field. Default is 'null', no schema entry will be returned.
+ * }
+ */
+function register_rest_field( $object_type, $attribute, $args = array() ) {
+       $defaults = array(
+               'get_callback'    => null,
+               'update_callback' => null,
+               'schema'          => null,
+       );
+
+       $args = wp_parse_args( $args, $defaults );
+
+       global $wp_rest_additional_fields;
+
+       $object_types = (array) $object_type;
+
+       foreach ( $object_types as $object_type ) {
+               $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
+       }
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Registers rewrite rules for the API.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 4.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -125,6 +167,71 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Registers default REST API routes.
+ *
+ * @since 4.7.0
+ */
+function create_initial_rest_routes() {
+       foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
+               $class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller';
+
+               if ( ! class_exists( $class ) ) {
+                       continue;
+               }
+               $controller = new $class( $post_type->name );
+               if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
+                       continue;
+               }
+
+               $controller->register_routes();
+
+               if ( post_type_supports( $post_type->name, 'revisions' ) ) {
+                       $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
+                       $revisions_controller->register_routes();
+               }
+       }
+
+       // Post types.
+       $controller = new WP_REST_Post_Types_Controller;
+       $controller->register_routes();
+
+       // Post statuses.
+       $controller = new WP_REST_Post_Statuses_Controller;
+       $controller->register_routes();
+
+       // Taxonomies.
+       $controller = new WP_REST_Taxonomies_Controller;
+       $controller->register_routes();
+
+       // Terms.
+       foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
+               $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
+
+               if ( ! class_exists( $class ) ) {
+                       continue;
+               }
+               $controller = new $class( $taxonomy->name );
+               if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
+                       continue;
+               }
+
+               $controller->register_routes();
+       }
+
+       // Users.
+       $controller = new WP_REST_Users_Controller;
+       $controller->register_routes();
+
+       // Comments.
+       $controller = new WP_REST_Comments_Controller;
+       $controller->register_routes();
+
+       // Settings.
+       $controller = new WP_REST_Settings_Controller;
+       $controller->register_routes();
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Loads the REST API.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 4.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -683,3 +790,296 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return array( $local, $utc );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Returns a contextual HTTP error code for authorization failure.
+ *
+ * @since 4.7.0
+ *
+ * @return integer 401 if the user is not logged in, 403 if the user is logged in.
+ */
+function rest_authorization_required_code() {
+       return is_user_logged_in() ? 403 : 401;
+}
+
+/**
+ * Validate a request argument based on details registered to the route.
+ *
+ * @since 4.7.0
+ *
+ * @param  mixed            $value
+ * @param  WP_REST_Request  $request
+ * @param  string           $param
+ * @return WP_Error|boolean
+ */
+function rest_validate_request_arg( $value, $request, $param ) {
+       $attributes = $request->get_attributes();
+       if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
+               return true;
+       }
+       $args = $attributes['args'][ $param ];
+
+       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;
+}
+
+/**
+ * Sanitize a request argument based on details registered to the route.
+ *
+ * @since 4.7.0
+ *
+ * @param  mixed            $value
+ * @param  WP_REST_Request  $request
+ * @param  string           $param
+ * @return mixed
+ */
+function rest_sanitize_request_arg( $value, $request, $param ) {
+       $attributes = $request->get_attributes();
+       if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
+               return $value;
+       }
+       $args = $attributes['args'][ $param ];
+
+       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;
+}
+
+/**
+ * Parse a request argument based on details registered to the route.
+ *
+ * Runs a validation check and sanitizes the value, primarily to be used via
+ * the `sanitize_callback` arguments in the endpoint args registration.
+ *
+ * @since 4.7.0
+ *
+ * @param  mixed            $value
+ * @param  WP_REST_Request  $request
+ * @param  string           $param
+ * @return mixed
+ */
+function rest_parse_request_arg( $value, $request, $param ) {
+       $is_valid = rest_validate_request_arg( $value, $request, $param );
+
+       if ( is_wp_error( $is_valid ) ) {
+               return $is_valid;
+       }
+
+       $value = rest_sanitize_request_arg( $value, $request, $param );
+
+       return $value;
+}
+
+/**
+ * Determines if a IPv4 address is valid.
+ *
+ * Does not handle IPv6 addresses.
+ *
+ * @since 4.7.0
+ *
+ * @param  string $ipv4 IP 32-bit address.
+ * @return string|false The valid IPv4 address, otherwise false.
+ */
+function rest_is_ip_address( $ipv4 ) {
+       $pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
+
+       if ( ! preg_match( $pattern, $ipv4 ) ) {
+               return false;
+       }
+
+       return $ipv4;
+}
+
+/**
+ * Changes a boolean-like value into the proper boolean value.
+ *
+ * @since 4.7.0
+ *
+ * @param bool|string|int $value The value being evaluated.
+ * @return boolean Returns the proper associated boolean value.
+ */
+function rest_sanitize_boolean( $value ) {
+       // String values are translated to `true`; make sure 'false' is false.
+       if ( is_string( $value )  ) {
+               $value = strtolower( $value );
+               if ( in_array( $value, array( 'false', '0' ), true ) ) {
+                       $value = false;
+               }
+       }
+
+       // Everything else will map nicely to boolean.
+       return (boolean) $value;
+}
+
+/**
+ * Determines if a given value is boolean-like.
+ *
+ * @since 4.7.0
+ *
+ * @param bool|string $maybe_bool The value being evaluated.
+ * @return boolean True if a boolean, otherwise false.
+ */
+function rest_is_boolean( $maybe_bool ) {
+       if ( is_bool( $maybe_bool ) ) {
+               return true;
+       }
+
+       if ( is_string( $maybe_bool ) ) {
+               $maybe_bool = strtolower( $maybe_bool );
+
+               $valid_boolean_values = array(
+                       'false',
+                       'true',
+                       '0',
+                       '1',
+               );
+
+               return in_array( $maybe_bool, $valid_boolean_values, true );
+       }
+
+       if ( is_int( $maybe_bool ) ) {
+               return in_array( $maybe_bool, array( 0, 1 ), true );
+       }
+
+       return false;
+}
+
+/**
+ * Retrieves the avatar urls in various sizes based on a given email address.
+ *
+ * @since 4.7.0
+ *
+ * @see get_avatar_url()
+ *
+ * @param string $email Email address.
+ * @return array $urls Gravatar url for each size.
+ */
+function rest_get_avatar_urls( $email ) {
+       $avatar_sizes = rest_get_avatar_sizes();
+
+       $urls = array();
+       foreach ( $avatar_sizes as $size ) {
+               $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
+       }
+
+       return $urls;
+}
+
+/**
+ * Retrieves the pixel sizes for avatars.
+ *
+ * @since 4.7.0
+ *
+ * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
+ */
+function rest_get_avatar_sizes() {
+       /**
+        * Filter the REST avatar sizes.
+        *
+        * Use this filter to adjust the array of sizes returned by the
+        * `rest_get_avatar_sizes` function.
+        *
+        * @since 4.4.0
+        *
+        * @param array $sizes An array of int values that are the pixel sizes for avatars.
+        *                     Default `[ 24, 48, 96 ]`.
+        */
+       return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
+}
</ins></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></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/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/script-loader.php     2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -499,6 +499,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'media-audiovideo', "/wp-includes/js/media-audiovideo$suffix.js", array( 'media-editor' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">        $scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'jquery', 'media-views', 'media-audiovideo' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        $scripts->add( 'wp-api', "/wp-includes/js/wp-api$suffix.js", array( 'jquery', 'backbone', 'underscore' ), false, 1 );
+       did_action( 'init' ) && $scripts->localize( 'wp-api', 'wpApiSettings', array(
+               'root'          => esc_url_raw( get_rest_url() ),
+               'nonce'         => wp_create_nonce( 'wp_rest' ),
+               'versionString' => 'wp/v2/',
+       ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         if ( is_admin() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'admin-tags', "/wp-admin/js/tags$suffix.js", array( 'jquery', 'wp-ajax-response' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                did_action( 'init' ) && $scripts->localize( 'admin-tags', 'tagsl10n', array(
</span></span></pre></div>
<a id="trunksrcwpincludestaxonomyphp"></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/taxonomy.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/taxonomy.php        2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-includes/taxonomy.php  2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -67,6 +67,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'delete_terms' => 'delete_categories',
</span><span class="cx" style="display: block; padding: 0 10px">                        'assign_terms' => 'assign_categories',
</span><span class="cx" style="display: block; padding: 0 10px">                ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'show_in_rest' => true,
+               'rest_base' => 'categories',
+               'rest_controller_class' => 'WP_REST_Terms_Controller',
</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">        register_taxonomy( 'post_tag', 'post', array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -83,6 +86,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'delete_terms' => 'delete_post_tags',
</span><span class="cx" style="display: block; padding: 0 10px">                        'assign_terms' => 'assign_post_tags',
</span><span class="cx" style="display: block; padding: 0 10px">                ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'show_in_rest' => true,
+               'rest_base' => 'tags',
+               'rest_controller_class' => 'WP_REST_Terms_Controller',
</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">        register_taxonomy( 'nav_menu', 'nav_menu_item', array(
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/src/wp-settings.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -218,6 +218,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require( ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> require( ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> require( ABSPATH . WPINC . '/rest-api/class-wp-rest-request.php' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-posts-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-attachments-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-types-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-statuses-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-revisions-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-taxonomies-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' );
+require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php' );
+require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' );
+require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-term-meta-fields.php' );
+require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> $GLOBALS['wp_embed'] = new WP_Embed();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunitdataimagescodeispoetrypng"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/images/codeispoetry.png</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/data/images/codeispoetry.png                          (rev 0)
+++ trunk/tests/phpunit/data/images/codeispoetry.png    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,579 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+
+
+
+<!DOCTYPE html>
+<html lang="en" class="">
+  <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# object: http://ogp.me/ns/object# article: http://ogp.me/ns/article# profile: http://ogp.me/ns/profile#">
+    <meta charset='utf-8'>
+
+    <link crossorigin="anonymous" href="https://assets-cdn.github.com/assets/github-c1d91683d54d54239f36043630b76fe2b39dbc4fc91cfbd38ef9f3743e56c851.css" media="all" rel="stylesheet" />
+    <link crossorigin="anonymous" href="https://assets-cdn.github.com/assets/github2-6b792c972e1d61f45ce186e5e65d3410c90cf78d94776abff9512f5417eb2bac.css" media="all" rel="stylesheet" />
+    
+    
+    
+
+    <link as="script" href="https://assets-cdn.github.com/assets/frameworks-ee521b8e9facac68ff27e93fc3ae0f8ed811d7bf9e434e84f4b9ea227780b084.js" rel="preload" />
+    <link as="script" href="https://assets-cdn.github.com/assets/github-863d0e4c2905010278cfd87e9c7c738e812530db73c36c274a185354977a2e41.js" rel="preload" />
+
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta http-equiv="Content-Language" content="en">
+    <meta name="viewport" content="width=1020">
+    
+    
+    <title>wp-cli.github.com/codeispoetry.png at master · wp-cli/wp-cli.github.com · GitHub</title>
+    <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub">
+    <link rel="fluid-icon" href="https://github.com/fluidicon.png" title="GitHub">
+    <link rel="apple-touch-icon" href="/apple-touch-icon.png">
+    <link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png">
+    <link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png">
+    <link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png">
+    <link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png">
+    <link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png">
+    <link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
+    <link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png">
+    <link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png">
+    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
+    <meta property="fb:app_id" content="1401488693436528">
+
+      <meta content="https://avatars1.githubusercontent.com/u/1570774?v=3&amp;s=400" name="twitter:image:src" /><meta content="@github" name="twitter:site" /><meta content="summary" name="twitter:card" /><meta content="wp-cli/wp-cli.github.com" name="twitter:title" /><meta content="wp-cli.github.com - wp-cli.org website" name="twitter:description" />
+      <meta content="https://avatars1.githubusercontent.com/u/1570774?v=3&amp;s=400" property="og:image" /><meta content="GitHub" property="og:site_name" /><meta content="object" property="og:type" /><meta content="wp-cli/wp-cli.github.com" property="og:title" /><meta content="https://github.com/wp-cli/wp-cli.github.com" property="og:url" /><meta content="wp-cli.github.com - wp-cli.org website" property="og:description" />
+      <meta name="browser-stats-url" content="https://api.github.com/_private/browser/stats">
+    <meta name="browser-errors-url" content="https://api.github.com/_private/browser/errors">
+    <link rel="assets" href="https://assets-cdn.github.com/">
+    
+    <meta name="pjax-timeout" content="1000">
+    
+
+    <meta name="msapplication-TileImage" content="/windows-tile.png">
+    <meta name="msapplication-TileColor" content="#ffffff">
+    <meta name="selected-link" value="repo_source" data-pjax-transient>
+
+    <meta name="google-site-verification" content="KT5gs8h0wvaagLKAVWq8bbeNwnZZK1r1XQysX3xurLU">
+<meta name="google-site-verification" content="ZzhVyEFwb7w3e0-uOTltm8Jsck2F5StVihD0exw2fsA">
+    <meta name="google-analytics" content="UA-3769691-2">
+
+<meta content="collector.githubapp.com" name="octolytics-host" /><meta content="github" name="octolytics-app-id" /><meta content="32359CA2:3F6A:789832:56BA6B37" name="octolytics-dimension-request_id" />
+<meta content="/&lt;user-name&gt;/&lt;repo-name&gt;/blob/show" data-pjax-transient="true" name="analytics-location" />
+
+
+
+  <meta class="js-ga-set" name="dimension1" content="Logged Out">
+
+
+
+        <meta name="hostname" content="github.com">
+    <meta name="user-login" content="">
+
+        <meta name="expected-hostname" content="github.com">
+      <meta name="js-proxy-site-detection-payload" content="YmM2NmFhNmJmZjU3OTZhZjZiMWI2MzA3N2E0M2Y2Zjc1OTM5ODcxYjQ1OWEyYzFkMjg3Y2NkOTI1ODhiMzZmNXx7InJlbW90ZV9hZGRyZXNzIjoiNTAuNTMuMTU2LjE2MiIsInJlcXVlc3RfaWQiOiIzMjM1OUNBMjozRjZBOjc4OTgzMjo1NkJBNkIzNyJ9">
+
+      <link rel="mask-icon" href="https://assets-cdn.github.com/pinned-octocat.svg" color="#4078c0">
+      <link rel="icon" type="image/x-icon" href="https://assets-cdn.github.com/favicon.ico">
+
+    <meta content="9ca3979ba13c29867416f58457ff40e521a92049" name="form-nonce" />
+
+    <meta http-equiv="x-pjax-version" content="88e74eaed8727c649ee4773743f8723a">
+
+      
+  <meta name="description" content="wp-cli.github.com - wp-cli.org website">
+  <meta name="go-import" content="github.com/wp-cli/wp-cli.github.com git https://github.com/wp-cli/wp-cli.github.com.git">
+
+  <meta content="1570774" name="octolytics-dimension-user_id" /><meta content="wp-cli" name="octolytics-dimension-user_login" /><meta content="6535856" name="octolytics-dimension-repository_id" /><meta content="wp-cli/wp-cli.github.com" name="octolytics-dimension-repository_nwo" /><meta content="true" name="octolytics-dimension-repository_public" /><meta content="false" name="octolytics-dimension-repository_is_fork" /><meta content="6535856" name="octolytics-dimension-repository_network_root_id" /><meta content="wp-cli/wp-cli.github.com" name="octolytics-dimension-repository_network_root_nwo" />
+  <link href="https://github.com/wp-cli/wp-cli.github.com/commits/master.atom" rel="alternate" title="Recent Commits to wp-cli.github.com:master" type="application/atom+xml">
+
+
+      <link rel="canonical" href="https://github.com/wp-cli/wp-cli.github.com/blob/master/behat-data/codeispoetry.png" data-pjax-transient>
+  </head>
+
+
+  <body class="logged_out   env-production  vis-public page-blob">
+    <a href="#start-of-content" tabindex="1" class="accessibility-aid js-skip-to-content">Skip to content</a>
+
+    
+    
+    
+
+
+
+      
+      <div class="header header-logged-out" role="banner">
+  <div class="container clearfix">
+
+    <a class="header-logo-wordmark" href="https://github.com/" data-ga-click="(Logged out) Header, go to homepage, icon:logo-wordmark">
+      <svg aria-hidden="true" class="octicon octicon-logo-github" height="28" role="img" version="1.1" viewBox="0 0 45 16" width="78"><path d="M8.64 5.19H4.88c-0.11 0-0.19 0.08-0.19 0.17v1.84c0 0.09 0.08 0.17 0.19 0.17h1.47v2.3s-0.33 0.11-1.25 0.11c-1.08 0-2.58-0.39-2.58-3.7s1.58-3.73 3.05-3.73c1.27 0 1.81 0.22 2.17 0.33 0.11 0.03 0.2-0.08 0.2-0.17l0.42-1.78c0-0.05-0.02-0.09-0.06-0.14-0.14-0.09-1.02-0.58-3.2-0.58C2.58 0 0 1.06 0 6.2s2.95 5.92 5.44 5.92c2.06 0 3.31-0.89 3.31-0.89 0.05-0.02 0.06-0.09 0.06-0.13V5.36c0-0.09-0.08-0.17-0.19-0.17h0.02zM27.7 0.44h-2.13c-0.09 0-0.17 0.08-0.17 0.17v4.09h-3.31V0.61c0-0.09-0.08-0.17-0.17-0.17h-2.13c-0.09 0-0.17 0.08-0.17 0.17v11.11c0 0.09 0.09 0.17 0.17 0.17h2.13c0.09 0 0.17-0.08 0.17-0.17V6.97h3.31l-0.02 4.75c0 0.09 0.08 0.17 0.17 0.17h2.13c0.09 0 0.17-0.08 0.17-0.17V0.61c0-0.09-0.08-0.17-0.17-0.17h0.02zM11.19 0.69c-0.77 0-1.38 0.61-1.38 1.38s0.61 1.38 1.38 1.3
 8c0.75 0 1.36-0.61 1.36-1.38s-0.61-1.38-1.36-1.38z m1.22 3.55c0-0.09-0.08-0.17-0.17-0.17H10.11c-0.09 0-0.17 0.09-0.17 0.2 0 0 0 6.17 0 7.34 0 0.2 0.13 0.27 0.3 0.27 0 0 0.91 0 1.92 0 0.2 0 0.25-0.09 0.25-0.27 0-0.39 0-7.36 0-7.36v-0.02z m23.52-0.16h-2.09c-0.11 0-0.17 0.08-0.17 0.19v5.44s-0.55 0.39-1.3 0.39-0.97-0.34-0.97-1.09c0-0.73 0-4.75 0-4.75 0-0.09-0.08-0.17-0.17-0.17h-2.14c-0.09 0-0.17 0.08-0.17 0.17 0 0 0 2.91 0 5.11s1.23 2.75 2.92 2.75c1.39 0 2.52-0.77 2.52-0.77s0.05 0.39 0.08 0.45c0.02 0.05 0.09 0.09 0.16 0.09h1.34c0.11 0 0.17-0.08 0.17-0.17l0.02-7.47c0-0.09-0.08-0.17-0.19-0.17z m5.77-0.25c-1.2 0-2.02 0.53-2.02 0.53V0.59c0-0.09-0.08-0.17-0.17-0.17h-2.13c-0.09 0-0.17 0.08-0.17 0.17l-0.02 11.11c0 0.09 0.09 0.17 0.19 0.17h1.48c0.06 0 0.11-0.02 0.14-0.08 0.05-0.06 0.09-0.52 0.09-0.52s0.88 0.83 2.52 0.83c1.94 0 3.05-0.98 3.05-4.41s-1.77-3.88-2.97-3.88z m-0.83 6.27c-0.73-0.02-1.22-0.36-1.22-0.36V6.22s0.48-0.3 1.08-0.34c0.77-0.08 1.5 0.16 1.5 1.97 0 1.91-0.33 2.28-1.36 2.25z m-22.
 33-0.05c-0.09 0-0.33 0.05-0.58 0.05-0.78 0-1.05-0.36-1.05-0.83s0-3.13 0-3.13h1.59c0.09 0 0.16-0.08 0.16-0.19V4.25c0-0.09-0.08-0.17-0.16-0.17h-1.59V1.97c0-0.08-0.05-0.13-0.14-0.13H14.61c-0.09 0-0.14 0.05-0.14 0.13v2.17s-1.09 0.27-1.16 0.28c-0.08 0.02-0.13 0.09-0.13 0.17v1.36c0 0.11 0.08 0.19 0.17 0.19h1.11s0 1.44 0 3.28c0 2.44 1.7 2.69 2.86 2.69 0.53 0 1.17-0.17 1.27-0.22 0.06-0.02 0.09-0.09 0.09-0.16v-1.5c0-0.11-0.08-0.19-0.17-0.19h0.02z"></path></svg>
+    </a>
+
+    <div class="header-actions" role="navigation">
+        <a class="btn btn-primary" href="/join?source=header-repo" data-ga-click="(Logged out) Header, clicked Sign up, text:sign-up">Sign up</a>
+      <a class="btn" href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com%2Fblob%2Fmaster%2Fbehat-data%2Fcodeispoetry.png" data-ga-click="(Logged out) Header, clicked Sign in, text:sign-in">Sign in</a>
+    </div>
+
+    <div class="site-search repo-scope js-site-search" role="search">
+      <!-- </textarea> --><!-- '"` --><form accept-charset="UTF-8" action="/wp-cli/wp-cli.github.com/search" class="js-site-search-form" data-global-search-url="/search" data-repo-search-url="/wp-cli/wp-cli.github.com/search" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
+  <label class="js-chromeless-input-container form-control">
+    <div class="scope-badge">This repository</div>
+    <input type="text"
+      class="js-site-search-focus js-site-search-field is-clearable chromeless-input"
+      data-hotkey="s"
+      name="q"
+      placeholder="Search"
+      aria-label="Search this repository"
+      data-global-scope-placeholder="Search GitHub"
+      data-repo-scope-placeholder="Search"
+      tabindex="1"
+      autocapitalize="off">
+  </label>
+</form>
+    </div>
+
+      <ul class="header-nav left" role="navigation">
+          <li class="header-nav-item">
+            <a class="header-nav-link" href="/explore" data-ga-click="(Logged out) Header, go to explore, text:explore">Explore</a>
+          </li>
+          <li class="header-nav-item">
+            <a class="header-nav-link" href="/features" data-ga-click="(Logged out) Header, go to features, text:features">Features</a>
+          </li>
+          <li class="header-nav-item">
+            <a class="header-nav-link" href="https://enterprise.github.com/" data-ga-click="(Logged out) Header, go to enterprise, text:enterprise">Enterprise</a>
+          </li>
+          <li class="header-nav-item">
+            <a class="header-nav-link" href="/pricing" data-ga-click="(Logged out) Header, go to pricing, text:pricing">Pricing</a>
+          </li>
+      </ul>
+
+  </div>
+</div>
+
+
+
+    <div id="start-of-content" class="accessibility-aid"></div>
+
+      <div id="js-flash-container">
+</div>
+
+
+    <div role="main" class="main-content">
+        <div itemscope itemtype="http://schema.org/WebPage">
+    <div id="js-repo-pjax-container" class="context-loader-container js-repo-nav-next" data-pjax-container>
+      
+<div class="pagehead repohead instapaper_ignore readability-menu experiment-repo-nav">
+  <div class="container repohead-details-container">
+
+    
+
+<ul class="pagehead-actions">
+
+  <li>
+      <a href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com"
+    class="btn btn-sm btn-with-count tooltipped tooltipped-n"
+    aria-label="You must be signed in to watch a repository" rel="nofollow">
+    <svg aria-hidden="true" class="octicon octicon-eye" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M8.06 2C3 2 0 8 0 8s3 6 8.06 6c4.94 0 7.94-6 7.94-6S13 2 8.06 2z m-0.06 10c-2.2 0-4-1.78-4-4 0-2.2 1.8-4 4-4 2.22 0 4 1.8 4 4 0 2.22-1.78 4-4 4z m2-4c0 1.11-0.89 2-2 2s-2-0.89-2-2 0.89-2 2-2 2 0.89 2 2z"></path></svg>
+    Watch
+  </a>
+  <a class="social-count" href="/wp-cli/wp-cli.github.com/watchers">
+    13
+  </a>
+
+  </li>
+
+  <li>
+      <a href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com"
+    class="btn btn-sm btn-with-count tooltipped tooltipped-n"
+    aria-label="You must be signed in to star a repository" rel="nofollow">
+    <svg aria-hidden="true" class="octicon octicon-star" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M14 6l-4.9-0.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14l4.33-2.33 4.33 2.33L10.4 9.26 14 6z"></path></svg>
+    Star
+  </a>
+
+    <a class="social-count js-social-count" href="/wp-cli/wp-cli.github.com/stargazers">
+      6
+    </a>
+
+  </li>
+
+  <li>
+      <a href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com"
+        class="btn btn-sm btn-with-count tooltipped tooltipped-n"
+        aria-label="You must be signed in to fork a repository" rel="nofollow">
+        <svg aria-hidden="true" class="octicon octicon-repo-forked" height="16" role="img" version="1.1" viewBox="0 0 10 16" width="10"><path d="M8 1c-1.11 0-2 0.89-2 2 0 0.73 0.41 1.38 1 1.72v1.28L5 8 3 6v-1.28c0.59-0.34 1-0.98 1-1.72 0-1.11-0.89-2-2-2S0 1.89 0 3c0 0.73 0.41 1.38 1 1.72v1.78l3 3v1.78c-0.59 0.34-1 0.98-1 1.72 0 1.11 0.89 2 2 2s2-0.89 2-2c0-0.73-0.41-1.38-1-1.72V9.5l3-3V4.72c0.59-0.34 1-0.98 1-1.72 0-1.11-0.89-2-2-2zM2 4.2c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z m3 10c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z m3-10c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z"></path></svg>
+        Fork
+      </a>
+
+    <a href="/wp-cli/wp-cli.github.com/network" class="social-count">
+      27
+    </a>
+  </li>
+</ul>
+
+    <h1 itemscope itemtype="http://data-vocabulary.org/Breadcrumb" class="entry-title public ">
+  <svg aria-hidden="true" class="octicon octicon-repo" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M4 9h-1v-1h1v1z m0-3h-1v1h1v-1z m0-2h-1v1h1v-1z m0-2h-1v1h1v-1z m8-1v12c0 0.55-0.45 1-1 1H6v2l-1.5-1.5-1.5 1.5V14H1c-0.55 0-1-0.45-1-1V1C0 0.45 0.45 0 1 0h10c0.55 0 1 0.45 1 1z m-1 10H1v2h2v-1h3v1h5V11z m0-10H2v9h9V1z"></path></svg>
+  <span class="author"><a href="/wp-cli" class="url fn" itemprop="url" rel="author"><span itemprop="title">wp-cli</span></a></span><!--
+--><span class="path-divider">/</span><!--
+--><strong><a href="/wp-cli/wp-cli.github.com" data-pjax="#js-repo-pjax-container">wp-cli.github.com</a></strong>
+
+  <span class="page-context-loader">
+    <img alt="" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" />
+  </span>
+
+</h1>
+
+  </div>
+  <div class="container">
+    
+<nav class="reponav js-repo-nav js-sidenav-container-pjax js-octicon-loaders"
+     role="navigation"
+     data-pjax="#js-repo-pjax-container">
+
+  <a href="/wp-cli/wp-cli.github.com" aria-label="Code" aria-selected="true" class="js-selected-navigation-item selected reponav-item" data-hotkey="g c" data-selected-links="repo_source repo_downloads repo_commits repo_releases repo_tags repo_branches /wp-cli/wp-cli.github.com">
+    <svg aria-hidden="true" class="octicon octicon-code" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M9.5 3l-1.5 1.5 3.5 3.5L8 11.5l1.5 1.5 4.5-5L9.5 3zM4.5 3L0 8l4.5 5 1.5-1.5L2.5 8l3.5-3.5L4.5 3z"></path></svg>
+    Code
+</a>
+
+  <a href="/wp-cli/wp-cli.github.com/pulls" class="js-selected-navigation-item reponav-item" data-hotkey="g p" data-selected-links="repo_pulls /wp-cli/wp-cli.github.com/pulls">
+    <svg aria-hidden="true" class="octicon octicon-git-pull-request" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M11 11.28c0-1.73 0-6.28 0-6.28-0.03-0.78-0.34-1.47-0.94-2.06s-1.28-0.91-2.06-0.94c0 0-1.02 0-1 0V0L4 3l3 3V4h1c0.27 0.02 0.48 0.11 0.69 0.31s0.3 0.42 0.31 0.69v6.28c-0.59 0.34-1 0.98-1 1.72 0 1.11 0.89 2 2 2s2-0.89 2-2c0-0.73-0.41-1.38-1-1.72z m-1 2.92c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2zM4 3c0-1.11-0.89-2-2-2S0 1.89 0 3c0 0.73 0.41 1.38 1 1.72 0 1.55 0 5.56 0 6.56-0.59 0.34-1 0.98-1 1.72 0 1.11 0.89 2 2 2s2-0.89 2-2c0-0.73-0.41-1.38-1-1.72V4.72c0.59-0.34 1-0.98 1-1.72z m-0.8 10c0 0.66-0.55 1.2-1.2 1.2s-1.2-0.55-1.2-1.2 0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2z m-1.2-8.8c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z"></path></svg>
+    Pull requests
+    <span class="counter">3</span>
+</a>
+
+  <a href="/wp-cli/wp-cli.github.com/pulse" class="js-selected-navigation-item reponav-item" data-selected-links="pulse /wp-cli/wp-cli.github.com/pulse">
+    <svg aria-hidden="true" class="octicon octicon-pulse" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M11.5 8L8.8 5.4 6.6 8.5 5.5 1.6 2.38 8H0V10h3.6L4.5 8.2l0.9 5.4L9 8.5l1.6 1.5H14V8H11.5z"></path></svg>
+    Pulse
+</a>
+  <a href="/wp-cli/wp-cli.github.com/graphs" class="js-selected-navigation-item reponav-item" data-selected-links="repo_graphs repo_contributors /wp-cli/wp-cli.github.com/graphs">
+    <svg aria-hidden="true" class="octicon octicon-graph" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M16 14v1H0V0h1v14h15z m-11-1H3V8h2v5z m4 0H7V3h2v10z m4 0H11V6h2v7z"></path></svg>
+    Graphs
+</a>
+
+</nav>
+
+  </div>
+</div>
+
+<div class="container new-discussion-timeline experiment-repo-nav">
+  <div class="repository-content">
+
+    
+
+<a href="/wp-cli/wp-cli.github.com/blob/663ab6d46c67329aac052ee829cfced92c98a597/behat-data/codeispoetry.png" class="hidden js-permalink-shortcut" data-hotkey="y">Permalink</a>
+
+<!-- blob contrib key: blob_contributors:v21:03324a4d6ead46e4f13ad85cbdf84ead -->
+
+<div class="file-navigation js-zeroclipboard-container">
+  
+<div class="select-menu js-menu-container js-select-menu left">
+  <button class="btn btn-sm select-menu-button js-menu-target css-truncate" data-hotkey="w"
+    title="master"
+    type="button" aria-label="Switch branches or tags" tabindex="0" aria-haspopup="true">
+    <i>Branch:</i>
+    <span class="js-select-button css-truncate-target">master</span>
+  </button>
+
+  <div class="select-menu-modal-holder js-menu-content js-navigation-container" data-pjax aria-hidden="true">
+
+    <div class="select-menu-modal">
+      <div class="select-menu-header">
+        <svg aria-label="Close" class="octicon octicon-x js-menu-close" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z"></path></svg>
+        <span class="select-menu-title">Switch branches/tags</span>
+      </div>
+
+      <div class="select-menu-filters">
+        <div class="select-menu-text-filter">
+          <input type="text" aria-label="Filter branches/tags" id="context-commitish-filter-field" class="js-filterable-field js-navigation-enable" placeholder="Filter branches/tags">
+        </div>
+        <div class="select-menu-tabs">
+          <ul>
+            <li class="select-menu-tab">
+              <a href="#" data-tab-filter="branches" data-filter-placeholder="Filter branches/tags" class="js-select-menu-tab" role="tab">Branches</a>
+            </li>
+            <li class="select-menu-tab">
+              <a href="#" data-tab-filter="tags" data-filter-placeholder="Find a tag…" class="js-select-menu-tab" role="tab">Tags</a>
+            </li>
+          </ul>
+        </div>
+      </div>
+
+      <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="branches" role="menu">
+
+        <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
+
+
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/add-global-search/behat-data/codeispoetry.png"
+               data-name="add-global-search"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="add-global-search">
+                add-global-search
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/deb-package/behat-data/codeispoetry.png"
+               data-name="deb-package"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="deb-package">
+                deb-package
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/feature/rework-globals-section/behat-data/codeispoetry.png"
+               data-name="feature/rework-globals-section"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="feature/rework-globals-section">
+                feature/rework-globals-section
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/fix-html-command-flag-minus/behat-data/codeispoetry.png"
+               data-name="fix-html-command-flag-minus"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="fix-html-command-flag-minus">
+                fix-html-command-flag-minus
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/highlighting/behat-data/codeispoetry.png"
+               data-name="highlighting"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="highlighting">
+                highlighting
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open selected"
+               href="/wp-cli/wp-cli.github.com/blob/master/behat-data/codeispoetry.png"
+               data-name="master"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="master">
+                master
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/phpdoc/behat-data/codeispoetry.png"
+               data-name="phpdoc"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="phpdoc">
+                phpdoc
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/post-changes-hightlighting/behat-data/codeispoetry.png"
+               data-name="post-changes-hightlighting"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="post-changes-hightlighting">
+                post-changes-hightlighting
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/post-contributing/behat-data/codeispoetry.png"
+               data-name="post-contributing"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="post-contributing">
+                post-contributing
+              </span>
+            </a>
+            <a class="select-menu-item js-navigation-item js-navigation-open "
+               href="/wp-cli/wp-cli.github.com/blob/redesign-2014/behat-data/codeispoetry.png"
+               data-name="redesign-2014"
+               data-skip-pjax="true"
+               rel="nofollow">
+              <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+              <span class="select-menu-item-text css-truncate-target" title="redesign-2014">
+                redesign-2014
+              </span>
+            </a>
+        </div>
+
+          <div class="select-menu-no-results">Nothing to show</div>
+      </div>
+
+      <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="tags">
+        <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
+
+
+        </div>
+
+        <div class="select-menu-no-results">Nothing to show</div>
+      </div>
+
+    </div>
+  </div>
+</div>
+
+  <div class="btn-group right">
+    <a href="/wp-cli/wp-cli.github.com/find/master"
+          class="js-show-file-finder btn btn-sm"
+          data-pjax
+          data-hotkey="t">
+      Find file
+    </a>
+    <button aria-label="Copy file path to clipboard" class="js-zeroclipboard btn btn-sm zeroclipboard-button tooltipped tooltipped-s" data-copied-hint="Copied!" type="button">Copy path</button>
+  </div>
+  <div class="breadcrumb js-zeroclipboard-target">
+    <span class="repo-root js-repo-root"><span itemscope="" itemtype="http://data-vocabulary.org/Breadcrumb"><a href="/wp-cli/wp-cli.github.com" class="" data-branch="master" data-pjax="true" itemscope="url"><span itemprop="title">wp-cli.github.com</span></a></span></span><span class="separator">/</span><span itemscope="" itemtype="http://data-vocabulary.org/Breadcrumb"><a href="/wp-cli/wp-cli.github.com/tree/master/behat-data" class="" data-branch="master" data-pjax="true" itemscope="url"><span itemprop="title">behat-data</span></a></span><span class="separator">/</span><strong class="final-path">codeispoetry.png</strong>
+  </div>
+</div>
+
+
+  <div class="commit-tease">
+      <span class="right">
+        <a class="commit-tease-sha" href="/wp-cli/wp-cli.github.com/commit/9a03d83f0b0cfe167a8927cf199f4113cb5bd7f3" data-pjax>
+          9a03d83
+        </a>
+        <time datetime="2016-02-03T13:13:36Z" is="relative-time">Feb 3, 2016</time>
+      </span>
+      <div>
+        <img alt="@danielbachhuber" class="avatar" height="20" src="https://avatars3.githubusercontent.com/u/36432?v=3&amp;s=40" width="20" />
+        <a href="/danielbachhuber" class="user-mention" rel="contributor">danielbachhuber</a>
+          <a href="/wp-cli/wp-cli.github.com/commit/9a03d83f0b0cfe167a8927cf199f4113cb5bd7f3" class="message" data-pjax="true" title="Add IPTC data to the test image">Add IPTC data to the test image</a>
+      </div>
+
+    <div class="commit-tease-contributors">
+      <a class="muted-link contributors-toggle" href="#blob_contributors_box" rel="facebox">
+        <strong>1</strong>
+         contributor
+      </a>
+      
+    </div>
+
+    <div id="blob_contributors_box" style="display:none">
+      <h2 class="facebox-header" data-facebox-id="facebox-header">Users who have contributed to this file</h2>
+      <ul class="facebox-user-list" data-facebox-id="facebox-description">
+          <li class="facebox-user-list-item">
+            <img alt="@danielbachhuber" height="24" src="https://avatars1.githubusercontent.com/u/36432?v=3&amp;s=48" width="24" />
+            <a href="/danielbachhuber">danielbachhuber</a>
+          </li>
+      </ul>
+    </div>
+  </div>
+
+<div class="file">
+  <div class="file-header">
+  <div class="file-actions">
+
+    <div class="btn-group">
+      <a href="/wp-cli/wp-cli.github.com/raw/master/behat-data/codeispoetry.png" class="btn btn-sm " id="raw-url">Raw</a>
+      <a href="/wp-cli/wp-cli.github.com/commits/master/behat-data/codeispoetry.png" class="btn btn-sm " rel="nofollow">History</a>
+    </div>
+
+
+        <!-- </textarea> --><!-- '"` --><form accept-charset="UTF-8" action="/wp-cli/wp-cli.github.com/delete/master/behat-data/codeispoetry.png" class="inline-form" data-form-nonce="9ca3979ba13c29867416f58457ff40e521a92049" method="post"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /><input name="authenticity_token" type="hidden" value="S96qX2oUX+Gcc45YxV/LuTaZytjKoCSdzjnCuQKuAvtK9j0OCzZK7/lSmLCAbyF+r6NfG1UoWuu7rPzR9+ax+g==" /></div>
+          <button class="btn-octicon btn-octicon-danger tooltipped tooltipped-nw" type="submit"
+            aria-label="You must be signed in to make or propose changes" data-disable-with>
+            <svg aria-hidden="true" class="octicon octicon-trashcan" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M10 2H8c0-0.55-0.45-1-1-1H4c-0.55 0-1 0.45-1 1H1c-0.55 0-1 0.45-1 1v1c0 0.55 0.45 1 1 1v9c0 0.55 0.45 1 1 1h7c0.55 0 1-0.45 1-1V5c0.55 0 1-0.45 1-1v-1c0-0.55-0.45-1-1-1z m-1 12H2V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9z m1-10H1v-1h9v1z"></path></svg>
+          </button>
+</form>  </div>
+
+  <div class="file-info">
+    15.6 KB
+  </div>
+</div>
+
+  
+
+  <div class="blob-wrapper data type-text">
+      <div class="image">
+          <span class="border-wrap"><img src="/wp-cli/wp-cli.github.com/blob/master/behat-data/codeispoetry.png?raw=true" alt="codeispoetry.png"></span>
+      </div>
+  </div>
+
+</div>
+
+<a href="#jump-to-line" rel="facebox[.linejump]" data-hotkey="l" style="display:none">Jump to Line</a>
+<div id="jump-to-line" style="display:none">
+  <!-- </textarea> --><!-- '"` --><form accept-charset="UTF-8" action="" class="js-jump-to-line-form" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
+    <input class="linejump-input js-jump-to-line-field" type="text" placeholder="Jump to line&hellip;" aria-label="Jump to line" autofocus>
+    <button type="submit" class="btn">Go</button>
+</form></div>
+
+  </div>
+  <div class="modal-backdrop"></div>
+</div>
+
+
+    </div>
+  </div>
+
+    </div>
+
+        <div class="container">
+  <div class="site-footer" role="contentinfo">
+    <ul class="site-footer-links right">
+        <li><a href="https://status.github.com/" data-ga-click="Footer, go to status, text:status">Status</a></li>
+      <li><a href="https://developer.github.com" data-ga-click="Footer, go to api, text:api">API</a></li>
+      <li><a href="https://training.github.com" data-ga-click="Footer, go to training, text:training">Training</a></li>
+      <li><a href="https://shop.github.com" data-ga-click="Footer, go to shop, text:shop">Shop</a></li>
+        <li><a href="https://github.com/blog" data-ga-click="Footer, go to blog, text:blog">Blog</a></li>
+        <li><a href="https://github.com/about" data-ga-click="Footer, go to about, text:about">About</a></li>
+        <li><a href="https://github.com/pricing" data-ga-click="Footer, go to pricing, text:pricing">Pricing</a></li>
+
+    </ul>
+
+    <a href="https://github.com" aria-label="Homepage">
+      <svg aria-hidden="true" class="octicon octicon-mark-github" height="24" role="img" title="GitHub " version="1.1" viewBox="0 0 16 16" width="24"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 0.4 0.07 0.55-0.17 0.55-0.38 0-0.19-0.01-0.82-0.01-1.49-2.01 0.37-2.53-0.49-2.69-0.94-0.09-0.23-0.48-0.94-0.82-1.13-0.28-0.15-0.68-0.52-0.01-0.53 0.63-0.01 1.08 0.58 1.23 0.82 0.72 1.21 1.87 0.87 2.33 0.66 0.07-0.52 0.28-0.87 0.51-1.07-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.59 0.82-2.15-0.08-0.2-0.36-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82 0.64-0.18 1.32-0.27 2-0.27 0.68 0 1.36 0.09 2 0.27 1.53-1.04 2.2-0.82 2.2-0.82 0.44 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.82 1.27 0.82 2.15 0 3.07-1.87 3.75-3.65 3.95 0.29 0.25 0.54 0.73 0.54 1.48 0 1.07-0.01 1.93-0.01 2.2 0 0.21 0.15 0.46 0.55 0.38C13.71 14.53 16 11.53 16 8 16 3.58 12.42 0 8 0z"></path></svg>
+</a>
+    <ul class="site-footer-links">
+      <li>&copy; 2016 <span title="0.03841s from github-fe118-cp1-prd.iad.github.net">GitHub</span>, Inc.</li>
+        <li><a href="https://github.com/site/terms" data-ga-click="Footer, go to terms, text:terms">Terms</a></li>
+        <li><a href="https://github.com/site/privacy" data-ga-click="Footer, go to privacy, text:privacy">Privacy</a></li>
+        <li><a href="https://github.com/security" data-ga-click="Footer, go to security, text:security">Security</a></li>
+        <li><a href="https://github.com/contact" data-ga-click="Footer, go to contact, text:contact">Contact</a></li>
+        <li><a href="https://help.github.com" data-ga-click="Footer, go to help, text:help">Help</a></li>
+    </ul>
+  </div>
+</div>
+
+
+
+    
+    
+    
+
+    <div id="ajax-error-message" class="flash flash-error">
+      <svg aria-hidden="true" class="octicon octicon-alert" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M15.72 12.5l-6.85-11.98C8.69 0.21 8.36 0.02 8 0.02s-0.69 0.19-0.87 0.5l-6.85 11.98c-0.18 0.31-0.18 0.69 0 1C0.47 13.81 0.8 14 1.15 14h13.7c0.36 0 0.69-0.19 0.86-0.5S15.89 12.81 15.72 12.5zM9 12H7V10h2V12zM9 9H7V5h2V9z"></path></svg>
+      <button type="button" class="flash-close js-flash-close js-ajax-error-dismiss" aria-label="Dismiss error">
+        <svg aria-hidden="true" class="octicon octicon-x" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z"></path></svg>
+      </button>
+      Something went wrong with that request. Please try again.
+    </div>
+
+
+      <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/compat-ef12e71982f00805539829712436829b02370b2e561de18ece6796130cbb9bbe.js"></script>
+      <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/frameworks-ee521b8e9facac68ff27e93fc3ae0f8ed811d7bf9e434e84f4b9ea227780b084.js"></script>
+      <script async="async" crossorigin="anonymous" src="https://assets-cdn.github.com/assets/github-863d0e4c2905010278cfd87e9c7c738e812530db73c36c274a185354977a2e41.js"></script>
+      
+      
+      
+    <div class="js-stale-session-flash stale-session-flash flash flash-warn flash-banner hidden">
+      <svg aria-hidden="true" class="octicon octicon-alert" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M15.72 12.5l-6.85-11.98C8.69 0.21 8.36 0.02 8 0.02s-0.69 0.19-0.87 0.5l-6.85 11.98c-0.18 0.31-0.18 0.69 0 1C0.47 13.81 0.8 14 1.15 14h13.7c0.36 0 0.69-0.19 0.86-0.5S15.89 12.81 15.72 12.5zM9 12H7V10h2V12zM9 9H7V5h2V9z"></path></svg>
+      <span class="signed-in-tab-flash">You signed in with another tab or window. <a href="">Reload</a> to refresh your session.</span>
+      <span class="signed-out-tab-flash">You signed out in another tab or window. <a href="">Reload</a> to refresh your session.</span>
+    </div>
+    <div class="facebox" id="facebox" style="display:none;">
+  <div class="facebox-popup">
+    <div class="facebox-content" role="dialog" aria-labelledby="facebox-header" aria-describedby="facebox-description">
+    </div>
+    <button type="button" class="facebox-close js-facebox-close" aria-label="Close modal">
+      <svg aria-hidden="true" class="octicon octicon-x" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z"></path></svg>
+    </button>
+  </div>
+</div>
+
+  </body>
+</html>
+
</ins></span></pre></div>
<a id="trunktestsphpunitincludesbootstrapphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/includes/bootstrap.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/bootstrap.php        2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/tests/phpunit/includes/bootstrap.php  2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -40,6 +40,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> define( 'WP_MEMORY_LIMIT', -1 );
</span><span class="cx" style="display: block; padding: 0 10px"> define( 'WP_MAX_MEMORY_LIMIT', -1 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+define( 'REST_TESTS_IMPOSSIBLY_HIGH_NUMBER', 99999999 );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // Should we run in multisite mode?
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -88,6 +90,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> require dirname( __FILE__ ) . '/testcase.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require dirname( __FILE__ ) . '/testcase-rest-api.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require dirname( __FILE__ ) . '/testcase-rest-controller.php';
+require dirname( __FILE__ ) . '/testcase-rest-post-type-controller.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require dirname( __FILE__ ) . '/testcase-xmlrpc.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require dirname( __FILE__ ) . '/testcase-ajax.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require dirname( __FILE__ ) . '/testcase-canonical.php';
</span></span></pre></div>
<a id="trunktestsphpunitincludestestcaserestcontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/includes/testcase-rest-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/testcase-rest-controller.php                         (rev 0)
+++ trunk/tests/phpunit/includes/testcase-rest-controller.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,48 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+abstract class WP_Test_REST_Controller_Testcase extends WP_Test_REST_TestCase {
+
+       protected $server;
+
+       public function setUp() {
+               parent::setUp();
+               add_filter( 'rest_url', array( $this, 'filter_rest_url_for_leading_slash' ), 10, 2 );
+               /** @var WP_REST_Server $wp_rest_server */
+               global $wp_rest_server;
+               $this->server = $wp_rest_server = new Spy_REST_Server;
+               do_action( 'rest_api_init' );
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+               remove_filter( 'rest_url', array( $this, 'test_rest_url_for_leading_slash' ), 10, 2 );
+               /** @var WP_REST_Server $wp_rest_server */
+               global $wp_rest_server;
+               $wp_rest_server = null;
+       }
+
+       abstract public function test_register_routes();
+
+       abstract public function test_context_param();
+
+       abstract public function test_get_items();
+
+       abstract public function test_get_item();
+
+       abstract public function test_create_item();
+
+       abstract public function test_update_item();
+
+       abstract public function test_delete_item();
+
+       abstract public function test_prepare_item();
+
+       abstract public function test_get_item_schema();
+
+       public function filter_rest_url_for_leading_slash( $url, $path ) {
+               // Make sure path for rest_url has a leading slash for proper resolution.
+               $this->assertTrue( 0 === strpos( $path, '/' ) );
+
+               return $url;
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunitincludestestcaserestposttypecontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/includes/testcase-rest-post-type-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/testcase-rest-post-type-controller.php                               (rev 0)
+++ trunk/tests/phpunit/includes/testcase-rest-post-type-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,312 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+abstract class WP_Test_REST_Post_Type_Controller_Testcase extends WP_Test_REST_Controller_Testcase {
+
+       protected function check_post_data( $post, $data, $context, $links ) {
+               $post_type_obj = get_post_type_object( $post->post_type );
+
+               // Standard fields
+               $this->assertEquals( $post->ID, $data['id'] );
+               $this->assertEquals( $post->post_name, $data['slug'] );
+               $this->assertEquals( get_permalink( $post->ID ), $data['link'] );
+               if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
+                       $this->assertNull( $data['date_gmt'] );
+               }
+               $this->assertEquals( mysql_to_rfc3339( $post->post_date ), $data['date'] );
+
+               if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
+                       $this->assertNull( $data['modified_gmt'] );
+               }
+               $this->assertEquals( mysql_to_rfc3339( $post->post_modified ), $data['modified'] );
+
+               // author
+               if ( post_type_supports( $post->post_type, 'author' ) ) {
+                       $this->assertEquals( $post->post_author, $data['author'] );
+               } else {
+                       $this->assertEmpty( $data['author'] );
+               }
+
+               // post_parent
+               if ( $post_type_obj->hierarchical ) {
+                       $this->assertArrayHasKey( 'parent', $data );
+                       if ( $post->post_parent ) {
+                               if ( is_int( $data['parent'] ) ) {
+                                       $this->assertEquals( $post->post_parent, $data['parent'] );
+                               } else {
+                                       $this->assertEquals( $post->post_parent, $data['parent']['id'] );
+                                       $this->check_get_post_response( $data['parent'], get_post( $data['parent']['id'] ), 'view-parent' );
+                               }
+                       } else {
+                               $this->assertEmpty( $data['parent'] );
+                       }
+               } else {
+                       $this->assertFalse( isset( $data['parent'] ) );
+               }
+
+               // page attributes
+               if ( $post_type_obj->hierarchical && post_type_supports( $post->post_type, 'page-attributes' ) ) {
+                       $this->assertEquals( $post->menu_order, $data['menu_order'] );
+               } else {
+                       $this->assertFalse( isset( $data['menu_order'] ) );
+               }
+
+               // Comments
+               if ( post_type_supports( $post->post_type, 'comments' ) ) {
+                       $this->assertEquals( $post->comment_status, $data['comment_status'] );
+                       $this->assertEquals( $post->ping_status, $data['ping_status'] );
+               } else {
+                       $this->assertFalse( isset( $data['comment_status'] ) );
+                       $this->assertFalse( isset( $data['ping_status'] ) );
+               }
+
+               if ( 'post' === $post->post_type ) {
+                       $this->assertEquals( is_sticky( $post->ID ), $data['sticky'] );
+               }
+
+               if ( 'post' === $post->post_type && 'edit' === $context ) {
+                       $this->assertEquals( $post->post_password, $data['password'] );
+               }
+
+               if ( 'page' === $post->post_type ) {
+                       $this->assertEquals( get_page_template_slug( $post->ID ), $data['template'] );
+               }
+
+               if ( post_type_supports( $post->post_type, 'thumbnail' ) ) {
+                       $this->assertEquals( (int) get_post_thumbnail_id( $post->ID ), $data['featured_media'] );
+               } else {
+                       $this->assertFalse( isset( $data['featured_media'] ) );
+               }
+
+               // Check post format.
+               if ( post_type_supports( $post->post_type, 'post-formats' ) ) {
+                       $post_format = get_post_format( $post->ID );
+                       if ( empty( $post_format ) ) {
+                               $this->assertEquals( 'standard', $data['format'] );
+                       } else {
+                               $this->assertEquals( get_post_format( $post->ID ), $data['format'] );
+                       }
+               } else {
+                       $this->assertFalse( isset( $data['format'] ) );
+               }
+
+               // Check filtered values.
+               if ( post_type_supports( $post->post_type, 'title' ) ) {
+                       add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+                       $this->assertEquals( get_the_title( $post->ID ), $data['title']['rendered'] );
+                       remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+                       if ( 'edit' === $context ) {
+                               $this->assertEquals( $post->post_title, $data['title']['raw'] );
+                       } else {
+                               $this->assertFalse( isset( $data['title']['raw'] ) );
+                       }
+               } else {
+                       $this->assertFalse( isset( $data['title'] ) );
+               }
+
+               if ( post_type_supports( $post->post_type, 'editor' ) ) {
+                       // TODO: apply content filter for more accurate testing.
+                       if ( ! $post->post_password ) {
+                               $this->assertEquals( wpautop( $post->post_content ), $data['content']['rendered'] );
+                       }
+
+                       if ( 'edit' === $context ) {
+                               $this->assertEquals( $post->post_content, $data['content']['raw'] );
+                       } else {
+                               $this->assertFalse( isset( $data['content']['raw'] ) );
+                       }
+               } else {
+                       $this->assertFalse( isset( $data['content'] ) );
+               }
+
+               if ( post_type_supports( $post->post_type, 'excerpt' ) ) {
+                       if ( empty( $post->post_password ) ) {
+                               // TODO: apply excerpt filter for more accurate testing.
+                               $this->assertEquals( wpautop( $post->post_excerpt ), $data['excerpt']['rendered'] );
+                       } else {
+                               // TODO: better testing for excerpts for password protected posts.
+                       }
+                       if ( 'edit' === $context ) {
+                               $this->assertEquals( $post->post_excerpt, $data['excerpt']['raw'] );
+                       } else {
+                               $this->assertFalse( isset( $data['excerpt']['raw'] ) );
+                       }
+               } else {
+                       $this->assertFalse( isset( $data['excerpt'] ) );
+               }
+
+               $this->assertEquals( $post->guid, $data['guid']['rendered'] );
+
+               if ( 'edit' === $context ) {
+                       $this->assertEquals( $post->guid, $data['guid']['raw'] );
+                       $this->assertEquals( $post->post_status, $data['status'] );
+
+                       if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
+                               $this->assertNull( $data['date_gmt'] );
+                       } else {
+                               $this->assertEquals( mysql_to_rfc3339( $post->post_date_gmt ), $data['date_gmt'] );
+                       }
+
+                       if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
+                               $this->assertNull( $data['modified_gmt'] );
+                       } else {
+                               $this->assertEquals( mysql_to_rfc3339( $post->post_modified_gmt ), $data['modified_gmt'] );
+                       }
+               }
+
+               $taxonomies = wp_list_filter( get_object_taxonomies( $post->post_type, 'objects' ), array( 'show_in_rest' => true ) );
+               foreach ( $taxonomies as $taxonomy ) {
+                       $this->assertTrue( isset( $data[ $taxonomy->rest_base ] ) );
+                       $terms = wp_get_object_terms( $post->ID, $taxonomy->name, array( 'fields' => 'ids' ) );
+                       sort( $terms );
+                       sort( $data[ $taxonomy->rest_base ] );
+                       $this->assertEquals( $terms, $data[ $taxonomy->rest_base ] );
+               }
+
+               // test links
+               if ( $links ) {
+
+                       $links = test_rest_expand_compact_links( $links );
+                       $post_type = get_post_type_object( $data['type'] );
+                       $this->assertEquals( $links['self'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base . '/' . $data['id'] ) );
+                       $this->assertEquals( $links['collection'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base ) );
+                       $this->assertEquals( $links['about'][0]['href'], rest_url( 'wp/v2/types/' . $data['type'] ) );
+
+                       if ( post_type_supports( $post->post_type, 'author' ) && $data['author'] ) {
+                               $this->assertEquals( $links['author'][0]['href'], rest_url( 'wp/v2/users/' . $data['author'] ) );
+                       }
+
+                       if ( post_type_supports( $post->post_type, 'comments' ) ) {
+                               $this->assertEquals( $links['replies'][0]['href'], add_query_arg( 'post', $data['id'], rest_url( 'wp/v2/comments' ) ) );
+                       }
+
+                       if ( post_type_supports( $post->post_type, 'revisions' ) ) {
+                               $this->assertEquals( $links['version-history'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base . '/' . $data['id'] . '/revisions' ) );
+                       }
+
+                       if ( $post_type->hierarchical && ! empty( $data['parent'] ) ) {
+                               $this->assertEquals( $links['up'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base . '/' . $data['parent'] ) );
+                       }
+
+                       if ( ! in_array( $data['type'], array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
+                               $this->assertEquals( $links['https://api.w.org/attachment'][0]['href'], add_query_arg( 'parent', $data['id'], rest_url( 'wp/v2/media' ) ) );
+                       }
+
+                       if ( ! empty( $data['featured_media'] ) ) {
+                               $this->assertEquals( $links['https://api.w.org/featuredmedia'][0]['href'], rest_url( 'wp/v2/media/' . $data['featured_media'] ) );
+                       }
+
+                       $num = 0;
+                       foreach ( $taxonomies as $key => $taxonomy ) {
+                               $this->assertEquals( $taxonomy->name, $links['https://api.w.org/term'][ $num ]['attributes']['taxonomy'] );
+                               $this->assertEquals( add_query_arg( 'post', $data['id'], rest_url( 'wp/v2/' . $taxonomy->rest_base ) ), $links['https://api.w.org/term'][ $num ]['href'] );
+                               $num++;
+                       }
+               }
+
+       }
+
+       protected function check_get_posts_response( $response, $context = 'view' ) {
+               $this->assertNotInstanceOf( 'WP_Error', $response );
+               $response = rest_ensure_response( $response );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $headers = $response->get_headers();
+               $this->assertArrayHasKey( 'X-WP-Total', $headers );
+               $this->assertArrayHasKey( 'X-WP-TotalPages', $headers );
+
+               $all_data = $response->get_data();
+               foreach ( $all_data as $data ) {
+                       $post = get_post( $data['id'] );
+                       // as the links for the post are "response_links" format in the data array we have to pull them
+                       // out and parse them.
+                       $links = $data['_links'];
+                       foreach ( $links as &$links_array ) {
+                               foreach ( $links_array as &$link ) {
+                                       $attributes = array_diff_key( $link, array( 'href' => 1, 'name' => 1 ) );
+                                       $link = array_diff_key( $link, $attributes );
+                                       $link['attributes'] = $attributes;
+                               }
+                       }
+
+                       $this->check_post_data( $post, $data, $context, $links );
+               }
+       }
+
+       protected function check_get_post_response( $response, $context = 'view' ) {
+               $this->assertNotInstanceOf( 'WP_Error', $response );
+               $response = rest_ensure_response( $response );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $post = get_post( $data['id'] );
+               $this->check_post_data( $post, $data, $context, $response->get_links() );
+
+       }
+
+       protected function check_create_post_response( $response ) {
+               $this->assertNotInstanceOf( 'WP_Error', $response );
+               $response = rest_ensure_response( $response );
+
+               $this->assertEquals( 201, $response->get_status() );
+               $headers = $response->get_headers();
+               $this->assertArrayHasKey( 'Location', $headers );
+
+               $data = $response->get_data();
+               $post = get_post( $data['id'] );
+               $this->check_post_data( $post, $data, 'edit', $response->get_links() );
+       }
+
+       protected function check_update_post_response( $response ) {
+               $this->assertNotInstanceOf( 'WP_Error', $response );
+               $response = rest_ensure_response( $response );
+
+               $this->assertEquals( 200, $response->get_status() );
+               $headers = $response->get_headers();
+               $this->assertArrayNotHasKey( 'Location', $headers );
+
+               $data = $response->get_data();
+               $post = get_post( $data['id'] );
+               $this->check_post_data( $post, $data, 'edit', $response->get_links() );
+       }
+
+       protected function set_post_data( $args = array() ) {
+               $defaults = array(
+                       'title'   => rand_str(),
+                       'content' => rand_str(),
+                       'excerpt' => rand_str(),
+                       'name'    => 'test',
+                       'status'  => 'publish',
+                       'author'  => get_current_user_id(),
+                       'type'    => 'post',
+               );
+
+               return wp_parse_args( $args, $defaults );
+       }
+
+       protected function set_raw_post_data( $args = array() ) {
+               return wp_parse_args( $args, $this->set_post_data( array(
+                       'title'   => array(
+                               'raw' => rand_str(),
+                       ),
+                       'content' => array(
+                               'raw' => rand_str(),
+                       ),
+                       'excerpt' => array(
+                               'raw' => rand_str(),
+                       ),
+               ) ) );
+       }
+
+       /**
+        * Overwrite the default protected title format.
+        *
+        * By default WordPress will show password protected posts with a title of
+        * "Protected: %s", as the REST API communicates the protected status of a post
+        * in a machine readable format, we remove the "Protected: " prefix.
+        *
+        * @return string
+        */
+       public function protected_title_format() {
+               return '%s';
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunitincludesutilsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/includes/utils.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/utils.php    2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/tests/phpunit/includes/utils.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -453,3 +453,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return $i;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+function test_rest_expand_compact_links( $links ) {
+       if ( empty( $links['curies'] ) ) {
+               return $links;
+       }
+       foreach ( $links as $rel => $links_array ) {
+               if ( ! strpos( $rel, ':' ) ) {
+                       continue;
+               }
+
+               $name = explode( ':', $rel );
+
+               $curie = wp_list_filter( $links['curies'], array( 'name' => $name[0] ) );
+               $full_uri = str_replace( '{rel}', $name[1], $curie[0]['href'] );
+               $links[ $full_uri ] = $links_array;
+               unset( $links[ $rel ] );
+       }
+       return $links;
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestattachmentscontrollerphp"></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-attachments-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php                                (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php  2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,862 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Attachments_Controller functionality
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->editor_id = $this->factory->user->create( array(
+                       'role' => 'editor',
+               ) );
+               $this->author_id = $this->factory->user->create( array(
+                       'role' => 'author',
+               ) );
+               $this->contributor_id = $this->factory->user->create( array(
+                       'role' => 'contributor',
+               ) );
+
+               // Add an uploader role to test upload capabilities.
+               add_role( 'uploader', 'File upload role' );
+               $role = get_role( 'uploader' );
+               $role->add_cap( 'upload_files' );
+               $role->add_cap( 'read' );
+               $role->add_cap( 'level_0' );
+               $this->uploader_id = $this->factory->user->create( array(
+                       'role' => 'uploader',
+               ) );
+
+               $orig_file = DIR_TESTDATA . '/images/canola.jpg';
+               $this->test_file = '/tmp/canola.jpg';
+               copy( $orig_file, $this->test_file );
+               $orig_file2 = DIR_TESTDATA . '/images/codeispoetry.png';
+               $this->test_file2 = '/tmp/codeispoetry.png';
+               copy( $orig_file2, $this->test_file2 );
+
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/media', $routes );
+               $this->assertCount( 2, $routes['/wp/v2/media'] );
+               $this->assertArrayHasKey( '/wp/v2/media/(?P<id>[\d]+)', $routes );
+               $this->assertCount( 3, $routes['/wp/v2/media/(?P<id>[\d]+)'] );
+       }
+
+       public static function disposition_provider() {
+               return array(
+                       // Types
+                       array( 'attachment; filename="foo.jpg"', 'foo.jpg' ),
+                       array( 'inline; filename="foo.jpg"', 'foo.jpg' ),
+                       array( 'form-data; filename="foo.jpg"', 'foo.jpg' ),
+
+                       // Formatting
+                       array( 'attachment; filename="foo.jpg"', 'foo.jpg' ),
+                       array( 'attachment; filename=foo.jpg', 'foo.jpg' ),
+                       array( 'attachment;filename="foo.jpg"', 'foo.jpg' ),
+                       array( 'attachment;filename=foo.jpg', 'foo.jpg' ),
+                       array( 'attachment; filename = "foo.jpg"', 'foo.jpg' ),
+                       array( 'attachment; filename = foo.jpg', 'foo.jpg' ),
+                       array( "attachment;\tfilename\t=\t\"foo.jpg\"", 'foo.jpg' ),
+                       array( "attachment;\tfilename\t=\tfoo.jpg", 'foo.jpg' ),
+                       array( 'attachment; filename = my foo picture.jpg', 'my foo picture.jpg' ),
+
+                       // Extensions
+                       array( 'form-data; name="myfile"; filename="foo.jpg"', 'foo.jpg' ),
+                       array( 'form-data; name="myfile"; filename="foo.jpg"; something="else"', 'foo.jpg' ),
+                       array( 'form-data; name=myfile; filename=foo.jpg; something=else', 'foo.jpg' ),
+                       array( 'form-data; name=myfile; filename=my foo.jpg; something=else', 'my foo.jpg' ),
+
+                       // Invalid
+                       array( 'filename="foo.jpg"', null ),
+                       array( 'filename-foo.jpg', null ),
+                       array( 'foo.jpg', null ),
+                       array( 'unknown; notfilename="foo.jpg"', null ),
+               );
+       }
+
+       /**
+        * @dataProvider disposition_provider
+        */
+       public function test_parse_disposition( $header, $expected ) {
+               $header_list = array( $header );
+               $parsed = WP_REST_Attachments_Controller::get_filename_from_disposition( $header_list );
+               $this->assertEquals( $expected, $parsed );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media/' . $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'after',
+                       'author',
+                       'author_exclude',
+                       'before',
+                       'context',
+                       'exclude',
+                       'filter',
+                       'include',
+                       'media_type',
+                       'mime_type',
+                       'offset',
+                       'order',
+                       'orderby',
+                       'page',
+                       'parent',
+                       'parent_exclude',
+                       'per_page',
+                       'search',
+                       'slug',
+                       'status',
+                       ), $keys );
+               $media_types = array(
+                       'application',
+                       'video',
+                       'image',
+                       'audio',
+               );
+               if ( ! is_multisite() ) {
+                       $media_types[] = 'text';
+               }
+               $this->assertEqualSets( $media_types, $data['endpoints'][0]['args']['media_type']['enum'] );
+       }
+
+       public function test_get_items() {
+               wp_set_current_user( 0 );
+               $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) );
+               $id2 = $this->factory->attachment->create_object( $this->test_file, $draft_post, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $published_post = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->attachment->create_object( $this->test_file, $published_post, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 2, $data );
+               $ids = wp_list_pluck( $data, 'id' );
+               $this->assertTrue( in_array( $id1, $ids, true ) );
+               $this->assertFalse( in_array( $id2, $ids, true ) );
+               $this->assertTrue( in_array( $id3, $ids, true ) );
+
+               $this->check_get_posts_response( $response );
+       }
+
+       public function test_get_items_logged_in_editor() {
+               wp_set_current_user( $this->editor_id );
+               $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) );
+               $id2 = $this->factory->attachment->create_object( $this->test_file, $draft_post, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $published_post = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->attachment->create_object( $this->test_file, $published_post, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertCount( 3, $data );
+               $ids = wp_list_pluck( $data, 'id' );
+               $this->assertTrue( in_array( $id1, $ids, true ) );
+               $this->assertTrue( in_array( $id2, $ids, true ) );
+               $this->assertTrue( in_array( $id3, $ids, true ) );
+       }
+
+       public function test_get_items_media_type() {
+               $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $id1, $data[0]['id'] );
+               // media_type=video
+               $request->set_param( 'media_type', 'video' );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 0, $response->get_data() );
+               // media_type=image
+               $request->set_param( 'media_type', 'image' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $id1, $data[0]['id'] );
+       }
+
+       public function test_get_items_mime_type() {
+               $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $id1, $data[0]['id'] );
+               // mime_type=image/png
+               $request->set_param( 'mime_type', 'image/png' );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 0, $response->get_data() );
+               // mime_type=image/jpeg
+               $request->set_param( 'mime_type', 'image/jpeg' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $id1, $data[0]['id'] );
+       }
+
+       public function test_get_items_parent() {
+               $post_id = $this->factory->post->create( array( 'post_title' => 'Test Post' ) );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, $post_id, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $attachment_id2 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               // all attachments
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 2, count( $response->get_data() ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               // attachments without a parent
+               $request->set_param( 'parent', 0 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $attachment_id2, $data[0]['id'] );
+               // attachments with parent=post_id
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $request->set_param( 'parent', $post_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $attachment_id, $data[0]['id'] );
+               // attachments with invalid parent
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 0, count( $data ) );
+       }
+
+       public function test_get_items_invalid_status_param_is_discarded() {
+               wp_set_current_user( $this->editor_id );
+               $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $request->set_param( 'status', 'publish' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( 'inherit', $data[0]['status'] );
+       }
+
+       public function test_get_items_private_status() {
+               // Logged out users can't make the request
+               wp_set_current_user( 0 );
+               $attachment_id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_status'    => 'private',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $request->set_param( 'status', 'private' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+               // Properly authorized users can make the request
+               wp_set_current_user( $this->editor_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( $attachment_id1, $data[0]['id'] );
+       }
+
+       public function test_get_items_invalid_date() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $request->set_param( 'after', rand_str() );
+               $request->set_param( 'before', rand_str() );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_items_valid_date() {
+               $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_date'      => '2016-01-15T00:00:00Z',
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $id2 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_date'      => '2016-01-16T00:00:00Z',
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $id3 = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_date'      => '2016-01-17T00:00:00Z',
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
+               $request->set_param( 'after', '2016-01-15T00:00:00Z' );
+               $request->set_param( 'before', '2016-01-17T00:00:00Z' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $id2, $data[0]['id'] );
+       }
+
+       public function test_get_item() {
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               update_post_meta( $attachment_id, '_wp_attachment_image_alt', 'Sample alt text' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $this->check_get_post_response( $response );
+               $data = $response->get_data();
+               $this->assertEquals( 'image/jpeg', $data['mime_type'] );
+       }
+
+       public function test_get_item_sizes() {
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ), $this->test_file );
+
+               add_image_size( 'rest-api-test', 119, 119, true );
+               wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $this->test_file ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $image_src = wp_get_attachment_image_src( $attachment_id, 'rest-api-test' );
+               $original_image_src = wp_get_attachment_image_src( $attachment_id, 'full' );
+               remove_image_size( 'rest-api-test' );
+
+               $this->assertEquals( $image_src[0], $data['media_details']['sizes']['rest-api-test']['source_url'] );
+               $this->assertEquals( 'image/jpeg', $data['media_details']['sizes']['rest-api-test']['mime_type'] );
+               $this->assertEquals( $original_image_src[0], $data['media_details']['sizes']['full']['source_url'] );
+               $this->assertEquals( 'image/jpeg', $data['media_details']['sizes']['full']['mime_type'] );
+       }
+
+       public function test_get_item_sizes_with_no_url() {
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ), $this->test_file );
+
+               add_image_size( 'rest-api-test', 119, 119, true );
+               wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $this->test_file ) );
+
+               add_filter( 'wp_get_attachment_image_src', '__return_false' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               remove_filter( 'wp_get_attachment_image_src', '__return_false' );
+               remove_image_size( 'rest-api-test' );
+
+               $this->assertFalse( isset( $data['media_details']['sizes']['rest-api-test']['source_url'] ) );
+       }
+
+       public function test_get_item_private_post() {
+               wp_set_current_user( 0 );
+               $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) );
+               $id1 = $this->factory->attachment->create_object( $this->test_file, $draft_post, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 403, $response->get_status() );
+       }
+
+       public function test_create_item() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_header( 'Content-Type', 'image/jpeg' );
+               $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 201, $response->get_status() );
+               $this->assertEquals( 'image', $data['media_type'] );
+               $this->assertEquals( 'A field of amazing canola', $data['title']['rendered'] );
+               $this->assertEquals( 'The description for the image', $data['caption'] );
+       }
+
+       public function test_create_item_default_filename_title() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_file_params( array(
+                       'file' => array(
+                               'file'     => file_get_contents( $this->test_file2 ),
+                               'name'     => 'codeispoetry.jpg',
+                               'size'     => filesize( $this->test_file2 ),
+                               'tmp_name' => $this->test_file2,
+                       ),
+               ) );
+               $request->set_header( 'Content-MD5', md5_file( $this->test_file2 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'codeispoetry', $data['title']['raw'] );
+       }
+
+       public function test_create_item_with_files() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_file_params( array(
+                       'file' => array(
+                               'file'     => file_get_contents( $this->test_file ),
+                               'name'     => 'canola.jpg',
+                               'size'     => filesize( $this->test_file ),
+                               'tmp_name' => $this->test_file,
+                       ),
+               ) );
+               $request->set_header( 'Content-MD5', md5_file( $this->test_file ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+       }
+
+       public function test_create_item_with_upload_files_role() {
+               wp_set_current_user( $this->uploader_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_file_params( array(
+                       'file' => array(
+                               'file'     => file_get_contents( $this->test_file ),
+                               'name'     => 'canola.jpg',
+                               'size'     => filesize( $this->test_file ),
+                               'tmp_name' => $this->test_file,
+                       ),
+               ) );
+               $request->set_header( 'Content-MD5', md5_file( $this->test_file ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+       }
+
+       public function test_create_item_empty_body() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_upload_no_data', $response, 400 );
+       }
+
+       public function test_create_item_missing_content_type() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_upload_no_content_type', $response, 400 );
+       }
+
+       public function test_create_item_missing_content_disposition() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_header( 'Content-Type', 'image/jpeg' );
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_upload_no_content_disposition', $response, 400 );
+       }
+
+       public function test_create_item_bad_md5_header() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_header( 'Content-Type', 'image/jpeg' );
+               $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
+               $request->set_header( 'Content-MD5', 'abc123' );
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_upload_hash_mismatch', $response, 412 );
+       }
+
+       public function test_create_item_with_files_bad_md5_header() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_file_params( array(
+                       'file' => array(
+                               'file'     => file_get_contents( $this->test_file ),
+                               'name'     => 'canola.jpg',
+                               'size'     => filesize( $this->test_file ),
+                               'tmp_name' => $this->test_file,
+                       ),
+               ) );
+               $request->set_header( 'Content-MD5', 'abc123' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_upload_hash_mismatch', $response, 412 );
+       }
+
+       public function test_create_item_invalid_upload_files_capability() {
+               wp_set_current_user( $this->contributor_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
+       }
+
+       public function test_create_item_invalid_edit_permissions() {
+               $post_id = $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_create_item_invalid_upload_permissions() {
+               $post_id = $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
+               wp_set_current_user( $this->uploader_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_create_item_invalid_post_type() {
+               $attachment_id = $this->factory->post->create( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => 0 ) );
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_header( 'Content-Type', 'image/jpeg' );
+               $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $request->set_param( 'post', $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_item_alt_text() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_header( 'Content-Type', 'image/jpeg' );
+               $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
+
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $request->set_param( 'alt_text', 'test alt text' );
+               $response = $this->server->dispatch( $request );
+               $attachment = $response->get_data();
+               $this->assertEquals( 'test alt text', $attachment['alt_text'] );
+       }
+
+       public function test_create_item_unsafe_alt_text() {
+               wp_set_current_user( $this->author_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
+               $request->set_header( 'Content-Type', 'image/jpeg' );
+               $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
+               $request->set_body( file_get_contents( $this->test_file ) );
+               $request->set_param( 'alt_text', '<script>alert(document.cookie)</script>' );
+               $response = $this->server->dispatch( $request );
+               $attachment = $response->get_data();
+               $this->assertEquals( '', $attachment['alt_text'] );
+       }
+
+       public function test_update_item() {
+               wp_set_current_user( $this->editor_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
+               $request->set_param( 'title', 'My title is very cool' );
+               $request->set_param( 'caption', 'This is a better caption.' );
+               $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' );
+               $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $attachment = get_post( $data['id'] );
+               $this->assertEquals( 'My title is very cool', $data['title']['raw'] );
+               $this->assertEquals( 'My title is very cool', $attachment->post_title );
+               $this->assertEquals( 'This is a better caption.', $data['caption'] );
+               $this->assertEquals( 'This is a better caption.', $attachment->post_excerpt );
+               $this->assertEquals( 'Without a description, my attachment is descriptionless.', $data['description'] );
+               $this->assertEquals( 'Without a description, my attachment is descriptionless.', $attachment->post_content );
+               $this->assertEquals( 'Alt text is stored outside post schema.', $data['alt_text'] );
+               $this->assertEquals( 'Alt text is stored outside post schema.', get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ) );
+       }
+
+       public function test_update_item_parent() {
+               wp_set_current_user( $this->editor_id );
+               $original_parent = $this->factory->post->create( array() );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, $original_parent, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+
+               $attachment = get_post( $attachment_id );
+               $this->assertEquals( $original_parent, $attachment->post_parent );
+
+               $new_parent = $this->factory->post->create( array() );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
+               $request->set_param( 'post', $new_parent );
+               $this->server->dispatch( $request );
+
+               $attachment = get_post( $attachment_id );
+               $this->assertEquals( $new_parent, $attachment->post_parent );
+       }
+
+       public function test_update_item_invalid_permissions() {
+               wp_set_current_user( $this->author_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
+               $request->set_param( 'caption', 'This is a better caption.' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_update_item_invalid_post_type() {
+               $attachment_id = $this->factory->post->create( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => 0 ) );
+               wp_set_current_user( $this->editor_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
+               $request->set_param( 'post', $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_delete_item() {
+               wp_set_current_user( $this->editor_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/media/' . $attachment_id );
+               $request['force'] = true;
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+       }
+
+       public function test_delete_item_no_trash() {
+               wp_set_current_user( $this->editor_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+
+               // Attempt trashing
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/media/' . $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 );
+
+               // Ensure the post still exists
+               $post = get_post( $attachment_id );
+               $this->assertNotEmpty( $post );
+       }
+
+       public function test_delete_item_invalid_delete_permissions() {
+               wp_set_current_user( $this->author_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/media/' . $attachment_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+       }
+
+       public function test_prepare_item() {
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+
+               $attachment = get_post( $attachment_id );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/media/%d', $attachment_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->check_post_data( $attachment, $data, 'view', $response->get_links() );
+               $this->check_post_data( $attachment, $data, 'embed', $response->get_links() );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 23, count( $properties ) );
+               $this->assertArrayHasKey( 'author', $properties );
+               $this->assertArrayHasKey( 'alt_text', $properties );
+               $this->assertArrayHasKey( 'caption', $properties );
+               $this->assertArrayHasKey( 'description', $properties );
+               $this->assertArrayHasKey( 'comment_status', $properties );
+               $this->assertArrayHasKey( 'date', $properties );
+               $this->assertArrayHasKey( 'date_gmt', $properties );
+               $this->assertArrayHasKey( 'guid', $properties );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'media_type', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'mime_type', $properties );
+               $this->assertArrayHasKey( 'media_details', $properties );
+               $this->assertArrayHasKey( 'modified', $properties );
+               $this->assertArrayHasKey( 'modified_gmt', $properties );
+               $this->assertArrayHasKey( 'post', $properties );
+               $this->assertArrayHasKey( 'ping_status', $properties );
+               $this->assertArrayHasKey( 'status', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'source_url', $properties );
+               $this->assertArrayHasKey( 'title', $properties );
+               $this->assertArrayHasKey( 'type', $properties );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'attachment', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+               ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function test_additional_field_update_errors() {
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'attachment', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               wp_set_current_user( $this->editor_id );
+               $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'post_excerpt'   => 'A sample caption',
+                       'post_author'    => $this->editor_id,
+               ) );
+               // Check for error on update.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/media/%d', $attachment_id ) );
+               $request->set_body_params(array(
+                       'my_custom_int' => 'returnError',
+               ));
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object, $request ) {
+               return 123;
+       }
+
+       public function additional_field_update_callback( $value, $attachment ) {
+               if ( 'returnError' === $value ) {
+                       return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
+               }
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+               if ( file_exists( $this->test_file ) ) {
+                       unlink( $this->test_file );
+               }
+               if ( file_exists( $this->test_file2 ) ) {
+                       unlink( $this->test_file2 );
+               }
+       }
+
+       protected function check_post_data( $attachment, $data, $context = 'view', $links ) {
+               parent::check_post_data( $attachment, $data, $context, $links );
+
+               $this->assertEquals( get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ), $data['alt_text'] );
+               $this->assertEquals( $attachment->post_excerpt, $data['caption'] );
+               $this->assertEquals( $attachment->post_content, $data['description'] );
+               $this->assertTrue( isset( $data['media_details'] ) );
+
+               if ( $attachment->post_parent ) {
+                       $this->assertEquals( $attachment->post_parent, $data['post'] );
+               } else {
+                       $this->assertNull( $data['post'] );
+               }
+
+               $this->assertEquals( wp_get_attachment_url( $attachment->ID ), $data['source_url'] );
+
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestcategoriescontrollerphp"></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-categories-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-categories-controller.php                         (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-categories-controller.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,882 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Terms_Controller functionality, used for
+ * Categories.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Categories_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->administrator = $this->factory->user->create( array(
+                       'role' => 'administrator',
+               ) );
+               $this->subscriber = $this->factory->user->create( array(
+                       'role' => 'subscriber',
+               ) );
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/categories', $routes );
+               $this->assertArrayHasKey( '/wp/v2/categories/(?P<id>[\d]+)', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $category1 = $this->factory->category->create( array( 'name' => 'Season 5' ) );
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories/' . $category1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'context',
+                       'exclude',
+                       'hide_empty',
+                       'include',
+                       'order',
+                       'orderby',
+                       'page',
+                       'parent',
+                       'per_page',
+                       'post',
+                       'search',
+                       'slug',
+                       ), $keys );
+       }
+
+       public function test_get_items() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $this->check_get_taxonomy_terms_response( $response );
+       }
+
+       public function test_get_items_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_items_hide_empty_arg() {
+               $post_id = $this->factory->post->create();
+               $category1 = $this->factory->category->create( array( 'name' => 'Season 5' ) );
+               $category2 = $this->factory->category->create( array( 'name' => 'The Be Sharps' ) );
+               wp_set_object_terms( $post_id, array( $category1, $category2 ), 'category' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'hide_empty', true );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'Season 5', $data[0]['name'] );
+               $this->assertEquals( 'The Be Sharps', $data[1]['name'] );
+       }
+
+       public function test_get_items_parent_zero_arg() {
+               $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) );
+               $parent2 = $this->factory->category->create( array( 'name' => 'Marge' ) );
+               $this->factory->category->create(
+                       array(
+                               'name'   => 'Bart',
+                               'parent' => $parent1,
+                       )
+               );
+               $this->factory->category->create(
+                       array(
+                               'name'   => 'Lisa',
+                               'parent' => $parent2,
+                       )
+               );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'parent', 0 );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+
+               $args = array(
+                       'hide_empty' => false,
+                       'parent'     => 0,
+               );
+               $categories = get_terms( 'category', $args );
+               $this->assertEquals( count( $categories ), count( $data ) );
+       }
+
+       public function test_get_items_parent_zero_arg_string() {
+               $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) );
+               $parent2 = $this->factory->category->create( array( 'name' => 'Marge' ) );
+               $this->factory->category->create(
+                       array(
+                               'name'   => 'Bart',
+                               'parent' => $parent1,
+                       )
+               );
+               $this->factory->category->create(
+                       array(
+                               'name'   => 'Lisa',
+                               'parent' => $parent2,
+                       )
+               );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'parent', '0' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+
+               $args = array(
+                       'hide_empty' => false,
+                       'parent'     => 0,
+               );
+               $categories = get_terms( 'category', $args );
+               $this->assertEquals( count( $categories ), count( $data ) );
+       }
+
+       public function test_get_items_by_parent_non_found() {
+               $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'parent', $parent1 );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+
+               $this->assertEquals( array(), $data );
+       }
+
+       public function test_get_items_invalid_page() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'page', 0 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+               $data = $response->get_data();
+               $first_error = array_shift( $data['data']['params'] );
+               $this->assertContains( 'page must be greater than 1 (inclusive)', $first_error );
+       }
+
+       public function test_get_items_include_query() {
+               $id1 = $this->factory->category->create();
+               $this->factory->category->create();
+               $id3 = $this->factory->category->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               // Orderby=>asc
+               $request->set_param( 'include', array( $id3, $id1 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id1, $data[0]['id'] );
+               // Orderby=>include
+               $request->set_param( 'orderby', 'include' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id3, $data[0]['id'] );
+       }
+
+       public function test_get_items_exclude_query() {
+               $id1 = $this->factory->category->create();
+               $id2 = $this->factory->category->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+               $request->set_param( 'exclude', array( $id2 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+       }
+
+       public function test_get_items_orderby_args() {
+               $this->factory->category->create( array( 'name' => 'Apple' ) );
+               $this->factory->category->create( array( 'name' => 'Banana' ) );
+               /*
+                * Tests:
+                * - orderby
+                * - order
+                * - per_page
+                */
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'orderby', 'name' );
+               $request->set_param( 'order', 'desc' );
+               $request->set_param( 'per_page', 1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Uncategorized', $data[0]['name'] );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'orderby', 'name' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'per_page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+       }
+
+       public function test_get_items_orderby_id() {
+               $this->factory->category->create( array( 'name' => 'Cantaloupe' ) );
+               $this->factory->category->create( array( 'name' => 'Apple' ) );
+               $this->factory->category->create( array( 'name' => 'Banana' ) );
+               // defaults to orderby=name, order=asc
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+               $this->assertEquals( 'Banana', $data[1]['name'] );
+               $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
+               $this->assertEquals( 'Uncategorized', $data[3]['name'] );
+               // orderby=id, with default order=asc
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'orderby', 'id' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Uncategorized', $data[0]['name'] );
+               $this->assertEquals( 'Cantaloupe', $data[1]['name'] );
+               $this->assertEquals( 'Apple', $data[2]['name'] );
+               $this->assertEquals( 'Banana', $data[3]['name'] );
+               // orderby=id, order=desc
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'orderby', 'id' );
+               $request->set_param( 'order', 'desc' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 'Banana', $data[0]['name'] );
+               $this->assertEquals( 'Apple', $data[1]['name'] );
+               $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
+       }
+
+       protected function post_with_categories() {
+               $post_id = $this->factory->post->create();
+               $category1 = $this->factory->category->create( array(
+                       'name' => 'DC',
+                       'description' => 'Purveyor of fine detective comics',
+               ) );
+               $category2 = $this->factory->category->create( array(
+                       'name' => 'Marvel',
+                       'description' => 'Home of the Marvel Universe',
+               ) );
+               $category3 = $this->factory->category->create( array(
+                       'name' => 'Image',
+                       'description' => 'American independent comic publisher',
+               ) );
+               wp_set_object_terms( $post_id, array( $category1, $category2, $category3 ), 'category' );
+
+               return $post_id;
+       }
+
+       public function test_get_items_post_args() {
+               $post_id = $this->post_with_categories();
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 3, count( $data ) );
+
+               // Check ordered by name by default
+               $names = wp_list_pluck( $data, 'name' );
+               $this->assertEquals( array( 'DC', 'Image', 'Marvel' ), $names );
+       }
+
+       public function test_get_items_post_ordered_by_description() {
+               $post_id = $this->post_with_categories();
+
+               // Regular request
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'post', $post_id );
+               $request->set_param( 'orderby', 'description' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 3, count( $data ) );
+               $names = wp_list_pluck( $data, 'name' );
+               $this->assertEquals( array( 'Image', 'Marvel', 'DC' ), $names, 'Terms should be ordered by description' );
+
+               // Flip the order
+               $request->set_param( 'order', 'desc' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 3, count( $data ) );
+               $names = wp_list_pluck( $data, 'name' );
+               $this->assertEquals( array( 'DC', 'Marvel', 'Image' ), $names, 'Terms should be reverse-ordered by description' );
+       }
+
+       public function test_get_items_post_ordered_by_id() {
+               $post_id = $this->post_with_categories();
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'post', $post_id );
+               $request->set_param( 'orderby', 'id' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 3, count( $data ) );
+               $names = wp_list_pluck( $data, 'name' );
+               $this->assertEquals( array( 'DC', 'Marvel', 'Image' ), $names );
+       }
+
+       public function test_get_items_custom_tax_post_args() {
+               register_taxonomy( 'batman', 'post', array( 'show_in_rest' => true ) );
+               $controller = new WP_REST_Terms_Controller( 'batman' );
+               $controller->register_routes();
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'batman' ) );
+               $term2 = $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'batman' ) );
+               $this->factory->term->create( array( 'name' => 'Car', 'taxonomy' => 'batman' ) );
+               $post_id = $this->factory->post->create();
+               wp_set_object_terms( $post_id, array( $term1, $term2 ), 'batman' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/batman' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'Cape', $data[0]['name'] );
+       }
+
+       public function test_get_items_search_args() {
+               $this->factory->category->create( array( 'name' => 'Apple' ) );
+               $this->factory->category->create( array( 'name' => 'Banana' ) );
+               /*
+                * Tests:
+                * - search
+                */
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'search', 'App' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'search', 'Garbage' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 0, count( $data ) );
+       }
+
+       public function test_get_items_slug_arg() {
+               $this->factory->category->create( array( 'name' => 'Apple' ) );
+               $this->factory->category->create( array( 'name' => 'Banana' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'slug', 'apple' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+       }
+
+       public function test_get_terms_parent_arg() {
+               $category1 = $this->factory->category->create( array( 'name' => 'Parent' ) );
+               $this->factory->category->create( array( 'name' => 'Child', 'parent' => $category1 ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'parent', $category1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Child', $data[0]['name'] );
+       }
+
+       public function test_get_terms_private_taxonomy() {
+               register_taxonomy( 'robin', 'post', array( 'public' => false ) );
+               $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
+               $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'robin' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_terms_invalid_taxonomy() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/invalid-taxonomy' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_terms_pagination_headers() {
+               // Start of the index + Uncategorized default term
+               for ( $i = 0; $i < 49; $i++ ) {
+                       $this->factory->category->create( array(
+                               'name'   => "Category {$i}",
+                               ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 50, $headers['X-WP-Total'] );
+               $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
+               $this->assertCount( 10, $response->get_data() );
+               $next_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( 'wp/v2/categories' ) );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // 3rd page
+               $this->factory->category->create( array(
+                               'name'   => 'Category 51',
+                               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $this->assertCount( 10, $response->get_data() );
+               $prev_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( 'wp/v2/categories' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 4,
+                       ), rest_url( 'wp/v2/categories' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // Last page
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'page', 6 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $this->assertCount( 1, $response->get_data() );
+               $prev_link = add_query_arg( array(
+                       'page'    => 5,
+                       ), rest_url( 'wp/v2/categories' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+               // Out of bounds
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'page', 8 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $this->assertCount( 0, $response->get_data() );
+               $prev_link = add_query_arg( array(
+                       'page'    => 6,
+                       ), rest_url( 'wp/v2/categories' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+       }
+
+       public function test_get_items_per_page_exceeds_number_of_items() {
+               // Start of the index + Uncategorized default term
+               for ( $i = 0; $i < 17; $i++ ) {
+                       $this->factory->category->create( array(
+                               'name'   => "Category {$i}",
+                               ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'page', 1 );
+               $request->set_param( 'per_page', 100 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 18, $headers['X-WP-Total'] );
+               $this->assertEquals( 1, $headers['X-WP-TotalPages'] );
+               $this->assertCount( 18, $response->get_data() );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
+               $request->set_param( 'page', 2 );
+               $request->set_param( 'per_page', 100 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 18, $headers['X-WP-Total'] );
+               $this->assertEquals( 1, $headers['X-WP-TotalPages'] );
+               $this->assertCount( 0, $response->get_data() );
+       }
+
+       public function test_get_item() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/1' );
+               $response = $this->server->dispatch( $request );
+               $this->check_get_taxonomy_term_response( $response );
+       }
+
+       public function test_get_term_invalid_taxonomy() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/invalid-taxonomy/1' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_term_invalid_term() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_get_item_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/1' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_term_private_taxonomy() {
+               register_taxonomy( 'robin', 'post', array( 'public' => false ) );
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin/' . $term1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_item_incorrect_taxonomy() {
+               register_taxonomy( 'robin', 'post' );
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $term1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_create_item() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
+               $request->set_param( 'name', 'My Awesome Term' );
+               $request->set_param( 'description', 'This term is so awesome.' );
+               $request->set_param( 'slug', 'so-awesome' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+               $headers = $response->get_headers();
+               $data = $response->get_data();
+               $this->assertContains( '/wp/v2/categories/' . $data['id'], $headers['Location'] );
+               $this->assertEquals( 'My Awesome Term', $data['name'] );
+               $this->assertEquals( 'This term is so awesome.', $data['description'] );
+               $this->assertEquals( 'so-awesome', $data['slug'] );
+       }
+
+       public function test_create_item_invalid_taxonomy() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/invalid-taxonomy' );
+               $request->set_param( 'name', 'Invalid Taxonomy' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_create_item_incorrect_permissions() {
+               wp_set_current_user( $this->subscriber );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
+               $request->set_param( 'name', 'Incorrect permissions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
+       }
+
+       public function test_create_item_missing_arguments() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 );
+       }
+
+       public function test_create_item_with_parent() {
+               wp_set_current_user( $this->administrator );
+               $parent = wp_insert_term( 'test-category', 'category' );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
+               $request->set_param( 'name', 'My Awesome Term' );
+               $request->set_param( 'parent', $parent['term_id'] );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( $parent['term_id'], $data['parent'] );
+       }
+
+       public function test_create_item_invalid_parent() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
+               $request->set_param( 'name', 'My Awesome Term' );
+               $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 400 );
+       }
+
+       public function test_update_item() {
+               wp_set_current_user( $this->administrator );
+               $orig_args = array(
+                       'name'        => 'Original Name',
+                       'description' => 'Original Description',
+                       'slug'        => 'original-slug',
+                       );
+               $term = get_term_by( 'id', $this->factory->category->create( $orig_args ), 'category' );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
+               $request->set_param( 'name', 'New Name' );
+               $request->set_param( 'description', 'New Description' );
+               $request->set_param( 'slug', 'new-slug' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'New Name', $data['name'] );
+               $this->assertEquals( 'New Description', $data['description'] );
+               $this->assertEquals( 'new-slug', $data['slug'] );
+       }
+
+       public function test_update_item_invalid_taxonomy() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/invalid-taxonomy/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $request->set_param( 'name', 'Invalid Taxonomy' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_update_item_invalid_term() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $request->set_param( 'name', 'Invalid Term' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_update_item_incorrect_permissions() {
+               wp_set_current_user( $this->subscriber );
+               $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
+               $request->set_param( 'name', 'Incorrect permissions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+       }
+
+       public function test_update_item_parent() {
+               wp_set_current_user( $this->administrator );
+               $parent = get_term_by( 'id', $this->factory->category->create(), 'category' );
+               $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_taxonomy_id );
+               $request->set_param( 'parent', $parent->term_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( $parent->term_id, $data['parent'] );
+       }
+
+       public function test_update_item_invalid_parent() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
+               $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 400 );
+       }
+
+       public function test_delete_item() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->category->create( array( 'name' => 'Deleted Category' ) ), 'category' );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id );
+               $request->set_param( 'force', true );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Deleted Category', $data['name'] );
+       }
+
+       public function test_delete_item_force_false() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->category->create( array( 'name' => 'Deleted Category' ) ), 'category' );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id );
+               // force defaults to false
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 501, $response->get_status() );
+       }
+
+       public function test_delete_item_invalid_taxonomy() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/invalid-taxonomy/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_delete_item_invalid_term() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_delete_item_incorrect_permissions() {
+               wp_set_current_user( $this->subscriber );
+               $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+       }
+
+       public function test_prepare_item() {
+               $term = get_term( 1, 'category' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/1' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->check_taxonomy_term( $term, $data, $response->get_links() );
+       }
+
+       public function test_prepare_taxonomy_term_child() {
+               $child = $this->factory->category->create( array(
+                       'parent' => 1,
+               ) );
+               $term = get_term( $child, 'category' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $child );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->check_taxonomy_term( $term, $data, $response->get_links() );
+
+               $this->assertEquals( 1, $data['parent'] );
+
+               $links = $response->get_links();
+               $this->assertEquals( rest_url( 'wp/v2/categories/1' ), $links['up'][0]['href'] );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 9, count( $properties ) );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'count', $properties );
+               $this->assertArrayHasKey( 'description', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'name', $properties );
+               $this->assertArrayHasKey( 'parent', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'taxonomy', $properties );
+               $this->assertEquals( array_keys( get_taxonomies() ), $properties['taxonomy']['enum'] );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'category', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               $category_id = $this->factory->category->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $category_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object, $request ) {
+               return 123;
+       }
+
+       public function tearDown() {
+               _unregister_taxonomy( 'batman' );
+               _unregister_taxonomy( 'robin' );
+               parent::tearDown();
+       }
+
+       protected function check_get_taxonomy_terms_response( $response ) {
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $args = array(
+                       'hide_empty' => false,
+               );
+               $categories = get_terms( 'category', $args );
+               $this->assertEquals( count( $categories ), count( $data ) );
+               $this->assertEquals( $categories[0]->term_id, $data[0]['id'] );
+               $this->assertEquals( $categories[0]->name, $data[0]['name'] );
+               $this->assertEquals( $categories[0]->slug, $data[0]['slug'] );
+               $this->assertEquals( $categories[0]->taxonomy, $data[0]['taxonomy'] );
+               $this->assertEquals( $categories[0]->description, $data[0]['description'] );
+               $this->assertEquals( $categories[0]->count, $data[0]['count'] );
+       }
+
+       protected function check_taxonomy_term( $term, $data, $links ) {
+               $this->assertEquals( $term->term_id, $data['id'] );
+               $this->assertEquals( $term->name, $data['name'] );
+               $this->assertEquals( $term->slug, $data['slug'] );
+               $this->assertEquals( $term->description, $data['description'] );
+               $this->assertEquals( get_term_link( $term ),  $data['link'] );
+               $this->assertEquals( $term->count, $data['count'] );
+               $taxonomy = get_taxonomy( $term->taxonomy );
+               if ( $taxonomy->hierarchical ) {
+                       $this->assertEquals( $term->parent, $data['parent'] );
+               } else {
+                       $this->assertFalse( isset( $term->parent ) );
+               }
+
+               $relations = array(
+                       'self',
+                       'collection',
+                       'about',
+                       'https://api.w.org/post_type',
+               );
+
+               if ( ! empty( $data['parent'] ) ) {
+                       $relations[] = 'up';
+               }
+
+               $this->assertEqualSets( $relations, array_keys( $links ) );
+               $this->assertContains( 'wp/v2/taxonomies/' . $term->taxonomy, $links['about'][0]['href'] );
+               $this->assertEquals( add_query_arg( 'categories', $term->term_id, rest_url( 'wp/v2/posts' ) ), $links['https://api.w.org/post_type'][0]['href'] );
+       }
+
+       protected function check_get_taxonomy_term_response( $response ) {
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $category = get_term( 1, 'category' );
+               $this->check_taxonomy_term( $category, $data, $response->get_links() );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestcommentscontrollerphp"></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-comments-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-comments-controller.php                           (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-comments-controller.php     2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1857 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Comments_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase {
+
+       protected $admin_id;
+       protected $subscriber_id;
+
+       protected $post_id;
+       protected $private_id;
+       protected $draft_id;
+       protected $trash_id;
+
+       protected $approved_id;
+       protected $hold_id;
+
+       protected $endpoint;
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->admin_id = $this->factory->user->create( array(
+                       'role' => 'administrator',
+               ));
+               $this->subscriber_id = $this->factory->user->create( array(
+                       'role' => 'subscriber',
+               ));
+               $this->author_id = $this->factory->user->create( array(
+                       'role'         => 'author',
+                       'display_name' => 'Sea Captain',
+                       'first_name'   => 'Horatio',
+                       'last_name'    => 'McCallister',
+                       'user_email'   => 'captain@thefryingdutchman.com',
+                       'user_url'     => 'http://thefryingdutchman.com',
+               ));
+
+               $this->post_id = $this->factory->post->create();
+               $this->private_id = $this->factory->post->create( array(
+                       'post_status' => 'private',
+               ));
+               $this->draft_id = $this->factory->post->create( array(
+                       'post_status' => 'draft',
+               ));
+               $this->trash_id = $this->factory->post->create( array(
+                       'post_status' => 'trash',
+               ));
+
+               $this->approved_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => 0,
+               ));
+               $this->hold_id = $this->factory->comment->create( array(
+                       'comment_approved' => 0,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ));
+
+               $this->endpoint = new WP_REST_Comments_Controller;
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+
+               $this->assertArrayHasKey( '/wp/v2/comments', $routes );
+               $this->assertCount( 2, $routes['/wp/v2/comments'] );
+               $this->assertArrayHasKey( '/wp/v2/comments/(?P<id>[\d]+)', $routes );
+               $this->assertCount( 3, $routes['/wp/v2/comments/(?P<id>[\d]+)'] );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments/' . $this->approved_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'after',
+                       'author',
+                       'author_email',
+                       'author_exclude',
+                       'before',
+                       'context',
+                       'exclude',
+                       'include',
+                       'karma',
+                       'offset',
+                       'order',
+                       'orderby',
+                       'page',
+                       'parent',
+                       'parent_exclude',
+                       'per_page',
+                       'post',
+                       'search',
+                       'status',
+                       'type',
+                       ), $keys );
+       }
+
+       public function test_get_items() {
+               $this->factory->comment->create_post_comments( $this->post_id, 6 );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comments = $response->get_data();
+               // We created 6 comments in this method, plus $this->approved_id.
+               $this->assertCount( 7, $comments );
+       }
+
+       public function test_get_items_without_private_post_permission() {
+               wp_set_current_user( 0 );
+
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->private_id,
+               );
+               $private_comment = $this->factory->comment->create( $args );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $collection_data = $response->get_data();
+               $this->assertFalse( in_array( $private_comment, wp_list_pluck( $collection_data, 'id' ), true ) );
+       }
+
+       public function test_get_items_with_private_post_permission() {
+               wp_set_current_user( $this->admin_id );
+
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->private_id,
+               );
+               $private_comment = $this->factory->comment->create( $args );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $collection_data = $response->get_data();
+               $this->assertTrue( in_array( $private_comment, wp_list_pluck( $collection_data, 'id' ), true ) );
+       }
+
+       public function test_get_items_with_invalid_post() {
+               wp_set_current_user( 0 );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
+               ));
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $collection_data = $response->get_data();
+               $this->assertFalse( in_array( $comment_id, wp_list_pluck( $collection_data, 'id' ), true ) );
+
+               wp_delete_comment( $comment_id );
+       }
+
+       public function test_get_items_with_invalid_post_permission() {
+               wp_set_current_user( $this->admin_id );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
+               ));
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $collection_data = $response->get_data();
+               $this->assertTrue( in_array( $comment_id, wp_list_pluck( $collection_data, 'id' ), true ) );
+
+               wp_delete_comment( $comment_id );
+       }
+
+       public function test_get_items_no_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_items_no_post() {
+               $this->factory->comment->create_post_comments( 0, 2 );
+               wp_set_current_user( $this->admin_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'post', 0 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $comments = $response->get_data();
+               $this->assertCount( 2, $comments );
+       }
+
+       public function test_get_items_no_permission_for_no_post() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'post', 0 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
+       }
+
+       public function test_get_items_edit_context() {
+               wp_set_current_user( $this->admin_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+       }
+
+       public function test_get_items_for_post() {
+               $second_post_id = $this->factory->post->create();
+               $this->factory->comment->create_post_comments( $second_post_id, 2 );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_query_params( array(
+                       'post' => $second_post_id,
+               ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comments = $response->get_data();
+               $this->assertCount( 2, $comments );
+       }
+
+       public function test_get_items_include_query() {
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+               );
+               $id1 = $this->factory->comment->create( $args );
+               $this->factory->comment->create( $args );
+               $id3 = $this->factory->comment->create( $args );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               // Order=>asc
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'include', array( $id3, $id1 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id1, $data[0]['id'] );
+               // Orderby=>include
+               $request->set_param( 'orderby', 'include' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id3, $data[0]['id'] );
+       }
+
+       public function test_get_items_exclude_query() {
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+               );
+               $id1 = $this->factory->comment->create( $args );
+               $id2 = $this->factory->comment->create( $args );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+               $request->set_param( 'exclude', array( $id2 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+       }
+
+       public function test_get_items_offset_query() {
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+               );
+               $this->factory->comment->create( $args );
+               $this->factory->comment->create( $args );
+               $this->factory->comment->create( $args );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'offset', 1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 3, $response->get_data() );
+               // 'offset' works with 'per_page'
+               $request->set_param( 'per_page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+               // 'offset' takes priority over 'page'
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+       }
+
+       public function test_get_items_order_query() {
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+               );
+               $this->factory->comment->create( $args );
+               $this->factory->comment->create( $args );
+               $id3 = $this->factory->comment->create( $args );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               // order defaults to 'desc'
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $id3, $data[0]['id'] );
+               // order=>asc
+               $request->set_param( 'order', 'asc' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $this->approved_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_private_post_no_permissions() {
+               wp_set_current_user( 0 );
+               $post_id = $this->factory->post->create( array( 'post_status' => 'private' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read_post', $response, 401 );
+       }
+
+       public function test_get_items_author_arg() {
+               // Authorized
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->author_id,
+               );
+               $this->factory->comment->create( $args );
+               $args['user_id'] = $this->subscriber_id;
+               $this->factory->comment->create( $args );
+               unset( $args['user_id'] );
+               $this->factory->comment->create( $args );
+
+               // 'author' limits result to 1 of 3
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'author', $this->author_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $comments = $response->get_data();
+               $this->assertCount( 1, $comments );
+               // Multiple authors are supported
+               $request->set_param( 'author', array( $this->author_id, $this->subscriber_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $comments = $response->get_data();
+               $this->assertCount( 2, $comments );
+               // Unavailable to unauthenticated; defaults to error
+               wp_set_current_user( 0 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_param', $response, 401 );
+       }
+
+       public function test_get_items_author_exclude_arg() {
+               // Authorized
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->author_id,
+               );
+               $this->factory->comment->create( $args );
+               $args['user_id'] = $this->subscriber_id;
+               $this->factory->comment->create( $args );
+               unset( $args['user_id'] );
+               $this->factory->comment->create( $args );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $comments = $response->get_data();
+               $this->assertCount( 4, $comments );
+
+               // 'author_exclude' limits result to 3 of 4
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'author_exclude', $this->author_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $comments = $response->get_data();
+               $this->assertCount( 3, $comments );
+               // 'author_exclude' for both comment authors (2 of 4)
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'author_exclude', array( $this->author_id, $this->subscriber_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $comments = $response->get_data();
+               $this->assertCount( 2, $comments );
+               // Unavailable to unauthenticated; defaults to error
+               wp_set_current_user( 0 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_param', $response, 401 );
+       }
+
+       public function test_get_items_parent_arg() {
+               $args = array(
+                       'comment_approved'  => 1,
+                       'comment_post_ID'   => $this->post_id,
+               );
+               $parent_id = $this->factory->comment->create( $args );
+               $parent_id2 = $this->factory->comment->create( $args );
+               $args['comment_parent'] = $parent_id;
+               $this->factory->comment->create( $args );
+               $args['comment_parent'] = $parent_id2;
+               $this->factory->comment->create( $args );
+               // All comments in the database
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 5, $response->get_data() );
+               // Limit to the parent
+               $request->set_param( 'parent', $parent_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 1, $response->get_data() );
+               // Limit to two parents
+               $request->set_param( 'parent', array( $parent_id, $parent_id2 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+       }
+
+       public function test_get_items_parent_exclude_arg() {
+               $args = array(
+                       'comment_approved'  => 1,
+                       'comment_post_ID'   => $this->post_id,
+               );
+               $parent_id = $this->factory->comment->create( $args );
+               $parent_id2 = $this->factory->comment->create( $args );
+               $args['comment_parent'] = $parent_id;
+               $this->factory->comment->create( $args );
+               $args['comment_parent'] = $parent_id2;
+               $this->factory->comment->create( $args );
+               // All comments in the database
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 5, $response->get_data() );
+               // Exclude this particular parent
+               $request->set_param( 'parent_exclude', $parent_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 4, $response->get_data() );
+               // Exclude both comment parents
+               $request->set_param( 'parent_exclude', array( $parent_id, $parent_id2 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 3, $response->get_data() );
+       }
+
+       public function test_get_items_search_query() {
+               wp_set_current_user( $this->admin_id );
+               $args = array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'comment_content'  => 'foo',
+                       'comment_author'   => 'Homer J Simpson',
+               );
+               $id1 = $this->factory->comment->create( $args );
+               $args['comment_content'] = 'bar';
+               $this->factory->comment->create( $args );
+               $args['comment_content'] = 'burrito';
+               $this->factory->comment->create( $args );
+               // 3 comments, plus 1 created in construct
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 4, $response->get_data() );
+               // One matching comments
+               $request->set_param( 'search', 'foo' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $id1, $data[0]['id'] );
+       }
+
+       public function test_get_comments_pagination_headers() {
+               wp_set_current_user( $this->admin_id );
+               // Start of the index
+               for ( $i = 0; $i < 49; $i++ ) {
+                       $this->factory->comment->create( array(
+                               'comment_content'   => "Comment {$i}",
+                               'comment_post_ID'   => $this->post_id,
+                               ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 50, $headers['X-WP-Total'] );
+               $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( '/wp/v2/comments' ) );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // 3rd page
+               $this->factory->comment->create( array(
+                               'comment_content'   => 'Comment 51',
+                               'comment_post_ID'   => $this->post_id,
+                               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( '/wp/v2/comments' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 4,
+                       ), rest_url( '/wp/v2/comments' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // Last page
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'page', 6 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 5,
+                       ), rest_url( '/wp/v2/comments' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+               // Out of bounds
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'page', 8 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 6,
+                       ), rest_url( '/wp/v2/comments' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+       }
+
+       public function test_get_comments_invalid_date() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'after', rand_str() );
+               $request->set_param( 'before', rand_str() );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_comments_valid_date() {
+               $comment1 = $this->factory->comment->create( array(
+                       'comment_date'    => '2016-01-15T00:00:00Z',
+                       'comment_post_ID' => $this->post_id,
+               ) );
+               $comment2 = $this->factory->comment->create( array(
+                       'comment_date'    => '2016-01-16T00:00:00Z',
+                       'comment_post_ID' => $this->post_id,
+               ) );
+               $comment3 = $this->factory->comment->create( array(
+                       'comment_date'    => '2016-01-17T00:00:00Z',
+                       'comment_post_ID' => $this->post_id,
+               ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $request->set_param( 'after', '2016-01-15T00:00:00Z' );
+               $request->set_param( 'before', '2016-01-17T00:00:00Z' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $comment2, $data[0]['id'] );
+       }
+
+       public function test_get_item() {
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->check_comment_data( $data, 'view', $response->get_links() );
+       }
+
+       public function test_prepare_item() {
+               wp_set_current_user( $this->admin_id );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->set_query_params( array(
+                       'context' => 'edit',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->check_comment_data( $data, 'edit', $response->get_links() );
+       }
+
+       public function test_get_comment_author_avatar_urls() {
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertArrayHasKey( 24,  $data['author_avatar_urls'] );
+               $this->assertArrayHasKey( 48,  $data['author_avatar_urls'] );
+               $this->assertArrayHasKey( 96,  $data['author_avatar_urls'] );
+
+               $comment = get_comment( $this->approved_id );
+               /**
+                * Ignore the subdomain, since 'get_avatar_url randomly sets the Gravatar
+                * server when building the url string.
+                */
+               $this->assertEquals( substr( get_avatar_url( $comment->comment_author_email ), 9 ), substr( $data['author_avatar_urls'][96], 9 ) );
+       }
+
+       public function test_get_comment_invalid_id() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 );
+       }
+
+       public function test_get_comment_invalid_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $this->approved_id ) );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_comment_invalid_post_id() {
+               wp_set_current_user( 0 );
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
+               ));
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
+       }
+
+       public function test_get_comment_invalid_post_id_as_admin() {
+               wp_set_current_user( $this->admin_id );
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
+               ));
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+       }
+
+       public function test_get_comment_not_approved() {
+               wp_set_current_user( 0 );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->hold_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
+       }
+
+       public function test_get_comment_not_approved_same_user() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->hold_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+       }
+
+       public function test_get_comment_with_children_link() {
+               $comment_id_1 = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               $child_comment = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_parent'   => $comment_id_1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertArrayHasKey( 'children', $response->get_links() );
+       }
+
+       public function test_get_comment_without_children_link() {
+               $comment_id_1 = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertArrayNotHasKey( 'children', $response->get_links() );
+       }
+
+       public function test_create_item() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'    => $this->post_id,
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'content' => 'Worst Comment Ever!',
+                       'date'    => '2014-11-07T10:14:25',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->check_comment_data( $data, 'view', $response->get_links() );
+               $this->assertEquals( 'hold', $data['status'] );
+               $this->assertEquals( '2014-11-07T10:14:25', $data['date'] );
+               $this->assertEquals( $this->post_id, $data['post'] );
+       }
+
+       public function test_create_item_using_accepted_content_raw_value() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_name'  => 'Reverend Lovejoy',
+                       'author_email' => 'lovejoy@example.com',
+                       'author_url'   => 'http://timothylovejoy.jr',
+                       'content'      => array(
+                               'raw' => 'Once something has been approved by the government, it\'s no longer immoral.',
+                       ),
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+
+               $data = $response->get_data();
+               $new_comment = get_comment( $data['id'] );
+               $this->assertEquals( $params['content']['raw'], $new_comment->comment_content );
+       }
+
+       public function test_create_comment_missing_required_author_name_and_email_per_option_value() {
+               update_option( 'require_name_email', 1 );
+
+               $params = array(
+                       'post'    => $this->post_id,
+                       'content' => 'Now, I don\'t want you to worry class. These tests will have no affect on your grades. They merely determine your future social status and financial success. If any.',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_author_data_required', $response, 400 );
+
+               update_option( 'require_name_email', 0 );
+       }
+
+       public function test_create_comment_missing_required_author_name_per_option_value() {
+               update_option( 'require_name_email', 1 );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_email' => 'ekrabappel@springfield-elementary.edu',
+                       'content'      => 'Now, I don\'t want you to worry class. These tests will have no affect on your grades. They merely determine your future social status and financial success. If any.',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_author_required', $response, 400 );
+
+               update_option( 'require_name_email', 0 );
+       }
+
+       public function test_create_comment_missing_required_author_email_per_option_value() {
+               update_option( 'require_name_email', 1 );
+
+               $params = array(
+                       'post'        => $this->post_id,
+                       'author_name' => 'Edna Krabappel',
+                       'content'     => 'Now, I don\'t want you to worry class. These tests will have no affect on your grades. They merely determine your future social status and financial success. If any.',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_author_email_required', $response, 400 );
+
+               update_option( 'require_name_email', 0 );
+       }
+
+       public function test_create_item_invalid_blank_content() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_name'  => 'Reverend Lovejoy',
+                       'author_email' => 'lovejoy@example.com',
+                       'author_url'   => 'http://timothylovejoy.jr',
+                       'content'      => '',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 );
+       }
+
+       public function test_create_item_invalid_date() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_name'  => 'Reverend Lovejoy',
+                       'author_email' => 'lovejoy@example.com',
+                       'author_url'   => 'http://timothylovejoy.jr',
+                       'content'      => 'It\'s all over\, people! We don\'t have a prayer!',
+                       'date'         => rand_str(),
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+
+       public function test_create_item_assign_different_user() {
+               $subscriber_id = $this->factory->user->create( array(
+                       'role' => 'subscriber',
+                       'user_email' => 'cbg@androidsdungeon.com',
+               ));
+
+               wp_set_current_user( $this->admin_id );
+               $params = array(
+                       'post'    => $this->post_id,
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'author' => $subscriber_id,
+                       'content' => 'Worst Comment Ever!',
+                       'date'    => '2014-11-07T10:14:25',
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( $subscriber_id, $data['author'] );
+               $this->assertEquals( '127.0.0.1', $data['author_ip'] );
+       }
+
+       public function test_create_comment_without_type() {
+               $post_id = $this->factory->post->create();
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'post'    => $post_id,
+                       'author'       => $this->admin_id,
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'content' => 'Worst Comment Ever!',
+                       'date'    => '2014-11-07T10:14:25',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 'comment', $data['type'] );
+
+               $comment_id = $data['id'];
+
+               // Make sure the new comment is present in the collection.
+               $collection = new WP_REST_Request( 'GET', '/wp/v2/comments' );
+               $collection->set_param( 'post', $post_id );
+               $collection_response = $this->server->dispatch( $collection );
+               $collection_data = $collection_response->get_data();
+               $this->assertEquals( $comment_id, $collection_data[0]['id'] );
+       }
+
+       public function test_create_item_current_user() {
+               $user_id = $this->factory->user->create( array(
+                       'role' => 'subscriber',
+                       'user_email' => 'lylelanley@example.com',
+                       'first_name' => 'Lyle',
+                       'last_name' => 'Lanley',
+                       'display_name' => 'Lyle Lanley',
+                       'user_url' => 'http://simpsons.wikia.com/wiki/Lyle_Lanley',
+               ));
+
+               wp_set_current_user( $user_id );
+
+               $params = array(
+                       'post' => $this->post_id,
+                       'content' => "Well sir, there's nothing on earth like a genuine, bona fide, electrified, six-car Monorail!",
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 201, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( $user_id, $data['author'] );
+
+               // Check author data matches
+               $author = get_user_by( 'id', $user_id );
+               $comment = get_comment( $data['id'] );
+               $this->assertEquals( $author->display_name, $comment->comment_author );
+               $this->assertEquals( $author->user_email, $comment->comment_author_email );
+               $this->assertEquals( $author->user_url, $comment->comment_author_url );
+       }
+
+       public function test_create_comment_other_user() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'post'    => $this->post_id,
+                       'author_name'  => 'Homer Jay Simpson',
+                       'author_email' => 'chunkylover53@aol.com',
+                       'author_url'   => 'http://compuglobalhypermeganet.com',
+                       'content' => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
+                       'author'    => $this->subscriber_id,
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 201, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( $this->subscriber_id, $data['author'] );
+               $this->assertEquals( 'Homer Jay Simpson', $data['author_name'] );
+               $this->assertEquals( 'chunkylover53@aol.com', $data['author_email'] );
+               $this->assertEquals( 'http://compuglobalhypermeganet.com', $data['author_url'] );
+       }
+
+       public function test_create_comment_other_user_without_permission() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_name'  => 'Homer Jay Simpson',
+                       'author_email' => 'chunkylover53@aol.com',
+                       'author_url'   => 'http://compuglobalhypermeganet.com',
+                       'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
+                       'author'       => $this->admin_id,
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_comment_invalid_author', $response, 403 );
+       }
+
+       public function test_create_comment_karma_without_permission() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_name'  => 'Homer Jay Simpson',
+                       'author_email' => 'chunkylover53@aol.com',
+                       'author_url'   => 'http://compuglobalhypermeganet.com',
+                       'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
+                       'author'       => $this->subscriber_id,
+                       'karma'        => 100,
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_comment_invalid_karma', $response, 403 );
+       }
+
+       public function test_create_comment_status_without_permission() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author_name'  => 'Homer Jay Simpson',
+                       'author_email' => 'chunkylover53@aol.com',
+                       'author_url'   => 'http://compuglobalhypermeganet.com',
+                       'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
+                       'author'       => $this->subscriber_id,
+                       'status'        => 'approved',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_comment_invalid_status', $response, 403 );
+       }
+
+       public function test_create_comment_with_status_and_IP() {
+               $post_id = $this->factory->post->create();
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'post'         => $post_id,
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_ip'    => '139.130.4.5',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'content'      => 'Worst Comment Ever!',
+                       'status'       => 'approved',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 'approved', $data['status'] );
+               $this->assertEquals( '139.130.4.5', $data['author_ip'] );
+       }
+
+       public function test_create_comment_invalid_author_IP() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'author_ip'    => '867.5309',
+                       'content'      => 'Worst Comment Ever!',
+                       'status'       => 'approved',
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_comment_no_post_id() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'content'      => 'Worst Comment Ever!',
+                       'status'       => 'approved',
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+       }
+
+       public function test_create_comment_no_post_id_no_permission() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'author_name'  => 'Homer Jay Simpson',
+                       'author_email' => 'chunkylover53@aol.com',
+                       'author_url'   => 'http://compuglobalhypermeganet.com',
+                       'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
+                       'author'       => $this->subscriber_id,
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_comment_invalid_post_id', $response, 403 );
+       }
+
+       public function test_create_comment_draft_post() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'post'         => $this->draft_id,
+                       'author_name'  => 'Ishmael',
+                       'author_email' => 'herman-melville@earthlink.net',
+                       'author_url'   => 'https://en.wikipedia.org/wiki/Herman_Melville',
+                       'content'      => 'Call me Ishmael.',
+                       'author'       => $this->subscriber_id,
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_comment_draft_post', $response, 403 );
+       }
+
+       public function test_create_comment_trash_post() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'post'         => $this->trash_id,
+                       'author_name'  => 'Ishmael',
+                       'author_email' => 'herman-melville@earthlink.net',
+                       'author_url'   => 'https://en.wikipedia.org/wiki/Herman_Melville',
+                       'content'      => 'Call me Ishmael.',
+                       'author'       => $this->subscriber_id,
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_comment_trash_post', $response, 403 );
+       }
+
+       public function test_create_comment_private_post_invalid_permission() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'post'         => $this->private_id,
+                       'author_name'  => 'Homer Jay Simpson',
+                       'author_email' => 'chunkylover53@aol.com',
+                       'author_url'   => 'http://compuglobalhypermeganet.com',
+                       'content'      => 'I\’d be a vegetarian if bacon grew on trees.',
+                       'author'       => $this->subscriber_id,
+               );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_read_post', $response, 403 );
+       }
+
+       public function test_create_item_duplicate() {
+               $this->factory->comment->create(
+                       array(
+                               'comment_post_ID'      => $this->post_id,
+                               'comment_author'       => 'Guy N. Cognito',
+                               'comment_author_email' => 'chunkylover53@aol.co.uk',
+                               'comment_content'      => 'Homer? Who is Homer? My name is Guy N. Cognito.',
+                       )
+               );
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'    => $this->post_id,
+                       'author_name'  => 'Guy N. Cognito',
+                       'author_email' => 'chunkylover53@aol.co.uk',
+                       'content' => 'Homer? Who is Homer? My name is Guy N. Cognito.',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 409, $response->get_status() );
+       }
+
+       public function test_create_comment_closed() {
+               $post_id = $this->factory->post->create( array(
+                       'comment_status' => 'closed',
+               ));
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'      => $post_id,
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 403, $response->get_status() );
+       }
+
+       public function test_create_comment_require_login() {
+               wp_set_current_user( 0 );
+               update_option( 'comment_registration', 1 );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->set_param( 'post', $this->post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 401, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'rest_comment_login_required', $data['code'] );
+       }
+
+       public function test_create_item_invalid_author() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author'       => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
+                       'content'      => 'It\'s all over\, people! We don\'t have a prayer!',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_author_invalid', $response, 400 );
+       }
+
+       public function test_create_item_pull_author_info() {
+               wp_set_current_user( $this->admin_id );
+
+               $author = new WP_User( $this->author_id );
+               $params = array(
+                       'post'         => $this->post_id,
+                       'author'       => $this->author_id,
+                       'content'      => 'It\'s all over\, people! We don\'t have a prayer!',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $result = $response->get_data();
+               $this->assertSame( $this->author_id, $result['author'] );
+               $this->assertSame( 'Sea Captain', $result['author_name'] );
+               $this->assertSame( 'captain@thefryingdutchman.com', $result['author_email'] );
+               $this->assertSame( 'http://thefryingdutchman.com', $result['author_url'] );
+       }
+
+       public function test_create_comment_two_times() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'post'    => $this->post_id,
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'content' => 'Worst Comment Ever!',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+
+               $params = array(
+                       'post'    => $this->post_id,
+                       'author_name'  => 'Comic Book Guy',
+                       'author_email' => 'cbg@androidsdungeon.com',
+                       'author_url'   => 'http://androidsdungeon.com',
+                       'content'      => 'Shakes fist at sky',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 400, $response->get_status() );
+       }
+
+       public function test_update_item() {
+               $post_id = $this->factory->post->create();
+
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'author'       => $this->subscriber_id,
+                       'author_name'  => 'Disco Stu',
+                       'author_url'   => 'http://stusdisco.com',
+                       'author_email' => 'stu@stusdisco.com',
+                       'author_ip'    => '4.4.4.4',
+                       'content'      => 'Testing.',
+                       'date'         => '2014-11-07T10:14:25',
+                       'karma'        => 100,
+                       'post'         => $post_id,
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comment = $response->get_data();
+               $updated = get_comment( $this->approved_id );
+               $this->assertEquals( $params['content'], $comment['content']['raw'] );
+               $this->assertEquals( $params['author'], $comment['author'] );
+               $this->assertEquals( $params['author_name'], $comment['author_name'] );
+               $this->assertEquals( $params['author_url'], $comment['author_url'] );
+               $this->assertEquals( $params['author_email'], $comment['author_email'] );
+               $this->assertEquals( $params['author_ip'], $comment['author_ip'] );
+               $this->assertEquals( $params['post'], $comment['post'] );
+               $this->assertEquals( $params['karma'], $comment['karma'] );
+
+               $this->assertEquals( mysql_to_rfc3339( $updated->comment_date ), $comment['date'] );
+               $this->assertEquals( '2014-11-07T10:14:25', $comment['date'] );
+       }
+
+       public function test_update_comment_status() {
+               wp_set_current_user( $this->admin_id );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 0,
+                       'comment_post_ID'  => $this->post_id,
+               ));
+
+               $params = array(
+                       'status' => 'approve',
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comment = $response->get_data();
+               $updated = get_comment( $comment_id );
+               $this->assertEquals( 'approved', $comment['status'] );
+               $this->assertEquals( 1, $updated->comment_approved );
+       }
+
+       public function test_update_comment_field_does_not_use_default_values() {
+               wp_set_current_user( $this->admin_id );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 0,
+                       'comment_post_ID'  => $this->post_id,
+                       'comment_content'  => 'some content',
+               ));
+
+               $params = array(
+                       'status' => 'approve',
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comment = $response->get_data();
+               $updated = get_comment( $comment_id );
+               $this->assertEquals( 'approved', $comment['status'] );
+               $this->assertEquals( 1, $updated->comment_approved );
+               $this->assertEquals( 'some content', $updated->comment_content );
+       }
+
+       public function test_update_comment_date_gmt() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'date_gmt' => '2015-05-07T10:14:25',
+                       'content'  => 'I\'ll be deep in the cold, cold ground before I recognize Missouri.',
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comment = $response->get_data();
+               $updated = get_comment( $this->approved_id );
+               $this->assertEquals( $params['date_gmt'], $comment['date_gmt'] );
+               $this->assertEquals( $params['date_gmt'], mysql_to_rfc3339( $updated->comment_date_gmt ) );
+       }
+
+       public function test_update_comment_invalid_type() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'type' => 'trackback',
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_invalid_type', $response, 404 );
+       }
+
+       public function test_update_comment_with_raw_property() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'content' => array(
+                               'raw' => 'What the heck kind of name is Persephone?',
+                       ),
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $comment = $response->get_data();
+               $updated = get_comment( $this->approved_id );
+               $this->assertEquals( $params['content']['raw'], $updated->comment_content );
+       }
+
+       public function test_update_item_invalid_date() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'content' => rand_str(),
+                       'date'    => rand_str(),
+               );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_update_item_invalid_date_gmt() {
+               wp_set_current_user( $this->admin_id );
+
+               $params = array(
+                       'content'  => rand_str(),
+                       'date_gmt' => rand_str(),
+               );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_update_comment_invalid_id() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'content' => 'Oh, they have the internet on computers now!',
+               );
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 );
+       }
+
+       public function test_update_comment_invalid_permission() {
+               wp_set_current_user( 0 );
+
+               $params = array(
+                       'content' => 'Disco Stu likes disco music.',
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->hold_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+       }
+
+       public function test_update_comment_private_post_invalid_permission() {
+               $private_comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->private_id,
+                       'user_id'          => 0,
+               ));
+
+               wp_set_current_user( $this->subscriber_id );
+
+               $params = array(
+                       'content' => 'Disco Stu likes disco music.',
+               );
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $private_comment_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_update_comment_with_children_link() {
+               wp_set_current_user( $this->admin_id );
+               $comment_id_1 = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               $child_comment = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               // Check if comment 1 does not have the child link.
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertArrayNotHasKey( 'children', $response->get_links() );
+
+               // Change the comment parent.
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%s', $child_comment ) );
+               $request->set_param( 'parent', $comment_id_1 );
+               $request->set_param( 'content', rand_str() );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               // Check if comment 1 now has the child link.
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertArrayHasKey( 'children', $response->get_links() );
+       }
+
+       public function test_delete_item() {
+               wp_set_current_user( $this->admin_id );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ));
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $comment_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( $this->post_id, $data['post'] );
+       }
+
+       public function test_delete_item_skip_trash() {
+               wp_set_current_user( $this->admin_id );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ));
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $comment_id ) );
+               $request['force'] = true;
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( $this->post_id, $data['post'] );
+       }
+
+       public function test_delete_item_already_trashed() {
+               wp_set_current_user( $this->admin_id );
+
+               $comment_id = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ));
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $comment_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_already_trashed', $response, 410 );
+       }
+
+       public function test_delete_comment_invalid_id() {
+               wp_set_current_user( $this->admin_id );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 );
+       }
+
+       public function test_delete_comment_without_permission() {
+               wp_set_current_user( $this->subscriber_id );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+       }
+
+       public function test_delete_child_comment_link() {
+               wp_set_current_user( $this->admin_id );
+               $comment_id_1 = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               $child_comment = $this->factory->comment->create( array(
+                       'comment_approved' => 1,
+                       'comment_parent'   => $comment_id_1,
+                       'comment_post_ID'  => $this->post_id,
+                       'user_id'          => $this->subscriber_id,
+               ) );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%s', $child_comment ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               // Verify children link is gone.
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertArrayNotHasKey( 'children', $response->get_links() );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 18, count( $properties ) );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'author', $properties );
+               $this->assertArrayHasKey( 'author_avatar_urls', $properties );
+               $this->assertArrayHasKey( 'author_email', $properties );
+               $this->assertArrayHasKey( 'author_ip', $properties );
+               $this->assertArrayHasKey( 'author_name', $properties );
+               $this->assertArrayHasKey( 'author_url', $properties );
+               $this->assertArrayHasKey( 'author_user_agent', $properties );
+               $this->assertArrayHasKey( 'content', $properties );
+               $this->assertArrayHasKey( 'date', $properties );
+               $this->assertArrayHasKey( 'date_gmt', $properties );
+               $this->assertArrayHasKey( 'karma', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'parent', $properties );
+               $this->assertArrayHasKey( 'post', $properties );
+               $this->assertArrayHasKey( 'status', $properties );
+               $this->assertArrayHasKey( 'type', $properties );
+       }
+
+       public function test_get_item_schema_show_avatar() {
+               update_option( 'show_avatars', false );
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+
+               $this->assertArrayNotHasKey( 'author_avatar_urls', $properties );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'comment', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $this->approved_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments/' . $this->approved_id );
+               $request->set_body_params(array(
+                       'my_custom_int' => 123,
+                       'content'       => 'abc',
+               ));
+
+               wp_set_current_user( 1 );
+               $this->server->dispatch( $request );
+               $this->assertEquals( 123, get_comment_meta( $this->approved_id, 'my_custom_int', true ) );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
+               $request->set_body_params(array(
+                       'my_custom_int' => 123,
+                       'title'         => 'hello',
+                       'content'       => 'goodbye',
+                       'post'          => $this->post_id,
+               ));
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 123, $response->data['my_custom_int'] );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function test_additional_field_update_errors() {
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'comment', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               wp_set_current_user( $this->admin_id );
+
+               // Check for error on update.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
+               $request->set_body_params(array(
+                       'my_custom_int' => 'returnError',
+                       'content' => 'abc',
+               ));
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object ) {
+               return get_comment_meta( $object['id'], 'my_custom_int', true );
+       }
+
+       public function additional_field_update_callback( $value, $comment ) {
+               if ( 'returnError' === $value ) {
+                       return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
+               }
+               update_comment_meta( $comment->comment_ID, 'my_custom_int', $value );
+       }
+
+       protected function check_comment_data( $data, $context, $links ) {
+               $comment = get_comment( $data['id'] );
+
+               $this->assertEquals( $comment->comment_ID, $data['id'] );
+               $this->assertEquals( $comment->comment_post_ID, $data['post'] );
+               $this->assertEquals( $comment->comment_parent, $data['parent'] );
+               $this->assertEquals( $comment->user_id, $data['author'] );
+               $this->assertEquals( $comment->comment_author, $data['author_name'] );
+               $this->assertEquals( $comment->comment_author_url, $data['author_url'] );
+               $this->assertEquals( wpautop( $comment->comment_content ), $data['content']['rendered'] );
+               $this->assertEquals( mysql_to_rfc3339( $comment->comment_date ), $data['date'] );
+               $this->assertEquals( mysql_to_rfc3339( $comment->comment_date_gmt ), $data['date_gmt'] );
+               $this->assertEquals( get_comment_link( $comment ), $data['link'] );
+               $this->assertContains( 'author_avatar_urls', $data );
+               $this->assertEqualSets( array(
+                       'self',
+                       'collection',
+                       'up',
+               ), array_keys( $links ) );
+
+               if ( 'edit' === $context ) {
+                       $this->assertEquals( $comment->comment_author_email, $data['author_email'] );
+                       $this->assertEquals( $comment->comment_author_IP, $data['author_ip'] );
+                       $this->assertEquals( $comment->comment_agent, $data['author_user_agent'] );
+                       $this->assertEquals( $comment->comment_content, $data['content']['raw'] );
+                       $this->assertEquals( $comment->comment_karma, $data['karma'] );
+               }
+
+               if ( 'edit' !== $context ) {
+                       $this->assertArrayNotHasKey( 'author_email', $data );
+                       $this->assertArrayNotHasKey( 'author_ip', $data );
+                       $this->assertArrayNotHasKey( 'author_user_agent', $data );
+                       $this->assertArrayNotHasKey( 'raw', $data['content'] );
+                       $this->assertArrayNotHasKey( 'karma', $data );
+               }
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestcontrollerphp"></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-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-controller.php                            (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-controller.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,217 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Controller functionality
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Controller extends WP_Test_REST_TestCase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->request = new WP_REST_Request( 'GET', '/wp/v2/testroute', array(
+                       'args'     => array(
+                               'someinteger'     => array(
+                                       'type'        => 'integer',
+                               ),
+                               'someboolean'     => array(
+                                       'type'        => 'boolean',
+                               ),
+                               'somestring'      => array(
+                                       'type'        => 'string',
+                               ),
+                               'someenum'        => array(
+                                       'type'        => 'string',
+                                       'enum'        => array( 'a' ),
+                               ),
+                               'somedate'        => array(
+                                       'type'        => 'string',
+                                       'format'      => 'date-time',
+                               ),
+                               'someemail'       => array(
+                                       'type'        => 'string',
+                                       'format'      => 'email',
+                               ),
+                       ),
+               ));
+       }
+
+       public function test_validate_schema_type_integer() {
+
+               $this->assertTrue(
+                       rest_validate_request_arg( '123', $this->request, 'someinteger' )
+               );
+
+               $this->assertErrorResponse(
+                       'rest_invalid_param',
+                       rest_validate_request_arg( 'abc', $this->request, 'someinteger' )
+               );
+       }
+
+       public function test_validate_schema_type_boolean() {
+
+               $this->assertTrue(
+                       rest_validate_request_arg( true, $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( false, $this->request, 'someboolean' )
+               );
+
+               $this->assertTrue(
+                       rest_validate_request_arg( 'true', $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( 'TRUE', $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( 'false', $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( 'False', $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( '1', $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( '0', $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( 1, $this->request, 'someboolean' )
+               );
+               $this->assertTrue(
+                       rest_validate_request_arg( 0, $this->request, 'someboolean' )
+               );
+
+               // Check sanitize testing.
+               $this->assertEquals( false,
+                       rest_sanitize_request_arg( 'false', $this->request, 'someboolean' )
+               );
+               $this->assertEquals( false,
+                       rest_sanitize_request_arg( '0', $this->request, 'someboolean' )
+               );
+               $this->assertEquals( false,
+                       rest_sanitize_request_arg( 0, $this->request, 'someboolean' )
+               );
+               $this->assertEquals( false,
+                       rest_sanitize_request_arg( 'FALSE', $this->request, 'someboolean' )
+               );
+               $this->assertEquals( true,
+                       rest_sanitize_request_arg( 'true', $this->request, 'someboolean' )
+               );
+               $this->assertEquals( true,
+                       rest_sanitize_request_arg( '1', $this->request, 'someboolean' )
+               );
+               $this->assertEquals( true,
+                       rest_sanitize_request_arg( 1, $this->request, 'someboolean' )
+               );
+               $this->assertEquals( true,
+                       rest_sanitize_request_arg( 'TRUE', $this->request, 'someboolean' )
+               );
+
+               $this->assertErrorResponse(
+                       'rest_invalid_param',
+                       rest_validate_request_arg( '123', $this->request, 'someboolean' )
+               );
+       }
+
+       public function test_validate_schema_type_string() {
+
+               $this->assertTrue(
+                       rest_validate_request_arg( '123', $this->request, 'somestring' )
+               );
+
+               $this->assertErrorResponse(
+                       'rest_invalid_param',
+                       rest_validate_request_arg( array( 'foo' => 'bar' ), $this->request, 'somestring' )
+               );
+       }
+
+       public function test_validate_schema_enum() {
+
+               $this->assertTrue(
+                       rest_validate_request_arg( 'a', $this->request, 'someenum' )
+               );
+
+               $this->assertErrorResponse(
+                       'rest_invalid_param',
+                       rest_validate_request_arg( 'd', $this->request, 'someenum' )
+               );
+       }
+
+       public function test_validate_schema_format_email() {
+
+               $this->assertTrue(
+                       rest_validate_request_arg( 'joe@foo.bar', $this->request, 'someemail' )
+               );
+
+               $this->assertErrorResponse(
+                       'rest_invalid_email',
+                       rest_validate_request_arg( 'd', $this->request, 'someemail' )
+               );
+       }
+
+       public function test_validate_schema_format_date_time() {
+
+               $this->assertTrue(
+                       rest_validate_request_arg( '2010-01-01T12:00:00', $this->request, 'somedate' )
+               );
+
+               $this->assertErrorResponse(
+                       'rest_invalid_date',
+                       rest_validate_request_arg( '2010-18-18T12:00:00', $this->request, 'somedate' )
+               );
+       }
+
+       public function test_get_endpoint_args_for_item_schema_description() {
+               $controller = new WP_REST_Test_Controller();
+               $args       = $controller->get_endpoint_args_for_item_schema();
+               $this->assertEquals( 'A pretty string.', $args['somestring']['description'] );
+               $this->assertFalse( isset( $args['someinteger']['description'] ) );
+       }
+
+       public function test_get_endpoint_args_for_item_schema_arg_options() {
+
+               $controller = new WP_REST_Test_Controller();
+               $args       = $controller->get_endpoint_args_for_item_schema();
+
+               $this->assertFalse( $args['someargoptions']['required'] );
+               $this->assertEquals( '__return_true', $args['someargoptions']['sanitize_callback'] );
+       }
+
+       public function test_get_endpoint_args_for_item_schema_default_value() {
+
+               $controller = new WP_REST_Test_Controller();
+
+               $args = $controller->get_endpoint_args_for_item_schema();
+
+               $this->assertEquals( 'a', $args['somedefault']['default'] );
+       }
+
+       public $rest_the_post_filter_apply_count = 0;
+
+       public function test_get_post() {
+               $post_id = $this->factory()->post->create( array( 'post_title' => 'Original' ) );
+               $controller = new WP_REST_Test_Controller();
+
+               $post = $controller->get_post( $post_id );
+               $this->assertEquals( 'Original', $post->post_title );
+
+               $filter_apply_count = $this->rest_the_post_filter_apply_count;
+               add_filter( 'rest_the_post', array( $this, 'filter_rest_the_post_for_test_get_post' ), 10, 2 );
+               $post = $controller->get_post( $post_id );
+               $this->assertEquals( 'Overridden', $post->post_title );
+               $this->assertEquals( 1 + $filter_apply_count, $this->rest_the_post_filter_apply_count );
+       }
+
+       public function filter_rest_the_post_for_test_get_post( $post, $post_id ) {
+               $this->assertInstanceOf( 'WP_Post', $post );
+               $this->assertInternalType( 'int', $post_id );
+               $post->post_title = 'Overridden';
+               $this->rest_the_post_filter_apply_count += 1;
+               return $post;
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestpagescontrollerphp"></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-pages-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-pages-controller.php                              (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-pages-controller.php        2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,407 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Posts_Controller functionality, used for
+ * Pages
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Pages_Controller extends WP_Test_REST_Post_Type_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->editor_id = $this->factory->user->create( array(
+                       'role' => 'editor',
+               ) );
+               $this->author_id = $this->factory->user->create( array(
+                       'role' => 'author',
+               ) );
+
+               $this->has_setup_template = false;
+               add_filter( 'theme_page_templates', array( $this, 'filter_theme_page_templates' ) );
+       }
+
+       public function test_register_routes() {
+
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages/' . $page_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'after',
+                       'author',
+                       'author_exclude',
+                       'before',
+                       'context',
+                       'exclude',
+                       'filter',
+                       'include',
+                       'menu_order',
+                       'offset',
+                       'order',
+                       'orderby',
+                       'page',
+                       'parent',
+                       'parent_exclude',
+                       'per_page',
+                       'search',
+                       'slug',
+                       'status',
+                       ), $keys );
+       }
+
+       public function test_get_items() {
+
+       }
+
+       public function test_get_items_parent_query() {
+               $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1 ) );
+               // No parent
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               // Filter to parent
+               $request->set_param( 'parent', $id1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $id2, $data[0]['id'] );
+       }
+
+       public function test_get_items_parents_query() {
+               $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1 ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id3 ) );
+               // No parent
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 4, count( $data ) );
+               // Filter to parents
+               $request->set_param( 'parent', array( $id1, $id3 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEqualSets( array( $id2, $id4 ), wp_list_pluck( $data, 'id' ) );
+       }
+
+       public function test_get_items_parent_exclude_query() {
+               $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
+               $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1 ) );
+               // No parent
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               // Filter to parent
+               $request->set_param( 'parent_exclude', $id1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $id1, $data[0]['id'] );
+       }
+
+       public function test_get_items_menu_order_query() {
+               $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 2 ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 3 ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 1 ) );
+               // No parent
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEqualSets( array( $id1, $id2, $id3, $id4 ), wp_list_pluck( $data, 'id' ) );
+               // Filter to menu_order
+               $request->set_param( 'menu_order', 1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEqualSets( array( $id4 ), wp_list_pluck( $data, 'id' ) );
+               // Order by menu order
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'orderby', 'menu_order' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $id1, $data[0]['id'] );
+               $this->assertEquals( $id4, $data[1]['id'] );
+               $this->assertEquals( $id2, $data[2]['id'] );
+               $this->assertEquals( $id3, $data[3]['id'] );
+       }
+
+       public function test_get_items_min_max_pages_query() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $request->set_param( 'per_page', 0 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+               $data = $response->get_data();
+               // Safe format for 4.4 and 4.5 https://core.trac.wordpress.org/ticket/35028
+               $first_error = array_shift( $data['data']['params'] );
+               $this->assertContains( 'per_page must be between 1 (inclusive) and 100 (inclusive)', $first_error );
+               $request->set_param( 'per_page', 101 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+               $data = $response->get_data();
+               $first_error = array_shift( $data['data']['params'] );
+               $this->assertContains( 'per_page must be between 1 (inclusive) and 100 (inclusive)', $first_error );
+       }
+
+       public function test_get_items_private_filter_query_var() {
+               // Private query vars inaccessible to unauthorized users
+               wp_set_current_user( 0 );
+               $page_id = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
+               $draft_id = $this->factory->post->create( array( 'post_status' => 'draft', 'post_type' => 'page' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $request->set_param( 'filter', array( 'post_status' => 'draft' ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $page_id, $data[0]['id'] );
+               // But they are accessible to authorized users
+               wp_set_current_user( $this->editor_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $draft_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_invalid_date() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $request->set_param( 'after', rand_str() );
+               $request->set_param( 'before', rand_str() );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_items_valid_date() {
+               $post1 = $this->factory->post->create( array( 'post_date' => '2016-01-15T00:00:00Z', 'post_type' => 'page' ) );
+               $post2 = $this->factory->post->create( array( 'post_date' => '2016-01-16T00:00:00Z', 'post_type' => 'page' ) );
+               $post3 = $this->factory->post->create( array( 'post_date' => '2016-01-17T00:00:00Z', 'post_type' => 'page' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $request->set_param( 'after', '2016-01-15T00:00:00Z' );
+               $request->set_param( 'before', '2016-01-17T00:00:00Z' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $post2, $data[0]['id'] );
+       }
+
+       public function test_get_item() {
+
+       }
+
+       public function test_get_item_invalid_post_type() {
+               $post_id = $this->factory->post->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages/' . $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 404, $response->get_status() );
+       }
+
+       public function test_create_item() {
+
+       }
+
+       public function test_create_item_with_template() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/pages' );
+               $params = $this->set_post_data( array(
+                       'template'       => 'page-my-test-template.php',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( 'page-my-test-template.php', $data['template'] );
+               $this->assertEquals( 'page-my-test-template.php', get_page_template_slug( $new_post->ID ) );
+       }
+
+       public function test_create_page_with_parent() {
+               $page_id = $this->factory->post->create( array(
+                       'type' => 'page',
+               ) );
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/pages' );
+               $params = $this->set_post_data( array(
+                       'parent' => $page_id,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 201, $response->get_status() );
+
+               $links = $response->get_links();
+               $this->assertArrayHasKey( 'up', $links );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( $page_id, $data['parent'] );
+               $this->assertEquals( $page_id, $new_post->post_parent );
+       }
+
+       public function test_create_page_with_invalid_parent() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/pages' );
+               $params = $this->set_post_data( array(
+                       'parent' => -1,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 400 );
+       }
+
+       public function test_update_item() {
+
+       }
+
+       public function test_delete_item() {
+
+       }
+
+       public function test_prepare_item() {
+
+       }
+
+       public function test_get_pages_params() {
+               $this->factory->post->create_many( 8, array(
+                       'post_type' => 'page',
+               ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
+               $request->set_query_params( array(
+                       'page'           => 2,
+                       'per_page'       => 4,
+               ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $headers = $response->get_headers();
+               $this->assertEquals( 8, $headers['X-WP-Total'] );
+               $this->assertEquals( 2, $headers['X-WP-TotalPages'] );
+
+               $all_data = $response->get_data();
+               $this->assertEquals( 4, count( $all_data ) );
+               foreach ( $all_data as $post ) {
+                       $this->assertEquals( 'page', $post['type'] );
+               }
+       }
+
+       public function test_update_page_menu_order() {
+
+               $page_id = $this->factory->post->create( array(
+                       'post_type' => 'page',
+               ) );
+
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/pages/%d', $page_id ) );
+
+               $request->set_body_params( array(
+                       'menu_order' => 1,
+               ) );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( 1, $new_data['menu_order'] );
+       }
+
+       public function test_update_page_menu_order_to_zero() {
+
+               $page_id = $this->factory->post->create( array(
+                       'post_type'  => 'page',
+                       'menu_order' => 1,
+               ) );
+
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/pages/%d', $page_id ) );
+
+               $request->set_body_params(array(
+                       'menu_order' => 0,
+               ));
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( 0, $new_data['menu_order'] );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 21, count( $properties ) );
+               $this->assertArrayHasKey( 'author', $properties );
+               $this->assertArrayHasKey( 'comment_status', $properties );
+               $this->assertArrayHasKey( 'content', $properties );
+               $this->assertArrayHasKey( 'date', $properties );
+               $this->assertArrayHasKey( 'date_gmt', $properties );
+               $this->assertArrayHasKey( 'guid', $properties );
+               $this->assertArrayHasKey( 'excerpt', $properties );
+               $this->assertArrayHasKey( 'featured_media', $properties );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'menu_order', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'modified', $properties );
+               $this->assertArrayHasKey( 'modified_gmt', $properties );
+               $this->assertArrayHasKey( 'parent', $properties );
+               $this->assertArrayHasKey( 'ping_status', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'status', $properties );
+               $this->assertArrayHasKey( 'template', $properties );
+               $this->assertArrayHasKey( 'title', $properties );
+               $this->assertArrayHasKey( 'type', $properties );
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+               remove_filter( 'theme_page_templates', array( $this, 'filter_theme_page_templates' ) );
+       }
+
+       public function filter_theme_page_templates( $page_templates ) {
+               return array(
+                       'page-my-test-template.php' => 'My Test Template',
+               );
+               return $page_templates;
+       }
+
+       protected function set_post_data( $args = array() ) {
+               $args = parent::set_post_data( $args );
+               $args['type'] = 'page';
+               return $args;
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestpostmetafieldsphp"></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-post-meta-fields.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php                              (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php        2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,636 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Posts meta functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase {
+       public function setUp() {
+               parent::setUp();
+
+               register_meta( 'post', 'test_single', array(
+                       'show_in_rest' => true,
+                       'single' => true,
+               ));
+               register_meta( 'post', 'test_multi', array(
+                       'show_in_rest' => true,
+                       'single' => false,
+               ));
+               register_meta( 'post', 'test_bad_auth', array(
+                       'show_in_rest' => true,
+                       'single' => true,
+                       'auth_callback' => '__return_false',
+               ));
+               register_meta( 'post', 'test_bad_auth_multi', array(
+                       'show_in_rest' => true,
+                       'single' => false,
+                       'auth_callback' => '__return_false',
+               ));
+               register_meta( 'post', 'test_no_rest', array() );
+               register_meta( 'post', 'test_rest_disabled', array(
+                       'show_in_rest' => false,
+               ));
+               register_meta( 'post', 'test_custom_schema', array(
+                       'single' => true,
+                       'type' => 'integer',
+                       'show_in_rest' => array(
+                               'schema' => array(
+                                       'type' => 'number',
+                               ),
+                       ),
+               ));
+               register_meta( 'post', 'test_invalid_type', array(
+                       'single' => true,
+                       'type' => false,
+                       'show_in_rest' => true,
+               ));
+
+               /** @var WP_REST_Server $wp_rest_server */
+               global $wp_rest_server;
+               $this->server = $wp_rest_server = new Spy_REST_Server;
+               do_action( 'rest_api_init' );
+
+               $this->post_id = $this->factory->post->create();
+       }
+
+       protected function grant_write_permission() {
+               // Ensure we have write permission.
+               $user = $this->factory->user->create( array(
+                       'role' => 'editor',
+               ));
+               wp_set_current_user( $user );
+       }
+
+       public function test_get_value() {
+               add_post_meta( $this->post_id, 'test_single', 'testvalue' );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertArrayHasKey( 'meta', $data );
+
+               $meta = (array) $data['meta'];
+               $this->assertArrayHasKey( 'test_single', $meta );
+               $this->assertEquals( 'testvalue', $meta['test_single'] );
+       }
+
+       /**
+        * @depends test_get_value
+        */
+       public function test_get_multi_value() {
+               add_post_meta( $this->post_id, 'test_multi', 'value1' );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertArrayHasKey( 'test_multi', $meta );
+               $this->assertInternalType( 'array', $meta['test_multi'] );
+               $this->assertContains( 'value1', $meta['test_multi'] );
+
+               // Check after an update.
+               add_post_meta( $this->post_id, 'test_multi', 'value2' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertContains( 'value1', $meta['test_multi'] );
+               $this->assertContains( 'value2', $meta['test_multi'] );
+       }
+
+       /**
+        * @depends test_get_value
+        */
+       public function test_get_unregistered() {
+               add_post_meta( $this->post_id, 'test_unregistered', 'value1' );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertArrayNotHasKey( 'test_unregistered', $meta );
+       }
+
+       /**
+        * @depends test_get_value
+        */
+       public function test_get_registered_no_api_access() {
+               add_post_meta( $this->post_id, 'test_no_rest', 'for_the_wicked' );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertArrayNotHasKey( 'test_no_rest', $meta );
+       }
+
+       /**
+        * @depends test_get_value
+        */
+       public function test_get_registered_api_disabled() {
+               add_post_meta( $this->post_id, 'test_rest_disabled', 'sleepless_nights' );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertArrayNotHasKey( 'test_rest_disabled', $meta );
+       }
+
+       public function test_get_value_types() {
+               register_meta( 'post', 'test_string', array(
+                       'show_in_rest' => true,
+                       'single' => true,
+                       'type' => 'string',
+               ));
+               register_meta( 'post', 'test_number', array(
+                       'show_in_rest' => true,
+                       'single' => true,
+                       'type' => 'number',
+               ));
+               register_meta( 'post', 'test_bool', array(
+                       'show_in_rest' => true,
+                       'single' => true,
+                       'type' => 'boolean',
+               ));
+
+               /** @var WP_REST_Server $wp_rest_server */
+               global $wp_rest_server;
+               $this->server = $wp_rest_server = new Spy_REST_Server;
+               do_action( 'rest_api_init' );
+
+               add_post_meta( $this->post_id, 'test_string', 42 );
+               add_post_meta( $this->post_id, 'test_number', '42' );
+               add_post_meta( $this->post_id, 'test_bool', 1 );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+
+               $this->assertArrayHasKey( 'test_string', $meta );
+               $this->assertInternalType( 'string', $meta['test_string'] );
+               $this->assertSame( '42', $meta['test_string'] );
+
+               $this->assertArrayHasKey( 'test_number', $meta );
+               $this->assertInternalType( 'float', $meta['test_number'] );
+               $this->assertSame( 42.0, $meta['test_number'] );
+
+               $this->assertArrayHasKey( 'test_bool', $meta );
+               $this->assertInternalType( 'boolean', $meta['test_bool'] );
+               $this->assertSame( true, $meta['test_bool'] );
+       }
+
+       /**
+        * @depends test_get_value
+        */
+       public function test_set_value() {
+               // Ensure no data exists currently.
+               $values = get_post_meta( $this->post_id, 'test_single', false );
+               $this->assertEmpty( $values );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_single' => 'test_value',
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $meta = get_post_meta( $this->post_id, 'test_single', false );
+               $this->assertNotEmpty( $meta );
+               $this->assertCount( 1, $meta );
+               $this->assertEquals( 'test_value', $meta[0] );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertArrayHasKey( 'test_single', $meta );
+               $this->assertEquals( 'test_value', $meta['test_single'] );
+       }
+
+       /**
+        * @depends test_get_value
+        */
+       public function test_set_duplicate_single_value() {
+               // Start with an existing metakey and value.
+               $values = update_post_meta( $this->post_id, 'test_single', 'test_value' );
+               $this->assertEquals( 'test_value', get_post_meta( $this->post_id, 'test_single', true ) );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_single' => 'test_value',
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $meta = get_post_meta( $this->post_id, 'test_single', true );
+               $this->assertNotEmpty( $meta );
+               $this->assertEquals( 'test_value', $meta );
+
+               $data = $response->get_data();
+               $meta = (array) $data['meta'];
+               $this->assertArrayHasKey( 'test_single', $meta );
+               $this->assertEquals( 'test_value', $meta['test_single'] );
+       }
+
+       /**
+        * @depends test_set_value
+        */
+       public function test_set_value_unauthenticated() {
+               $data = array(
+                       'meta' => array(
+                               'test_single' => 'test_value',
+                       ),
+               );
+
+               wp_set_current_user( 0 );
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+
+               // Check that the value wasn't actually updated.
+               $this->assertEmpty( get_post_meta( $this->post_id, 'test_single', false ) );
+       }
+
+       /**
+        * @depends test_set_value
+        */
+       public function test_set_value_blocked() {
+               $data = array(
+                       'meta' => array(
+                               'test_bad_auth' => 'test_value',
+                       ),
+               );
+
+               $this->grant_write_permission();
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+               $this->assertEmpty( get_post_meta( $this->post_id, 'test_bad_auth', false ) );
+       }
+
+       /**
+        * @depends test_set_value
+        */
+       public function test_set_value_db_error() {
+               $data = array(
+                       'meta' => array(
+                               'test_single' => 'test_value',
+                       ),
+               );
+
+               $this->grant_write_permission();
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               /**
+                * Disable showing error as the below is going to intentionally
+                * trigger a DB error.
+                */
+               global $wpdb;
+               $wpdb->suppress_errors = true;
+               add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+               $response = $this->server->dispatch( $request );
+               remove_filter( 'query', array( $this, 'error_insert_query' ) );
+               $wpdb->show_errors = true;
+       }
+
+       public function test_set_value_multiple() {
+               // Ensure no data exists currently.
+               $values = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertEmpty( $values );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_multi' => array( 'val1' ),
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $meta = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertNotEmpty( $meta );
+               $this->assertCount( 1, $meta );
+               $this->assertEquals( 'val1', $meta[0] );
+
+               // Add another value.
+               $data = array(
+                       'meta' => array(
+                               'test_multi' => array( 'val1', 'val2' ),
+                       ),
+               );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $meta = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertNotEmpty( $meta );
+               $this->assertCount( 2, $meta );
+               $this->assertContains( 'val1', $meta );
+               $this->assertContains( 'val2', $meta );
+       }
+
+       /**
+        * Test removing only one item with duplicate items.
+        */
+       public function test_set_value_remove_one() {
+               add_post_meta( $this->post_id, 'test_multi', 'c' );
+               add_post_meta( $this->post_id, 'test_multi', 'n' );
+               add_post_meta( $this->post_id, 'test_multi', 'n' );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_multi' => array( 'c', 'n' ),
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $meta = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertNotEmpty( $meta );
+               $this->assertCount( 2, $meta );
+               $this->assertContains( 'c', $meta );
+               $this->assertContains( 'n', $meta );
+       }
+
+       /**
+        * @depends test_set_value_multiple
+        */
+       public function test_set_value_multiple_unauthenticated() {
+               // Ensure no data exists currently.
+               $values = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertEmpty( $values );
+
+               wp_set_current_user( 0 );
+
+               $data = array(
+                       'meta' => array(
+                               'test_multi' => array( 'val1' ),
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+
+               $meta = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertEmpty( $meta );
+       }
+
+       /**
+        * @depends test_set_value_multiple
+        */
+       public function test_set_value_multiple_blocked() {
+               $data = array(
+                       'meta' => array(
+                               'test_bad_auth_multi' => array( 'test_value' ),
+                       ),
+               );
+
+               $this->grant_write_permission();
+
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+               $this->assertEmpty( get_post_meta( $this->post_id, 'test_bad_auth_multi', false ) );
+       }
+
+       public function test_add_multi_value_db_error() {
+               // Ensure no data exists currently.
+               $values = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertEmpty( $values );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_multi' => array( 'val1' ),
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               /**
+                * Disable showing error as the below is going to intentionally
+                * trigger a DB error.
+                */
+               global $wpdb;
+               $wpdb->suppress_errors = true;
+               add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+               $response = $this->server->dispatch( $request );
+               remove_filter( 'query', array( $this, 'error_insert_query' ) );
+               $wpdb->show_errors = true;
+
+               $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+       }
+
+       public function test_remove_multi_value_db_error() {
+               add_post_meta( $this->post_id, 'test_multi', 'val1' );
+               $values = get_post_meta( $this->post_id, 'test_multi', false );
+               $this->assertEquals( array( 'val1' ), $values );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_multi' => array(),
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               /**
+                * Disable showing error as the below is going to intentionally
+                * trigger a DB error.
+                */
+               global $wpdb;
+               $wpdb->suppress_errors = true;
+               add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+               $response = $this->server->dispatch( $request );
+               remove_filter( 'query', array( $this, 'error_delete_query' ) );
+               $wpdb->show_errors = true;
+
+               $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+       }
+
+       public function test_delete_value() {
+               add_post_meta( $this->post_id, 'test_single', 'val1' );
+               $current = get_post_meta( $this->post_id, 'test_single', true );
+               $this->assertEquals( 'val1', $current );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_single' => null,
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $meta = get_post_meta( $this->post_id, 'test_single', false );
+               $this->assertEmpty( $meta );
+       }
+
+       /**
+        * @depends test_delete_value
+        */
+       public function test_delete_value_blocked() {
+               add_post_meta( $this->post_id, 'test_bad_auth', 'val1' );
+               $current = get_post_meta( $this->post_id, 'test_bad_auth', true );
+               $this->assertEquals( 'val1', $current );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_bad_auth' => null,
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+
+               $meta = get_post_meta( $this->post_id, 'test_bad_auth', true );
+               $this->assertEquals( 'val1', $meta );
+       }
+
+       /**
+        * @depends test_delete_value
+        */
+       public function test_delete_value_db_error() {
+               add_post_meta( $this->post_id, 'test_single', 'val1' );
+               $current = get_post_meta( $this->post_id, 'test_single', true );
+               $this->assertEquals( 'val1', $current );
+
+               $this->grant_write_permission();
+
+               $data = array(
+                       'meta' => array(
+                               'test_single' => null,
+                       ),
+               );
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( $data );
+               /**
+                * Disable showing error as the below is going to intentionally
+                * trigger a DB error.
+                */
+               global $wpdb;
+               $wpdb->suppress_errors = true;
+               add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+               $response = $this->server->dispatch( $request );
+               remove_filter( 'query', array( $this, 'error_delete_query' ) );
+               $wpdb->show_errors = true;
+
+               $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+       }
+
+       public function test_get_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $schema = $data['schema'];
+
+               $this->assertArrayHasKey( 'meta', $schema['properties'] );
+               $meta_schema = $schema['properties']['meta']['properties'];
+
+               $this->assertArrayHasKey( 'test_single', $meta_schema );
+               $this->assertEquals( 'string', $meta_schema['test_single']['type'] );
+
+               $this->assertArrayHasKey( 'test_multi', $meta_schema );
+               $this->assertEquals( 'array', $meta_schema['test_multi']['type'] );
+               $this->assertArrayHasKey( 'items', $meta_schema['test_multi'] );
+               $this->assertEquals( 'string', $meta_schema['test_multi']['items']['type'] );
+
+               $this->assertArrayHasKey( 'test_custom_schema', $meta_schema );
+               $this->assertEquals( 'number', $meta_schema['test_custom_schema']['type'] );
+
+               $this->assertArrayNotHasKey( 'test_no_rest', $meta_schema );
+               $this->assertArrayNotHasKey( 'test_rest_disabled', $meta_schema );
+               $this->assertArrayNotHasKey( 'test_invalid_type', $meta_schema );
+       }
+
+       /**
+        * Internal function used to disable an insert query which
+        * will trigger a wpdb error for testing purposes.
+        */
+       public function error_insert_query( $query ) {
+               if ( strpos( $query, 'INSERT' ) === 0 ) {
+                       $query = '],';
+               }
+               return $query;
+       }
+
+       /**
+        * Internal function used to disable an insert query which
+        * will trigger a wpdb error for testing purposes.
+        */
+       public function error_delete_query( $query ) {
+               if ( strpos( $query, 'DELETE' ) === 0 ) {
+                       $query = '],';
+               }
+               return $query;
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestpoststatusescontrollerphp"></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-post-statuses-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php                              (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php        2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,194 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Posts_Statuses_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Post_Statuses_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/statuses', $routes );
+               $this->assertArrayHasKey( '/wp/v2/statuses/(?P<status>[\w-]+)', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'embed', 'view', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses/publish' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'embed', 'view', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_get_items() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $statuses = get_post_stati( array( 'public' => true ), 'objects' );
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'publish', $data['publish']['slug'] );
+       }
+
+       public function test_get_items_logged_in() {
+               $user_id = $this->factory->user->create( array( 'role' => 'author' ) );
+               wp_set_current_user( $user_id );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertEquals( 6, count( $data ) );
+               $this->assertEqualSets( array(
+                       'publish',
+                       'private',
+                       'pending',
+                       'draft',
+                       'trash',
+                       'future',
+               ), array_keys( $data ) );
+       }
+
+       public function test_get_items_unauthorized_context() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
+       }
+
+       public function test_get_item() {
+               $user_id = $this->factory->user->create( array( 'role' => 'author' ) );
+               wp_set_current_user( $user_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/publish' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->check_post_status_object_response( $response );
+       }
+
+       public function test_get_item_invalid_status() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/invalid' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_status_invalid', $response, 404 );
+       }
+
+       public function test_get_item_invalid_access() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/draft' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read_status', $response, 401 );
+       }
+
+       public function test_get_item_invalid_internal() {
+               $user_id = $this->factory->user->create();
+               wp_set_current_user( $user_id );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/inherit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read_status', $response, 403 );
+       }
+
+       public function test_create_item() {
+               /** Post statuses can't be created **/
+       }
+
+       public function test_update_item() {
+               /** Post statuses can't be updated **/
+       }
+
+       public function test_delete_item() {
+               /** Post statuses can't be deleted **/
+       }
+
+       public function test_prepare_item() {
+               $obj = get_post_status_object( 'publish' );
+               $endpoint = new WP_REST_Post_Statuses_Controller;
+               $request = new WP_REST_Request;
+               $request->set_param( 'context', 'edit' );
+               $data = $endpoint->prepare_item_for_response( $obj, $request );
+               $this->check_post_status_obj( $obj, $data->get_data(), $data->get_links() );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 7, count( $properties ) );
+               $this->assertArrayHasKey( 'name', $properties );
+               $this->assertArrayHasKey( 'private', $properties );
+               $this->assertArrayHasKey( 'protected', $properties );
+               $this->assertArrayHasKey( 'public', $properties );
+               $this->assertArrayHasKey( 'queryable', $properties );
+               $this->assertArrayHasKey( 'show_in_list', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'status', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/publish' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object ) {
+               return 123;
+       }
+
+       protected function check_post_status_obj( $status_obj, $data, $links ) {
+               $this->assertEquals( $status_obj->label, $data['name'] );
+               $this->assertEquals( $status_obj->private, $data['private'] );
+               $this->assertEquals( $status_obj->protected, $data['protected'] );
+               $this->assertEquals( $status_obj->public, $data['public'] );
+               $this->assertEquals( $status_obj->publicly_queryable, $data['queryable'] );
+               $this->assertEquals( $status_obj->show_in_admin_all_list, $data['show_in_list'] );
+               $this->assertEquals( $status_obj->name, $data['slug'] );
+               $this->assertEqualSets( array(
+                       'archives',
+               ), array_keys( $links ) );
+       }
+
+       protected function check_post_status_object_response( $response ) {
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $obj = get_post_status_object( 'publish' );
+               $this->check_post_status_obj( $obj, $data, $response->get_links() );
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestposttypescontrollerphp"></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-post-types-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-post-types-controller.php                         (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-post-types-controller.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,182 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Posts_Types_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Post_Types_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/types', $routes );
+               $this->assertArrayHasKey( '/wp/v2/types/(?P<type>[\w-]+)', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types/post' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_get_items() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types' );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
+               $this->assertEquals( count( $post_types ), count( $data ) );
+               $this->assertEquals( $post_types['post']->name, $data['post']['slug'] );
+               $this->check_post_type_obj( 'view', $post_types['post'], $data['post'], $data['post']['_links'] );
+               $this->assertEquals( $post_types['page']->name, $data['page']['slug'] );
+               $this->check_post_type_obj( 'view', $post_types['page'], $data['page'], $data['page']['_links'] );
+               $this->assertFalse( isset( $data['revision'] ) );
+       }
+
+       public function test_get_items_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
+       }
+
+       public function test_get_item() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
+               $response = $this->server->dispatch( $request );
+               $this->check_post_type_object_response( 'view', $response );
+       }
+
+       public function test_get_item_invalid_type() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types/invalid' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_type_invalid', $response, 404 );
+       }
+
+       public function test_get_item_edit_context() {
+               $editor_id = $this->factory->user->create( array( 'role' => 'editor' ) );
+               wp_set_current_user( $editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->check_post_type_object_response( 'edit', $response );
+       }
+
+       public function test_get_item_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_create_item() {
+               /** Post types can't be created **/
+       }
+
+       public function test_update_item() {
+               /** Post types can't be updated **/
+       }
+
+       public function test_delete_item() {
+               /** Post types can't be deleted **/
+       }
+
+       public function test_prepare_item() {
+               $obj = get_post_type_object( 'post' );
+               $endpoint = new WP_REST_Post_Types_Controller;
+               $request = new WP_REST_Request;
+               $request->set_param( 'context', 'edit' );
+               $response = $endpoint->prepare_item_for_response( $obj, $request );
+               $this->check_post_type_obj( 'edit', $obj, $response->get_data(), $response->get_links() );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 6, count( $properties ) );
+               $this->assertArrayHasKey( 'capabilities', $properties );
+               $this->assertArrayHasKey( 'description', $properties );
+               $this->assertArrayHasKey( 'hierarchical', $properties );
+               $this->assertArrayHasKey( 'labels', $properties );
+               $this->assertArrayHasKey( 'name', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'type', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types/schema' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object ) {
+               return 123;
+       }
+
+       protected function check_post_type_obj( $context, $post_type_obj, $data, $links ) {
+               $this->assertEquals( $post_type_obj->label, $data['name'] );
+               $this->assertEquals( $post_type_obj->name, $data['slug'] );
+               $this->assertEquals( $post_type_obj->description, $data['description'] );
+               $this->assertEquals( $post_type_obj->hierarchical, $data['hierarchical'] );
+
+               $links = test_rest_expand_compact_links( $links );
+               $this->assertEquals( rest_url( 'wp/v2/types' ), $links['collection'][0]['href'] );
+               $this->assertArrayHasKey( 'https://api.w.org/items', $links );
+               if ( 'edit' === $context ) {
+                       $this->assertEquals( $post_type_obj->cap, $data['capabilities'] );
+                       $this->assertEquals( $post_type_obj->labels, $data['labels'] );
+               } else {
+                       $this->assertFalse( isset( $data['capabilities'] ) );
+                       $this->assertFalse( isset( $data['labels'] ) );
+               }
+       }
+
+       protected function check_post_type_object_response( $context, $response ) {
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $obj = get_post_type_object( 'post' );
+               $this->check_post_type_obj( $context, $obj, $data, $response->get_links() );
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestpostscontrollerphp"></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-posts-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php                              (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-posts-controller.php        2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1958 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Posts_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+
+               $this->post_id = $this->factory->post->create();
+
+               $this->editor_id = $this->factory->user->create( array(
+                       'role' => 'editor',
+               ) );
+               $this->author_id = $this->factory->user->create( array(
+                       'role' => 'author',
+               ) );
+               $this->contributor_id = $this->factory->user->create( array(
+                       'role' => 'contributor',
+               ) );
+
+               register_post_type( 'youseeme', array( 'supports' => array(), 'show_in_rest' => true ) );
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+
+               $this->assertArrayHasKey( '/wp/v2/posts', $routes );
+               $this->assertCount( 2, $routes['/wp/v2/posts'] );
+               $this->assertArrayHasKey( '/wp/v2/posts/(?P<id>[\d]+)', $routes );
+               $this->assertCount( 3, $routes['/wp/v2/posts/(?P<id>[\d]+)'] );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'after',
+                       'author',
+                       'author_exclude',
+                       'before',
+                       'categories',
+                       'context',
+                       'exclude',
+                       'filter',
+                       'include',
+                       'offset',
+                       'order',
+                       'orderby',
+                       'page',
+                       'per_page',
+                       'search',
+                       'slug',
+                       'status',
+                       'sticky',
+                       'tags',
+                       ), $keys );
+       }
+
+       public function test_get_items() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_get_posts_response( $response );
+       }
+
+       /**
+        * A valid query that returns 0 results should return an empty JSON list.
+        *
+        * @issue 862
+        */
+       public function test_get_items_empty_query() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_query_params( array(
+                       'filter' => array( 'year' => 2008 ),
+               ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( array(), $response->get_data() );
+               $this->assertEquals( 200, $response->get_status() );
+       }
+
+       public function test_get_items_author_query() {
+               $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
+               $this->factory->post->create( array( 'post_author' => $this->author_id ) );
+               // All 3 posts
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 3, count( $response->get_data() ) );
+               // 2 of 3 posts
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'author', array( $this->editor_id, $this->author_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEqualSets( array( $this->editor_id, $this->author_id ), wp_list_pluck( $data, 'author' ) );
+               // 1 of 3 posts
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'author', $this->editor_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $this->editor_id, $data[0]['author'] );
+       }
+
+       public function test_get_items_author_exclude_query() {
+               $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
+               $this->factory->post->create( array( 'post_author' => $this->author_id ) );
+               // All 3 posts
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 3, count( $response->get_data() ) );
+               // 1 of 3 posts
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'author_exclude', array( $this->editor_id, $this->author_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertNotEquals( $this->editor_id, $data[0]['author'] );
+               $this->assertNotEquals( $this->author_id, $data[0]['author'] );
+               // 2 of 3 posts
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'author_exclude', $this->editor_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertNotEquals( $this->editor_id, $data[0]['author'] );
+               $this->assertNotEquals( $this->editor_id, $data[1]['author'] );
+       }
+
+       public function test_get_items_include_query() {
+               $id1 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               // Orderby=>desc
+               $request->set_param( 'include', array( $id1, $id3 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id3, $data[0]['id'] );
+               // Orderby=>include
+               $request->set_param( 'orderby', 'include' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id1, $data[0]['id'] );
+       }
+
+       public function test_get_items_exclude_query() {
+               $id1 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+               $request->set_param( 'exclude', array( $id2 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+       }
+
+       public function test_get_items_search_query() {
+               for ( $i = 0;  $i < 5;  $i++ ) {
+                       $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               }
+               $this->factory->post->create( array( 'post_title' => 'Search Result', 'post_status' => 'publish' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 7, count( $response->get_data() ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'search', 'Search Result' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Search Result', $data[0]['title']['rendered'] );
+       }
+
+       public function test_get_items_slug_query() {
+               $this->factory->post->create( array( 'post_title' => 'Apple', 'post_status' => 'publish' ) );
+               $this->factory->post->create( array( 'post_title' => 'Banana', 'post_status' => 'publish' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'slug', 'apple' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['title']['rendered'] );
+       }
+
+       public function test_get_items_status_query() {
+               wp_set_current_user( 0 );
+               $this->factory->post->create( array( 'post_status' => 'draft' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'status', 'publish' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 1, count( $response->get_data() ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'status', 'draft' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'status', 'draft' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 1, count( $response->get_data() ) );
+       }
+
+       public function test_get_items_status_without_permissions() {
+               $draft_id = $this->factory->post->create( array(
+                       'post_status' => 'draft',
+               ) );
+               wp_set_current_user( 0 );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $all_data = $response->get_data();
+               foreach ( $all_data as $post ) {
+                       $this->assertNotEquals( $draft_id, $post['id'] );
+               }
+       }
+
+       public function test_get_items_order_and_orderby() {
+               $this->factory->post->create( array( 'post_title' => 'Apple Pie', 'post_status' => 'publish' ) );
+               $this->factory->post->create( array( 'post_title' => 'Apple Sauce', 'post_status' => 'publish' ) );
+               $this->factory->post->create( array( 'post_title' => 'Apple Cobbler', 'post_status' => 'publish' ) );
+               $this->factory->post->create( array( 'post_title' => 'Apple Coffee Cake', 'post_status' => 'publish' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'search', 'Apple' );
+               // order defaults to 'desc'
+               $request->set_param( 'orderby', 'title' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'Apple Sauce', $data[0]['title']['rendered'] );
+               // order=>asc
+               $request->set_param( 'order', 'asc' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'Apple Cobbler', $data[0]['title']['rendered'] );
+       }
+
+       public function test_get_items_with_orderby_relevance() {
+               $this->factory->post->create( array( 'post_title' => 'Title is more relevant', 'post_content' => 'Content is', 'post_status' => 'publish' ) );
+               $this->factory->post->create( array( 'post_title' => 'Title is', 'post_content' => 'Content is less relevant', 'post_status' => 'publish' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'orderby', 'relevance' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_search_term_defined', $response, 400 );
+       }
+
+       public function test_get_items_ignore_sticky_posts_by_default() {
+               $this->markTestSkipped( 'Broken, see https://github.com/WP-API/WP-API/issues/2210' );
+               $post_id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_date' => '2015-01-01 12:00:00', 'post_date_gmt' => '2015-01-01 12:00:00' ) );
+               $post_id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_date' => '2015-01-02 12:00:00', 'post_date_gmt' => '2015-01-02 12:00:00' ) );
+               $post_id3 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_date' => '2015-01-03 12:00:00', 'post_date_gmt' => '2015-01-03 12:00:00' ) );
+               stick_post( $post_id2 );
+
+               // No stickies by default
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( array( $this->post_id, $post_id3, $post_id2, $post_id1 ), wp_list_pluck( $data, 'id' ) );
+
+               // Permit stickies
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'filter', array( 'ignore_sticky_posts' => false ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( array( $post_id2, $this->post_id, $post_id3, $post_id1 ), wp_list_pluck( $data, 'id' ) );
+       }
+
+       public function test_get_items_offset_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'offset', 1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 3, $response->get_data() );
+               // 'offset' works with 'per_page'
+               $request->set_param( 'per_page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+               // 'offset' takes priority over 'page'
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+       }
+
+       public function test_get_items_tags_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $tag = wp_insert_term( 'My Tag', 'post_tag' );
+
+               wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'tags', array( $tag['term_id'] ) );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $id1, $data[0]['id'] );
+       }
+
+       public function test_get_items_tags_exclude_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $tag = wp_insert_term( 'My Tag', 'post_tag' );
+
+               wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'tags_exclude', array( $tag['term_id'] ) );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 3, $data );
+               $this->assertEquals( $id4, $data[0]['id'] );
+               $this->assertEquals( $id3, $data[1]['id'] );
+               $this->assertEquals( $id2, $data[2]['id'] );
+       }
+
+       public function test_get_items_tags_and_categories_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $tag = wp_insert_term( 'My Tag', 'post_tag' );
+               $category = wp_insert_term( 'My Category', 'category' );
+
+               wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
+               wp_set_object_terms( $id2, array( $tag['term_id'] ), 'post_tag' );
+               wp_set_object_terms( $id1, array( $category['term_id'] ), 'category' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'tags', array( $tag['term_id'] ) );
+               $request->set_param( 'categories', array( $category['term_id'] ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 1, $response->get_data() );
+       }
+
+       public function test_get_items_tags_and_categories_exclude_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $tag = wp_insert_term( 'My Tag', 'post_tag' );
+               $category = wp_insert_term( 'My Category', 'category' );
+
+               wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
+               wp_set_object_terms( $id2, array( $tag['term_id'] ), 'post_tag' );
+               wp_set_object_terms( $id1, array( $category['term_id'] ), 'category' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'tags', array( $tag['term_id'] ) );
+               $request->set_param( 'categories_exclude', array( $category['term_id'] ) );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $id2, $data[0]['id'] );
+       }
+
+       public function test_get_items_sticky_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+
+               update_option( 'sticky_posts', array( $id2 ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'sticky', true );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 1, $response->get_data() );
+
+               $posts = $response->get_data();
+               $post = $posts[0];
+               $this->assertEquals( $id2, $post['id'] );
+       }
+
+       public function test_get_items_sticky_with_post__in_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+
+               update_option( 'sticky_posts', array( $id2 ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'sticky', true );
+               $request->set_param( 'include', array( $id1 ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 0, $response->get_data() );
+
+               update_option( 'sticky_posts', array( $id1, $id2 ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'sticky', true );
+               $request->set_param( 'include', array( $id1 ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertCount( 1, $response->get_data() );
+
+               $posts = $response->get_data();
+               $post = $posts[0];
+               $this->assertEquals( $id1, $post['id'] );
+       }
+
+       public function test_get_items_not_sticky_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+
+               update_option( 'sticky_posts', array( $id2 ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'sticky', false );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 1, $response->get_data() );
+
+               $posts = $response->get_data();
+               $post = $posts[0];
+               $this->assertEquals( $id1, $post['id'] );
+       }
+
+       public function test_get_items_sticky_with_post__not_in_query() {
+               $id1 = $this->post_id;
+               $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
+
+               update_option( 'sticky_posts', array( $id2 ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'sticky', false );
+               $request->set_param( 'exclude', array( $id3 ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 1, $response->get_data() );
+
+               $posts = $response->get_data();
+               $post = $posts[0];
+               $this->assertEquals( $id1, $post['id'] );
+       }
+
+       /**
+        * @group test
+        */
+       public function test_get_items_pagination_headers() {
+               // Start of the index
+               for ( $i = 0; $i < 49; $i++ ) {
+                       $this->factory->post->create( array(
+                               'post_title'   => "Post {$i}",
+                               ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 50, $headers['X-WP-Total'] );
+               $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // 3rd page
+               $this->factory->post->create( array(
+                               'post_title'   => 'Post 51',
+                               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 4,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // Last page
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'page', 6 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 5,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+               // Out of bounds
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'page', 8 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 6,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+
+               // With query params.
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_query_params( array( 'per_page' => 5, 'page' => 2 ) );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 11, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'per_page' => 5,
+                       'page'     => 1,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $next_link = add_query_arg( array(
+                       'per_page' => 5,
+                       'page'     => 3,
+                       ), rest_url( '/wp/v2/posts' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+       }
+
+       public function test_get_items_private_filter_query_var() {
+               // Private query vars inaccessible to unauthorized users
+               wp_set_current_user( 0 );
+               $draft_id = $this->factory->post->create( array( 'post_status' => 'draft' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'filter', array( 'post_status' => 'draft' ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $this->post_id, $data[0]['id'] );
+               // But they are accessible to authorized users
+               wp_set_current_user( $this->editor_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $draft_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_invalid_per_page() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_query_params( array( 'per_page' => -1 ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_items_invalid_posts_per_page_ignored() {
+               // This test ensures that filter[posts_per_page] is ignored, and that -1
+               // cannot be used to sidestep per_page's valid range to retrieve all posts
+               for ( $i = 0; $i < 20; $i++ ) {
+                       $this->factory->post->create( array( 'post_status' => 'publish' ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_query_params( array( 'filter' => array( 'posts_per_page' => -1 ) ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 10, $response->get_data() );
+       }
+
+       public function test_get_items_invalid_context() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'context', 'banana' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_items_invalid_date() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'after', rand_str() );
+               $request->set_param( 'before', rand_str() );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_items_valid_date() {
+               $post1 = $this->factory->post->create( array( 'post_date' => '2016-01-15T00:00:00Z' ) );
+               $post2 = $this->factory->post->create( array( 'post_date' => '2016-01-16T00:00:00Z' ) );
+               $post3 = $this->factory->post->create( array( 'post_date' => '2016-01-17T00:00:00Z' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_param( 'after', '2016-01-15T00:00:00Z' );
+               $request->set_param( 'before', '2016-01-17T00:00:00Z' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( $post2, $data[0]['id'] );
+       }
+
+       public function test_get_item() {
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_get_post_response( $response, 'view' );
+       }
+
+       public function test_get_item_links() {
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $links = $response->get_links();
+
+               $this->assertEquals( rest_url( '/wp/v2/posts/' . $this->post_id ), $links['self'][0]['href'] );
+               $this->assertEquals( rest_url( '/wp/v2/posts' ), $links['collection'][0]['href'] );
+
+               $this->assertEquals( rest_url( '/wp/v2/types/' . get_post_type( $this->post_id ) ), $links['about'][0]['href'] );
+
+               $replies_url = rest_url( '/wp/v2/comments' );
+               $replies_url = add_query_arg( 'post', $this->post_id, $replies_url );
+               $this->assertEquals( $replies_url, $links['replies'][0]['href'] );
+
+               $this->assertEquals( rest_url( '/wp/v2/posts/' . $this->post_id . '/revisions' ), $links['version-history'][0]['href'] );
+
+               $attachments_url = rest_url( '/wp/v2/media' );
+               $attachments_url = add_query_arg( 'parent', $this->post_id, $attachments_url );
+               $this->assertEquals( $attachments_url, $links['https://api.w.org/attachment'][0]['href'] );
+
+               $term_links = $links['https://api.w.org/term'];
+               $tag_link = $cat_link = $format_link = null;
+               foreach ( $term_links as $link ) {
+                       if ( 'post_tag' === $link['attributes']['taxonomy'] ) {
+                               $tag_link = $link;
+                       } elseif ( 'category' === $link['attributes']['taxonomy'] ) {
+                               $cat_link = $link;
+                       } elseif ( 'post_format' === $link['attributes']['taxonomy'] ) {
+                               $format_link = $link;
+                       }
+               }
+               $this->assertNotEmpty( $tag_link );
+               $this->assertNotEmpty( $cat_link );
+               $this->assertNull( $format_link );
+
+               $tags_url = add_query_arg( 'post', $this->post_id, rest_url( '/wp/v2/tags' ) );
+               $this->assertEquals( $tags_url, $tag_link['href'] );
+
+               $category_url = add_query_arg( 'post', $this->post_id, rest_url( '/wp/v2/categories' ) );
+               $this->assertEquals( $category_url, $cat_link['href'] );
+       }
+
+       public function test_get_item_links_no_author() {
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+               $links = $response->get_links();
+               $this->assertFalse( isset( $links['author'] ) );
+               wp_update_post( array( 'ID' => $this->post_id, 'post_author' => $this->author_id ) );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+               $links = $response->get_links();
+               $this->assertEquals( rest_url( '/wp/v2/users/' . $this->author_id ), $links['author'][0]['href'] );
+       }
+
+       public function test_get_post_without_permission() {
+               $draft_id = $this->factory->post->create( array(
+                       'post_status' => 'draft',
+               ) );
+               wp_set_current_user( 0 );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $draft_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_forbidden', $response, 403 );
+       }
+
+       public function test_get_post_invalid_id() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+       }
+
+       public function test_get_post_list_context_with_permission() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_query_params( array(
+                       'context' => 'edit',
+               ) );
+
+               wp_set_current_user( $this->editor_id );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->check_get_posts_response( $response, 'edit' );
+       }
+
+       public function test_get_post_list_context_without_permission() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+               $request->set_query_params( array(
+                       'context' => 'edit',
+               ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_post_context_without_permission() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_query_params( array(
+                       'context' => 'edit',
+               ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_post_with_password() {
+               $post_id = $this->factory->post->create( array(
+                       'post_password' => '$inthebananastand',
+               ) );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_get_post_response( $response, 'view' );
+
+               $data = $response->get_data();
+               $this->assertTrue( $data['content']['protected'] );
+               $this->assertTrue( $data['excerpt']['protected'] );
+       }
+
+       public function test_get_post_with_password_using_password() {
+               $post_id = $this->factory->post->create( array(
+                       'post_password' => '$inthebananastand',
+                       'post_content'  => 'Some secret content.',
+                       'post_excerpt'  => 'Some secret excerpt.',
+               ) );
+
+               $post = get_post( $post_id );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $request->set_param( 'password', '$inthebananastand' );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_get_post_response( $response, 'view' );
+
+               $data = $response->get_data();
+               $this->assertEquals( wpautop( $post->post_content ), $data['content']['rendered'] );
+               $this->assertEquals( wpautop( $post->post_excerpt ), $data['excerpt']['rendered'] );
+       }
+
+       public function test_get_post_with_password_using_incorrect_password() {
+               $post_id = $this->factory->post->create( array(
+                       'post_password' => '$inthebananastand',
+               ) );
+
+               $post = get_post( $post_id );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $request->set_param( 'password', 'wrongpassword' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_incorrect_password', $response, 403 );
+       }
+
+       public function test_get_post_with_password_without_permission() {
+               $post_id = $this->factory->post->create( array(
+                       'post_password' => '$inthebananastand',
+                       'post_content'  => 'Some secret content.',
+                       'post_excerpt'  => 'Some secret excerpt.',
+               ) );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->check_get_post_response( $response, 'view' );
+               $this->assertEquals( '', $data['content']['rendered'] );
+               $this->assertEquals( '', $data['excerpt']['rendered'] );
+
+       }
+
+       public function test_get_item_read_permission_custom_post_status() {
+               register_post_status( 'testpubstatus', array( 'public' => true ) );
+               register_post_status( 'testprivtatus', array( 'public' => false ) );
+               // Public status
+               wp_update_post( array( 'ID' => $this->post_id, 'post_status' => 'testpubstatus' ) );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               // Private status
+               wp_update_post( array( 'ID' => $this->post_id, 'post_status' => 'testprivtatus' ) );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 403, $response->get_status() );
+       }
+
+       public function test_prepare_item() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_query_params( array( 'context' => 'edit' ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_get_post_response( $response, 'edit' );
+       }
+
+       public function test_create_item() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $params = $this->set_post_data();
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_create_post_response( $response );
+       }
+
+       public function test_rest_create_item() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $request->add_header( 'content-type', 'application/json' );
+               $params = $this->set_post_data();
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_create_post_response( $response );
+       }
+
+       public function test_create_post_invalid_id() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'id' => '3',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_exists', $response, 400 );
+       }
+
+       public function test_create_post_as_contributor() {
+               wp_set_current_user( $this->contributor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data(array(
+                       'status' => 'pending',
+               ));
+
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $this->check_create_post_response( $response );
+       }
+
+       public function test_create_post_sticky() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'sticky' => true,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( true, $new_data['sticky'] );
+               $post = get_post( $new_data['id'] );
+               $this->assertEquals( true, is_sticky( $post->ID ) );
+       }
+
+       public function test_create_post_sticky_as_contributor() {
+               wp_set_current_user( $this->contributor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'sticky' => true,
+                       'status' => 'pending',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_assign_sticky', $response, 403 );
+       }
+
+       public function test_create_post_other_author_without_permission() {
+               wp_set_current_user( $this->author_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data(array(
+                       'author' => $this->editor_id,
+               ));
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_edit_others', $response, 403 );
+       }
+
+       public function test_create_post_without_permission() {
+               wp_set_current_user( 0 );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'status' => 'draft',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_create', $response, 401 );
+       }
+
+       public function test_create_post_draft() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'status' => 'draft',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( 'draft', $data['status'] );
+               $this->assertEquals( 'draft', $new_post->post_status );
+               // Confirm dates are null
+               $this->assertNull( $data['date_gmt'] );
+               $this->assertNull( $data['modified_gmt'] );
+       }
+
+       public function test_create_post_private() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'status' => 'private',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( 'private', $data['status'] );
+               $this->assertEquals( 'private', $new_post->post_status );
+       }
+
+       public function test_create_post_private_without_permission() {
+               wp_set_current_user( $this->author_id );
+               $user = wp_get_current_user();
+               $user->add_cap( 'publish_posts', false );
+               // Flush capabilities, https://core.trac.wordpress.org/ticket/28374
+               $user->get_role_caps();
+               $user->update_user_level_from_caps();
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'status' => 'private',
+                       'author' => $this->author_id,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_publish', $response, 403 );
+       }
+
+       public function test_create_post_publish_without_permission() {
+               wp_set_current_user( $this->author_id );
+               $user = wp_get_current_user();
+               $user->add_cap( 'publish_posts', false );
+               // Flush capabilities, https://core.trac.wordpress.org/ticket/28374
+               $user->get_role_caps();
+               $user->update_user_level_from_caps();
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'status' => 'publish',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_publish', $response, 403 );
+       }
+
+       public function test_create_post_invalid_status() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'status' => 'teststatus',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_post_with_format() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'format' => 'gallery',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( 'gallery', $data['format'] );
+               $this->assertEquals( 'gallery', get_post_format( $new_post->ID ) );
+       }
+
+       public function test_create_post_with_invalid_format() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'format' => 'testformat',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_update_post_with_featured_media() {
+
+               $file = DIR_TESTDATA . '/images/canola.jpg';
+               $this->attachment_id = $this->factory->attachment->create_object( $file, 0, array(
+                       'post_mime_type' => 'image/jpeg',
+                       'menu_order' => rand( 1, 100 ),
+               ) );
+
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'featured_media' => $this->attachment_id,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( $this->attachment_id, $data['featured_media'] );
+               $this->assertEquals( $this->attachment_id, (int) get_post_thumbnail_id( $new_post->ID ) );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $new_post->ID );
+               $params = $this->set_post_data( array(
+                       'featured_media' => 0,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 0, $data['featured_media'] );
+               $this->assertEquals( 0, (int) get_post_thumbnail_id( $new_post->ID ) );
+       }
+
+       public function test_create_post_invalid_author() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'author' => -1,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_author', $response, 400 );
+       }
+
+       public function test_create_post_invalid_author_without_permission() {
+               wp_set_current_user( $this->author_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'author' => $this->editor_id,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_edit_others', $response, 403 );
+       }
+
+       public function test_create_post_with_password() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'password' => 'testing',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertEquals( 'testing', $data['password'] );
+       }
+
+       public function test_create_post_with_falsy_password() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'password' => '0',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+
+               $this->assertEquals( '0', $data['password'] );
+       }
+
+       public function test_create_post_with_empty_string_password_and_sticky() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'password' => '',
+                       'sticky'   => true,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 201, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( '', $data['password'] );
+       }
+
+       public function test_create_post_with_password_and_sticky_fails() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'password' => '123',
+                       'sticky'   => true,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
+       }
+
+       public function test_create_post_custom_date() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'date' => '2010-01-01T02:00:00Z',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $time = gmmktime( 2, 0, 0, 1, 1, 2010 );
+               $this->assertEquals( '2010-01-01T02:00:00', $data['date'] );
+               $this->assertEquals( $time, strtotime( $new_post->post_date ) );
+       }
+
+       public function test_create_post_custom_date_with_timezone() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'date' => '2010-01-01T02:00:00-10:00',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $time = gmmktime( 12, 0, 0, 1, 1, 2010 );
+
+               $this->assertEquals( '2010-01-01T12:00:00', $data['date'] );
+               $this->assertEquals( '2010-01-01T12:00:00', $data['modified'] );
+
+               $this->assertEquals( $time, strtotime( $new_post->post_date ) );
+               $this->assertEquals( $time, strtotime( $new_post->post_modified ) );
+       }
+
+       public function test_create_post_with_db_error() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params  = $this->set_post_data( array() );
+               $request->set_body_params( $params );
+
+               /**
+                * Disable showing error as the below is going to intentionally
+                * trigger a DB error.
+                */
+               global $wpdb;
+               $wpdb->suppress_errors = true;
+               add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+               $response = $this->server->dispatch( $request );
+               remove_filter( 'query', array( $this, 'error_insert_query' ) );
+               $wpdb->show_errors = true;
+
+               $this->assertErrorResponse( 'db_insert_error', $response, 500 );
+       }
+
+       public function test_create_post_with_invalid_date() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'date' => '2010-60-01T02:00:00Z',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_post_with_invalid_date_gmt() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'date_gmt' => '2010-60-01T02:00:00',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_post_with_quotes_in_title() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'title' => "Rob O'Rourke's Diary",
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( "Rob O'Rourke's Diary", $new_data['title']['raw'] );
+       }
+
+       public function test_create_post_with_categories() {
+               wp_set_current_user( $this->editor_id );
+               $category = wp_insert_term( 'Test Category', 'category' );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'password'   => 'testing',
+                       'categories' => array(
+                               $category['term_id']
+                       ),
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertEquals( array( $category['term_id'] ), $data['categories'] );
+       }
+
+       public function test_create_post_with_invalid_categories() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $params = $this->set_post_data( array(
+                       'password'   => 'testing',
+                       'categories' => array(
+                               REST_TESTS_IMPOSSIBLY_HIGH_NUMBER
+                       ),
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertEquals( array(), $data['categories'] );
+       }
+
+       public function test_update_item() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $params = $this->set_post_data();
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_update_post_response( $response );
+               $new_data = $response->get_data();
+               $this->assertEquals( $this->post_id, $new_data['id'] );
+               $this->assertEquals( $params['title'], $new_data['title']['raw'] );
+               $this->assertEquals( $params['content'], $new_data['content']['raw'] );
+               $this->assertEquals( $params['excerpt'], $new_data['excerpt']['raw'] );
+               $post = get_post( $this->post_id );
+               $this->assertEquals( $params['title'], $post->post_title );
+               $this->assertEquals( $params['content'], $post->post_content );
+               $this->assertEquals( $params['excerpt'], $post->post_excerpt );
+       }
+
+       public function test_rest_update_post() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $params = $this->set_post_data();
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_update_post_response( $response );
+               $new_data = $response->get_data();
+               $this->assertEquals( $this->post_id, $new_data['id'] );
+               $this->assertEquals( $params['title'], $new_data['title']['raw'] );
+               $this->assertEquals( $params['content'], $new_data['content']['raw'] );
+               $this->assertEquals( $params['excerpt'], $new_data['excerpt']['raw'] );
+               $post = get_post( $this->post_id );
+               $this->assertEquals( $params['title'], $post->post_title );
+               $this->assertEquals( $params['content'], $post->post_content );
+               $this->assertEquals( $params['excerpt'], $post->post_excerpt );
+       }
+
+       public function test_rest_update_post_raw() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $params = $this->set_raw_post_data();
+               $request->set_body( wp_json_encode( $params ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_update_post_response( $response );
+               $new_data = $response->get_data();
+               $this->assertEquals( $this->post_id, $new_data['id'] );
+               $this->assertEquals( $params['title']['raw'], $new_data['title']['raw'] );
+               $this->assertEquals( $params['content']['raw'], $new_data['content']['raw'] );
+               $this->assertEquals( $params['excerpt']['raw'], $new_data['excerpt']['raw'] );
+               $post = get_post( $this->post_id );
+               $this->assertEquals( $params['title']['raw'], $post->post_title );
+               $this->assertEquals( $params['content']['raw'], $post->post_content );
+               $this->assertEquals( $params['excerpt']['raw'], $post->post_excerpt );
+       }
+
+       public function test_update_post_without_extra_params() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data();
+               unset( $params['type'] );
+               unset( $params['name'] );
+               unset( $params['author'] );
+               unset( $params['status'] );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->check_update_post_response( $response );
+       }
+
+       public function test_update_post_without_permission() {
+               wp_set_current_user( $this->editor_id );
+               $user = wp_get_current_user();
+               $user->add_cap( 'edit_published_posts', false );
+               // Flush capabilities, https://core.trac.wordpress.org/ticket/28374
+               $user->get_role_caps();
+               $user->update_user_level_from_caps();
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data();
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_update_post_sticky_as_contributor() {
+               wp_set_current_user( $this->contributor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'sticky' => true,
+                       'status' => 'pending',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_update_post_invalid_id() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+       }
+
+       public function test_update_post_invalid_route() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/pages/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+       }
+
+       public function test_update_post_with_format() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'format' => 'gallery',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+               $this->assertEquals( 'gallery', $data['format'] );
+               $this->assertEquals( 'gallery', get_post_format( $new_post->ID ) );
+       }
+
+       public function test_update_post_with_invalid_format() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'format' => 'testformat',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_update_post_ignore_readonly() {
+               wp_set_current_user( $this->editor_id );
+
+               $new_content = rand_str();
+               $expected_modified = current_time( 'mysql' );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'modified' => '2010-06-01T02:00:00Z',
+                       'content'  => $new_content,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               // The readonly modified param should be ignored, request should be a success.
+               $data = $response->get_data();
+               $new_post = get_post( $data['id'] );
+
+               $this->assertEquals( $new_content, $data['content']['raw'] );
+               $this->assertEquals( $new_content, $new_post->post_content );
+
+               // The modified date should equal the current time.
+               $this->assertEquals( date( 'Y-m-d', strtotime( mysql_to_rfc3339( $expected_modified ) ) ), date( 'Y-m-d', strtotime( $data['modified'] ) ) );
+               $this->assertEquals( date( 'Y-m-d', strtotime( $expected_modified ) ), date( 'Y-m-d', strtotime( $new_post->post_modified ) ) );
+       }
+
+       public function test_update_post_with_invalid_date() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'date' => rand_str(),
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_update_post_with_invalid_date_gmt() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'date_gmt' => rand_str(),
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_update_post_slug() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'slug' => 'sample-slug',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( 'sample-slug', $new_data['slug'] );
+               $post = get_post( $new_data['id'] );
+               $this->assertEquals( 'sample-slug', $post->post_name );
+       }
+
+       public function test_update_post_slug_accented_chars() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'slug' => 'tęst-acceńted-chäræcters',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( 'test-accented-charaecters', $new_data['slug'] );
+               $post = get_post( $new_data['id'] );
+               $this->assertEquals( 'test-accented-charaecters', $post->post_name );
+       }
+
+       public function test_update_post_sticky() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'sticky' => true,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( true, $new_data['sticky'] );
+               $post = get_post( $new_data['id'] );
+               $this->assertEquals( true, is_sticky( $post->ID ) );
+
+               // Updating another field shouldn't change sticky status
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'title'       => 'This should not reset sticky',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( true, $new_data['sticky'] );
+               $post = get_post( $new_data['id'] );
+               $this->assertEquals( true, is_sticky( $post->ID ) );
+       }
+
+       public function test_update_post_excerpt() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( array(
+                       'excerpt' => 'An Excerpt',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( 'An Excerpt', $new_data['excerpt']['raw'] );
+       }
+
+       public function test_update_post_empty_excerpt() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( array(
+                       'excerpt' => '',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( '', $new_data['excerpt']['raw'] );
+       }
+
+       public function test_update_post_content() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( array(
+                       'content' => 'Some Content',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( 'Some Content', $new_data['content']['raw'] );
+       }
+
+       public function test_update_post_empty_content() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( array(
+                       'content' => '',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( '', $new_data['content']['raw'] );
+       }
+
+       public function test_update_post_with_password_and_sticky_fails() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'password' => '123',
+                       'sticky'   => true,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
+       }
+
+       public function test_update_stick_post_with_password_fails() {
+               wp_set_current_user( $this->editor_id );
+
+               stick_post( $this->post_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'password' => '123',
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
+       }
+
+       public function test_update_password_protected_post_with_sticky_fails() {
+               wp_set_current_user( $this->editor_id );
+
+               wp_update_post( array( 'ID' => $this->post_id, 'post_password' => '123' ) );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'sticky' => true,
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
+       }
+
+       public function test_update_post_with_quotes_in_title() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'title' => "Rob O'Rourke's Diary",
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( "Rob O'Rourke's Diary", $new_data['title']['raw'] );
+       }
+
+       public function test_update_post_with_categories() {
+
+               wp_set_current_user( $this->editor_id );
+               $category = wp_insert_term( 'Test Category', 'category' );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'title' => 'Tester',
+                       'categories' => array(
+                               $category['term_id'],
+                       ),
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( array( $category['term_id'] ), $new_data['categories'] );
+               $categories_path = '';
+               $links = $response->get_links();
+               foreach ( $links['https://api.w.org/term'] as $link ) {
+                       if ( 'category' === $link['attributes']['taxonomy'] ) {
+                               $categories_path = $link['href'];
+                       }
+               }
+               $query = parse_url( $categories_path, PHP_URL_QUERY );
+               parse_str( $query, $args );
+               $request = new WP_REST_Request( 'GET', $args['rest_route'] );
+               unset( $args['rest_route'] );
+               $request->set_query_params( $args );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertCount( 1, $data );
+               $this->assertEquals( 'Test Category', $data[0]['name'] );
+       }
+
+       public function test_update_post_with_empty_categories() {
+
+               wp_set_current_user( $this->editor_id );
+               $category = wp_insert_term( 'Test Category', 'category' );
+               wp_set_object_terms( $this->post_id, $category['term_id'], 'category' );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $params = $this->set_post_data( array(
+                       'title' => 'Tester',
+                       'categories' => array(),
+               ) );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+               $new_data = $response->get_data();
+               $this->assertEquals( array(), $new_data['categories'] );
+       }
+
+       public function test_delete_item() {
+               $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) );
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertNotInstanceOf( 'WP_Error', $response );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Deleted post', $data['title']['raw'] );
+       }
+
+       public function test_delete_item_skip_trash() {
+               $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) );
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $request['force'] = true;
+               $response = $this->server->dispatch( $request );
+
+               $this->assertNotInstanceOf( 'WP_Error', $response );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Deleted post', $data['title']['raw'] );
+       }
+
+       public function test_delete_item_already_trashed() {
+               $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) );
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $post_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_already_trashed', $response, 410 );
+       }
+
+       public function test_delete_post_invalid_id() {
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+       }
+
+       public function test_delete_post_invalid_post_type() {
+               $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );
+               wp_set_current_user( $this->editor_id );
+
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $page_id );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
+       }
+
+       public function test_delete_post_without_permission() {
+               wp_set_current_user( $this->author_id );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+       }
+
+       public function test_register_post_type_invalid_controller() {
+
+               register_post_type( 'invalid-controller', array( 'show_in_rest' => true, 'rest_controller_class' => 'Fake_Class_Baba' ) );
+               create_initial_rest_routes();
+               $routes = $this->server->get_routes();
+               $this->assertFalse( isset( $routes['/wp/v2/invalid-controller'] ) );
+               _unregister_post_type( 'invalid-controller' );
+
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 25, count( $properties ) );
+               $this->assertArrayHasKey( 'author', $properties );
+               $this->assertArrayHasKey( 'comment_status', $properties );
+               $this->assertArrayHasKey( 'content', $properties );
+               $this->assertArrayHasKey( 'date', $properties );
+               $this->assertArrayHasKey( 'date_gmt', $properties );
+               $this->assertArrayHasKey( 'excerpt', $properties );
+               $this->assertArrayHasKey( 'featured_media', $properties );
+               $this->assertArrayHasKey( 'guid', $properties );
+               $this->assertArrayHasKey( 'format', $properties );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'modified', $properties );
+               $this->assertArrayHasKey( 'modified_gmt', $properties );
+               $this->assertArrayHasKey( 'password', $properties );
+               $this->assertArrayHasKey( 'ping_status', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'status', $properties );
+               $this->assertArrayHasKey( 'sticky', $properties );
+               $this->assertArrayHasKey( 'title', $properties );
+               $this->assertArrayHasKey( 'type', $properties );
+               $this->assertArrayHasKey( 'tags', $properties );
+               $this->assertArrayHasKey( 'tags_exclude', $properties );
+               $this->assertArrayHasKey( 'categories', $properties );
+               $this->assertArrayHasKey( 'categories_exclude', $properties );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'post', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               wp_set_current_user( 1 );
+
+               $post_id = $this->factory->post->create();
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $post_id );
+               $request->set_body_params(array(
+                       'my_custom_int' => 123,
+               ));
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 123, get_post_meta( $post_id, 'my_custom_int', true ) );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
+               $request->set_body_params(array(
+                       'my_custom_int' => 123,
+                       'title' => 'hello',
+               ));
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 123, $response->data['my_custom_int'] );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function test_additional_field_update_errors() {
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'post', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               wp_set_current_user( $this->editor_id );
+               // Check for error on update.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
+               $request->set_body_params( array(
+                       'my_custom_int' => 'returnError',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object ) {
+               return get_post_meta( $object['id'], 'my_custom_int', true );
+       }
+
+       public function additional_field_update_callback( $value, $post ) {
+               if ( 'returnError' === $value ) {
+                       return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
+               }
+               update_post_meta( $post->ID, 'my_custom_int', $value );
+       }
+
+       public function tearDown() {
+               _unregister_post_type( 'youseeeme' );
+               if ( isset( $this->attachment_id ) ) {
+                       $this->remove_added_uploads();
+               }
+               parent::tearDown();
+       }
+
+       /**
+        * Internal function used to disable an insert query which
+        * will trigger a wpdb error for testing purposes.
+        */
+       public function error_insert_query( $query ) {
+               if ( strpos( $query, 'INSERT' ) === 0 ) {
+                       $query = '],';
+               }
+               return $query;
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestrequestvalidationphp"></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-request-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-request-validation.php                            (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-request-validation.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,167 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class WP_Test_REST_Request_Validation extends WP_Test_REST_TestCase {
+
+       public function test_validate_within_min_max_range_inclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'minmaxrange' => array(
+                                       'type'    => 'integer',
+                                       'minimum' => 2,
+                                       'maximum' => 10,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (inclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (inclusive)', $ret->get_error_message() );
+       }
+
+       public function test_validate_within_min_max_range_min_exclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'minmaxrange' => array(
+                                       'type'             => 'integer',
+                                       'minimum'          => 2,
+                                       'maximum'          => 10,
+                                       'exclusiveMinimum' => true,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (inclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (inclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 3, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 9, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (inclusive)', $ret->get_error_message() );
+       }
+
+       public function test_validate_within_min_max_range_max_exclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'minmaxrange' => array(
+                                       'type'             => 'integer',
+                                       'minimum'          => 2,
+                                       'maximum'          => 10,
+                                       'exclusiveMaximum' => true,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 3, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 9, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (exclusive)', $ret->get_error_message() );
+       }
+
+       public function test_validate_within_min_max_range_both_exclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'minmaxrange' => array(
+                                       'type'             => 'integer',
+                                       'minimum'          => 2,
+                                       'maximum'          => 10,
+                                       'exclusiveMinimum' => true,
+                                       'exclusiveMaximum' => true,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 3, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 9, $request, 'minmaxrange' );
+               $this->assertTrue( $ret );
+               $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
+               $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
+       }
+
+       public function test_validate_greater_than_min_inclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'greaterthanmin' => array(
+                                       'type'             => 'integer',
+                                       'minimum'          => 2,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 1, $request, 'greaterthanmin' );
+               $this->assertEquals( 'greaterthanmin must be greater than 2 (inclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 2, $request, 'greaterthanmin' );
+               $this->assertTrue( $ret );
+       }
+
+       public function test_validate_greater_than_min_exclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'greaterthanmin' => array(
+                                       'type'             => 'integer',
+                                       'minimum'          => 2,
+                                       'exclusiveMinimum' => true,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 1, $request, 'greaterthanmin' );
+               $this->assertEquals( 'greaterthanmin must be greater than 2 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 2, $request, 'greaterthanmin' );
+               $this->assertEquals( 'greaterthanmin must be greater than 2 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 3, $request, 'greaterthanmin' );
+               $this->assertTrue( $ret );
+       }
+
+       public function test_validate_less_than_max_inclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'lessthanmax' => array(
+                                       'type'             => 'integer',
+                                       'maximum'          => 10,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 11, $request, 'lessthanmax' );
+               $this->assertEquals( 'lessthanmax must be less than 10 (inclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 10, $request, 'lessthanmax' );
+               $this->assertTrue( $ret );
+       }
+
+       public function test_validate_less_than_max_exclusive() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
+                       'args' => array(
+                               'lessthanmax' => array(
+                                       'type'             => 'integer',
+                                       'maximum'          => 10,
+                                       'exclusiveMaximum' => true,
+                               ),
+                       ),
+               ) );
+               $ret = rest_validate_request_arg( 11, $request, 'lessthanmax' );
+               $this->assertEquals( 'lessthanmax must be less than 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 10, $request, 'lessthanmax' );
+               $this->assertEquals( 'lessthanmax must be less than 10 (exclusive)', $ret->get_error_message() );
+               $ret = rest_validate_request_arg( 9, $request, 'lessthanmax' );
+               $this->assertTrue( $ret );
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestrevisionscontrollerphp"></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-revisions-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-revisions-controller.php                          (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-revisions-controller.php    2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,301 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Revisions_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->post_id = $this->factory->post->create();
+               $this->page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );
+
+               $this->editor_id = $this->factory->user->create( array(
+                       'role' => 'editor',
+               ) );
+               $this->contributor_id = $this->factory->user->create( array(
+                       'role' => 'contributor',
+               ) );
+
+               wp_update_post( array( 'post_content' => 'This content is better.', 'ID' => $this->post_id ) );
+               wp_update_post( array( 'post_content' => 'This content is marvelous.', 'ID' => $this->post_id ) );
+               $revisions = wp_get_post_revisions( $this->post_id );
+               $this->revision_1 = array_pop( $revisions );
+               $this->revision_id1 = $this->revision_1->ID;
+               $this->revision_2 = array_pop( $revisions );
+               $this->revision_id2 = $this->revision_2->ID;
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/posts/(?P<parent>[\d]+)/revisions', $routes );
+               $this->assertArrayHasKey( '/wp/v2/posts/(?P<parent>[\d]+)/revisions/(?P<id>[\d]+)', $routes );
+               $this->assertArrayHasKey( '/wp/v2/pages/(?P<parent>[\d]+)/revisions', $routes );
+               $this->assertArrayHasKey( '/wp/v2/pages/(?P<parent>[\d]+)/revisions/(?P<id>[\d]+)', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_1->ID );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_get_items() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertCount( 2, $data );
+
+               // Reverse chron
+               $this->assertEquals( $this->revision_id2, $data[0]['id'] );
+               $this->check_get_revision_response( $data[0], $this->revision_2 );
+
+               $this->assertEquals( $this->revision_id1, $data[1]['id'] );
+               $this->check_get_revision_response( $data[1], $this->revision_1 );
+       }
+
+       public function test_get_items_no_permission() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
+               wp_set_current_user( $this->contributor_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
+       }
+
+       public function test_get_items_missing_parent() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+       }
+
+       public function test_get_items_invalid_parent_post_type() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->page_id . '/revisions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+       }
+
+       public function test_get_item() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->check_get_revision_response( $response, $this->revision_1 );
+               $fields = array(
+                       'author',
+                       'date',
+                       'date_gmt',
+                       'modified',
+                       'modified_gmt',
+                       'guid',
+                       'id',
+                       'parent',
+                       'slug',
+                       'title',
+                       'excerpt',
+                       'content',
+               );
+               $data = $response->get_data();
+               $this->assertEqualSets( $fields, array_keys( $data ) );
+       }
+
+       public function test_get_item_embed_context() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+               $request->set_param( 'context', 'embed' );
+               $response = $this->server->dispatch( $request );
+               $fields = array(
+                       'author',
+                       'date',
+                       'id',
+                       'parent',
+                       'slug',
+                       'title',
+                       'excerpt',
+               );
+               $data = $response->get_data();
+               $this->assertEqualSets( $fields, array_keys( $data ) );
+       }
+
+       public function test_get_item_no_permission() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
+               wp_set_current_user( $this->contributor_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
+       }
+
+       public function test_get_item_missing_parent() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+       }
+
+       public function test_get_item_invalid_parent_post_type() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->page_id . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
+       }
+
+       public function test_delete_item() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertNull( get_post( $this->revision_id1 ) );
+       }
+
+       public function test_delete_item_no_permission() {
+               wp_set_current_user( $this->contributor_id );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
+       }
+
+       public function test_prepare_item() {
+               wp_set_current_user( $this->editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $this->check_get_revision_response( $response, $this->revision_1 );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 12, count( $properties ) );
+               $this->assertArrayHasKey( 'author', $properties );
+               $this->assertArrayHasKey( 'content', $properties );
+               $this->assertArrayHasKey( 'date', $properties );
+               $this->assertArrayHasKey( 'date_gmt', $properties );
+               $this->assertArrayHasKey( 'excerpt', $properties );
+               $this->assertArrayHasKey( 'guid', $properties );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'modified', $properties );
+               $this->assertArrayHasKey( 'modified_gmt', $properties );
+               $this->assertArrayHasKey( 'parent', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'title', $properties );
+       }
+
+       public function test_create_item() {
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $this->post_id . '/revisions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_update_item() {
+               $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'post-revision', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               wp_set_current_user( 1 );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object ) {
+               return get_post_meta( $object['id'], 'my_custom_int', true );
+       }
+
+       public function additional_field_update_callback( $value, $post ) {
+               update_post_meta( $post->ID, 'my_custom_int', $value );
+       }
+
+       protected function check_get_revision_response( $response, $revision ) {
+               if ( $response instanceof WP_REST_Response ) {
+                       $links = $response->get_links();
+                       $response = $response->get_data();
+               } else {
+                       $this->assertArrayHasKey( '_links', $response );
+                       $links = $response['_links'];
+               }
+
+               $this->assertEquals( $revision->post_author, $response['author'] );
+
+               $rendered_content = apply_filters( 'the_content', $revision->post_content );
+               $this->assertEquals( $rendered_content, $response['content']['rendered'] );
+
+               $this->assertEquals( mysql_to_rfc3339( $revision->post_date ), $response['date'] );
+               $this->assertEquals( mysql_to_rfc3339( $revision->post_date_gmt ), $response['date_gmt'] );
+
+               $rendered_excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $revision->post_excerpt, $revision ) );
+               $this->assertEquals( $rendered_excerpt, $response['excerpt']['rendered'] );
+
+               $rendered_guid = apply_filters( 'get_the_guid', $revision->guid );
+               $this->assertEquals( $rendered_guid, $response['guid']['rendered'] );
+
+               $this->assertEquals( $revision->ID, $response['id'] );
+               $this->assertEquals( mysql_to_rfc3339( $revision->post_modified ), $response['modified'] );
+               $this->assertEquals( mysql_to_rfc3339( $revision->post_modified_gmt ), $response['modified_gmt'] );
+               $this->assertEquals( $revision->post_name, $response['slug'] );
+
+               $rendered_title = get_the_title( $revision->ID );
+               $this->assertEquals( $rendered_title, $response['title']['rendered'] );
+
+               $parent = get_post( $revision->post_parent );
+               $parent_controller = new WP_REST_Posts_Controller( $parent->post_type );
+               $parent_object = get_post_type_object( $parent->post_type );
+               $parent_base = ! empty( $parent_object->rest_base ) ? $parent_object->rest_base : $parent_object->name;
+               $this->assertEquals( rest_url( '/wp/v2/' . $parent_base . '/' . $revision->post_parent ), $links['parent'][0]['href'] );
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestsettingscontrollerphp"></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-settings-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-settings-controller.php                           (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-settings-controller.php     2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,239 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_Test_REST_Settings_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->administrator = $this->factory->user->create( array(
+                       'role' => 'administrator',
+               ) );
+               $this->endpoint = new WP_REST_Settings_Controller();
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/settings', $routes );
+       }
+
+       public function test_get_items() {
+       }
+
+       public function test_context_param() {
+       }
+
+       public function test_get_item_is_not_public() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 403, $response->get_status() );
+       }
+
+       public function test_get_item() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( array(
+                       'title',
+                       'description',
+                       'url',
+                       'email',
+                       'timezone',
+                       'date_format',
+                       'time_format',
+                       'start_of_week',
+                       'language',
+                       'use_smilies',
+                       'default_category',
+                       'default_post_format',
+                       'posts_per_page',
+               ), array_keys( $data ) );
+       }
+
+       public function test_get_item_value_is_cast_to_type() {
+               wp_set_current_user( $this->administrator );
+               update_option( 'posts_per_page', 'invalid_number' ); // this is cast to (int) 1
+               $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 1, $data['posts_per_page'] );
+       }
+
+       public function test_get_item_with_custom_setting() {
+               wp_set_current_user( $this->administrator );
+
+               register_setting( 'somegroup', 'mycustomsetting', array(
+                       'show_in_rest' => array(
+                               'name'   => 'mycustomsettinginrest',
+                               'schema' => array(
+                                       'enum'    => array( 'validvalue1', 'validvalue2' ),
+                                       'default' => 'validvalue1',
+                               ),
+                       ),
+                       'type'         => 'string',
+               ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertArrayHasKey( 'mycustomsettinginrest', $data );
+               $this->assertEquals( 'validvalue1', $data['mycustomsettinginrest'] );
+
+               update_option( 'mycustomsetting', 'validvalue2' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'validvalue2', $data['mycustomsettinginrest'] );
+
+               unregister_setting( 'somegroup', 'mycustomsetting' );
+       }
+
+       public function get_setting_custom_callback( $result, $name, $args ) {
+               switch ( $name ) {
+                       case 'mycustomsetting1':
+                               return 'filtered1';
+               }
+               return $result;
+       }
+
+       public function test_get_item_with_filter() {
+               wp_set_current_user( $this->administrator );
+
+               add_filter( 'rest_pre_get_setting', array( $this, 'get_setting_custom_callback' ), 10, 3 );
+
+               register_setting( 'somegroup', 'mycustomsetting1', array(
+                       'show_in_rest' => array(
+                               'name'   => 'mycustomsettinginrest1',
+                       ),
+                       'type'         => 'string',
+               ) );
+
+               register_setting( 'somegroup', 'mycustomsetting2', array(
+                       'show_in_rest' => array(
+                               'name'   => 'mycustomsettinginrest2',
+                       ),
+                       'type'         => 'string',
+               ) );
+
+               update_option( 'mycustomsetting1', 'unfiltered1' );
+               update_option( 'mycustomsetting2', 'unfiltered2' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $this->assertArrayHasKey( 'mycustomsettinginrest1', $data );
+               $this->assertEquals( 'unfiltered1', $data['mycustomsettinginrest1'] );
+
+               $this->assertArrayHasKey( 'mycustomsettinginrest2', $data );
+               $this->assertEquals( 'unfiltered2', $data['mycustomsettinginrest2'] );
+
+               unregister_setting( 'somegroup', 'mycustomsetting' );
+               remove_all_filters( 'rest_pre_get_setting' );
+       }
+
+       public function test_create_item() {
+       }
+
+       public function test_update_item() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+               $request->set_param( 'title', 'The new title!' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 'The new title!', $data['title'] );
+               $this->assertEquals( get_option( 'blogname' ), $data['title'] );
+       }
+
+       public function update_setting_custom_callback( $result, $name, $value, $args ) {
+               if ( 'title' === $name && 'The new title!' === $value ) {
+                       // Do not allow changing the title in this case
+                       return true;
+               }
+
+               return false;
+       }
+
+       public function test_update_item_with_filter() {
+               wp_set_current_user( $this->administrator );
+
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+               $request->set_param( 'title', 'The old title!' );
+               $request->set_param( 'description', 'The old description!' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 'The old title!', $data['title'] );
+               $this->assertEquals( 'The old description!', $data['description'] );
+               $this->assertEquals( get_option( 'blogname' ), $data['title'] );
+               $this->assertEquals( get_option( 'blogdescription' ), $data['description'] );
+
+               add_filter( 'rest_pre_update_setting', array( $this, 'update_setting_custom_callback' ), 10, 4 );
+
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+               $request->set_param( 'title', 'The new title!' );
+               $request->set_param( 'description', 'The new description!' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 'The old title!', $data['title'] );
+               $this->assertEquals( 'The new description!', $data['description'] );
+               $this->assertEquals( get_option( 'blogname' ), $data['title'] );
+               $this->assertEquals( get_option( 'blogdescription' ), $data['description'] );
+
+               remove_all_filters( 'rest_pre_update_setting' );
+       }
+
+       public function test_update_item_with_invalid_type() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+               $request->set_param( 'title', array( 'rendered' => 'This should fail.' ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       /**
+        * Setting an item to "null" will essentially restore it to it's default value.
+        */
+       public function test_update_item_with_null() {
+               update_option( 'posts_per_page', 9 );
+
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
+               $request->set_param( 'posts_per_page', null );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 10, $data['posts_per_page'] );
+       }
+
+       public function test_delete_item() {
+       }
+
+       public function test_prepare_item() {
+       }
+
+       public function test_get_item_schema() {
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapiresttagscontrollerphp"></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-tags-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-tags-controller.php                               (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-tags-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,738 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Terms_Controller functionality, used for Tags.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Tags_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->administrator = $this->factory->user->create( array(
+                       'role' => 'administrator',
+               ) );
+               $this->subscriber = $this->factory->user->create( array(
+                       'role' => 'subscriber',
+               ) );
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+               $this->assertArrayHasKey( '/wp/v2/tags', $routes );
+               $this->assertArrayHasKey( '/wp/v2/tags/(?P<id>[\d]+)', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) );
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags/' . $tag1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'context',
+                       'exclude',
+                       'hide_empty',
+                       'include',
+                       'offset',
+                       'order',
+                       'orderby',
+                       'page',
+                       'per_page',
+                       'post',
+                       'search',
+                       'slug',
+                       ), $keys );
+       }
+
+       public function test_get_items() {
+               $this->factory->tag->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $this->check_get_taxonomy_terms_response( $response );
+       }
+
+       public function test_get_items_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_items_hide_empty_arg() {
+               $post_id = $this->factory->post->create();
+               $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) );
+               $tag2 = $this->factory->tag->create( array( 'name' => 'The Be Sharps' ) );
+               wp_set_object_terms( $post_id, array( $tag1, $tag2 ), 'post_tag' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'hide_empty', true );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'Season 5', $data[0]['name'] );
+               $this->assertEquals( 'The Be Sharps', $data[1]['name'] );
+       }
+
+       public function test_get_items_include_query() {
+               $id1 = $this->factory->tag->create();
+               $id2 = $this->factory->tag->create();
+               $id3 = $this->factory->tag->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               // Orderby=>asc
+               $request->set_param( 'include', array( $id3, $id1 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id1, $data[0]['id'] );
+               // Orderby=>include
+               $request->set_param( 'orderby', 'include' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id3, $data[0]['id'] );
+       }
+
+       public function test_get_items_exclude_query() {
+               $id1 = $this->factory->tag->create();
+               $id2 = $this->factory->tag->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+               $request->set_param( 'exclude', array( $id2 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+       }
+
+       public function test_get_items_offset_query() {
+               $id1 = $this->factory->tag->create();
+               $id2 = $this->factory->tag->create();
+               $id3 = $this->factory->tag->create();
+               $id4 = $this->factory->tag->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'offset', 1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 3, $response->get_data() );
+               // 'offset' works with 'per_page'
+               $request->set_param( 'per_page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+               // 'offset' takes priority over 'page'
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+       }
+
+
+       public function test_get_items_orderby_args() {
+               $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
+               $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
+               /*
+                * Tests:
+                * - orderby
+                * - order
+                * - per_page
+                */
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'orderby', 'name' );
+               $request->set_param( 'order', 'desc' );
+               $request->set_param( 'per_page', 1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Banana', $data[0]['name'] );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'orderby', 'name' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'per_page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+       }
+
+       public function test_get_items_orderby_id() {
+               $tag0 = $this->factory->tag->create( array( 'name' => 'Cantaloupe' ) );
+               $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
+               $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
+               // defaults to orderby=name, order=asc
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+               $this->assertEquals( 'Banana', $data[1]['name'] );
+               $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
+               // orderby=id, with default order=asc
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'orderby', 'id' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Cantaloupe', $data[0]['name'] );
+               $this->assertEquals( 'Apple', $data[1]['name'] );
+               $this->assertEquals( 'Banana', $data[2]['name'] );
+               // orderby=id, order=desc
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'orderby', 'id' );
+               $request->set_param( 'order', 'desc' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 200, $response->get_status() );
+               $this->assertEquals( 'Banana', $data[0]['name'] );
+               $this->assertEquals( 'Apple', $data[1]['name'] );
+               $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
+       }
+
+       public function test_get_items_post_args() {
+               $post_id = $this->factory->post->create();
+               $tag1 = $this->factory->tag->create( array( 'name' => 'DC' ) );
+               $tag2 = $this->factory->tag->create( array( 'name' => 'Marvel' ) );
+               $this->factory->tag->create( array( 'name' => 'Dark Horse' ) );
+               wp_set_object_terms( $post_id, array( $tag1, $tag2 ), 'post_tag' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'DC', $data[0]['name'] );
+       }
+
+       public function test_get_terms_post_args_paging() {
+               $post_id = $this->factory->post->create();
+               $tag_ids = array();
+
+               for ( $i = 0; $i < 30; $i++ ) {
+                       $tag_ids[] = $this->factory->tag->create( array(
+                               'name'   => "Tag {$i}",
+                       ) );
+               }
+               wp_set_object_terms( $post_id, $tag_ids, 'post_tag' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'post', $post_id );
+               $request->set_param( 'page', 1 );
+               $request->set_param( 'per_page', 15 );
+               $request->set_param( 'orderby', 'id' );
+               $response = $this->server->dispatch( $request );
+               $tags = $response->get_data();
+
+               $i = 0;
+               foreach ( $tags as $tag ) {
+                       $this->assertEquals( $tag['name'], "Tag {$i}" );
+                       $i++;
+               }
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'post', $post_id );
+               $request->set_param( 'page', 2 );
+               $request->set_param( 'per_page', 15 );
+               $request->set_param( 'orderby', 'id' );
+               $response = $this->server->dispatch( $request );
+               $tags = $response->get_data();
+
+               foreach ( $tags as $tag ) {
+                       $this->assertEquals( $tag['name'], "Tag {$i}" );
+                       $i++;
+               }
+       }
+
+       public function test_get_items_post_empty() {
+               $post_id = $this->factory->post->create();
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertCount( 0, $data );
+       }
+
+       public function test_get_items_custom_tax_post_args() {
+               register_taxonomy( 'batman', 'post', array( 'show_in_rest' => true ) );
+               $controller = new WP_REST_Terms_Controller( 'batman' );
+               $controller->register_routes();
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'batman' ) );
+               $term2 = $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'batman' ) );
+               $this->factory->term->create( array( 'name' => 'Car', 'taxonomy' => 'batman' ) );
+               $post_id = $this->factory->post->create();
+               wp_set_object_terms( $post_id, array( $term1, $term2 ), 'batman' );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/batman' );
+               $request->set_param( 'post', $post_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( 'Cape', $data[0]['name'] );
+       }
+
+       public function test_get_items_search_args() {
+               $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
+               $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
+               /*
+                * Tests:
+                * - search
+                */
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'search', 'App' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'search', 'Garbage' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 0, count( $data ) );
+       }
+
+       public function test_get_items_slug_arg() {
+               $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
+               $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'slug', 'apple' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( 'Apple', $data[0]['name'] );
+       }
+
+       public function test_get_terms_private_taxonomy() {
+               register_taxonomy( 'robin', 'post', array( 'public' => false ) );
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
+               $term2 = $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'robin' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_terms_pagination_headers() {
+               // Start of the index
+               for ( $i = 0; $i < 50; $i++ ) {
+                       $this->factory->tag->create( array(
+                               'name'   => "Tag {$i}",
+                               ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 50, $headers['X-WP-Total'] );
+               $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( 'wp/v2/tags' ) );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // 3rd page
+               $this->factory->tag->create( array(
+                               'name'   => 'Tag 51',
+                               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( 'wp/v2/tags' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 4,
+                       ), rest_url( 'wp/v2/tags' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // Last page
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'page', 6 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 5,
+                       ), rest_url( 'wp/v2/tags' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+               // Out of bounds
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'page', 8 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 6,
+                       ), rest_url( 'wp/v2/tags' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+       }
+
+       public function test_get_items_invalid_context() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
+               $request->set_param( 'context', 'banana' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_get_item() {
+               $id = $this->factory->tag->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id );
+               $response = $this->server->dispatch( $request );
+               $this->check_get_taxonomy_term_response( $response, $id );
+       }
+
+       public function test_get_term_invalid_term() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_get_item_invalid_permission_for_context() {
+               $id = $this->factory->tag->create();
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_term_private_taxonomy() {
+               register_taxonomy( 'robin', 'post', array( 'public' => false ) );
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin/' . $term1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+       }
+
+       public function test_get_item_incorrect_taxonomy() {
+               register_taxonomy( 'robin', 'post' );
+               $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $term1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_create_item() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
+               $request->set_param( 'name', 'My Awesome Term' );
+               $request->set_param( 'description', 'This term is so awesome.' );
+               $request->set_param( 'slug', 'so-awesome' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 201, $response->get_status() );
+               $headers = $response->get_headers();
+               $data = $response->get_data();
+               $this->assertContains( '/wp/v2/tags/' . $data['id'], $headers['Location'] );
+               $this->assertEquals( 'My Awesome Term', $data['name'] );
+               $this->assertEquals( 'This term is so awesome.', $data['description'] );
+               $this->assertEquals( 'so-awesome', $data['slug'] );
+       }
+
+       public function test_create_item_incorrect_permissions() {
+               wp_set_current_user( $this->subscriber );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
+               $request->set_param( 'name', 'Incorrect permissions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
+       }
+
+       public function test_create_item_missing_arguments() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 );
+       }
+
+       public function test_create_item_parent_non_hierarchical_taxonomy() {
+               wp_set_current_user( $this->administrator );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
+               $request->set_param( 'name', 'My Awesome Term' );
+               $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_taxonomy_not_hierarchical', $response, 400 );
+       }
+
+       public function test_update_item() {
+               wp_set_current_user( $this->administrator );
+               $orig_args = array(
+                       'name'        => 'Original Name',
+                       'description' => 'Original Description',
+                       'slug'        => 'original-slug',
+                       );
+               $term = get_term_by( 'id', $this->factory->tag->create( $orig_args ), 'post_tag' );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id );
+               $request->set_param( 'name', 'New Name' );
+               $request->set_param( 'description', 'New Description' );
+               $request->set_param( 'slug', 'new-slug' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'New Name', $data['name'] );
+               $this->assertEquals( 'New Description', $data['description'] );
+               $this->assertEquals( 'new-slug', $data['slug'] );
+       }
+
+       public function test_update_item_invalid_term() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $request->set_param( 'name', 'Invalid Term' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_update_item_incorrect_permissions() {
+               wp_set_current_user( $this->subscriber );
+               $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id );
+               $request->set_param( 'name', 'Incorrect permissions' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+       }
+
+       public function test_update_item_parent_non_hierarchical_taxonomy() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_taxonomy_id );
+               $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_taxonomy_not_hierarchical', $response, 400 );
+       }
+
+       public function test_delete_item() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id );
+               $request->set_param( 'force', true );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Deleted Tag', $data['name'] );
+       }
+
+       public function test_delete_item_force_false() {
+               wp_set_current_user( $this->administrator );
+               $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id );
+               // force defaults to false
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 501, $response->get_status() );
+       }
+
+       public function test_delete_item_invalid_term() {
+               wp_set_current_user( $this->administrator );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
+       }
+
+       public function test_delete_item_incorrect_permissions() {
+               wp_set_current_user( $this->subscriber );
+               $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+       }
+
+       public function test_prepare_item() {
+               $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $term->term_id );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->check_taxonomy_term( $term, $data, $response->get_links() );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 8, count( $properties ) );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'count', $properties );
+               $this->assertArrayHasKey( 'description', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'name', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'taxonomy', $properties );
+               $this->assertEquals( array_keys( get_taxonomies() ), $properties['taxonomy']['enum'] );
+       }
+
+       public function test_get_item_schema_non_hierarchical() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertFalse( isset( $properties['parent'] ) );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'tag', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               $tag_id = $this->factory->tag->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $tag_id );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function test_additional_field_update_errors() {
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'tag', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               wp_set_current_user( $this->administrator );
+               $tag_id = $this->factory->tag->create();
+               // Check for error on update.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/tags/%d', $tag_id ) );
+               $request->set_body_params( array(
+                       'my_custom_int' => 'returnError',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object, $request ) {
+               return 123;
+       }
+
+       public function additional_field_update_callback( $value, $tag ) {
+               if ( 'returnError' === $value ) {
+                       return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
+               }
+       }
+
+       public function tearDown() {
+               _unregister_taxonomy( 'batman' );
+               _unregister_taxonomy( 'robin' );
+               parent::tearDown();
+       }
+
+       protected function check_get_taxonomy_terms_response( $response ) {
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $args = array(
+                       'hide_empty' => false,
+               );
+               $tags = get_terms( 'post_tag', $args );
+               $this->assertEquals( count( $tags ), count( $data ) );
+               $this->assertEquals( $tags[0]->term_id, $data[0]['id'] );
+               $this->assertEquals( $tags[0]->name, $data[0]['name'] );
+               $this->assertEquals( $tags[0]->slug, $data[0]['slug'] );
+               $this->assertEquals( $tags[0]->taxonomy, $data[0]['taxonomy'] );
+               $this->assertEquals( $tags[0]->description, $data[0]['description'] );
+               $this->assertEquals( $tags[0]->count, $data[0]['count'] );
+       }
+
+       protected function check_taxonomy_term( $term, $data, $links ) {
+               $this->assertEquals( $term->term_id, $data['id'] );
+               $this->assertEquals( $term->name, $data['name'] );
+               $this->assertEquals( $term->slug, $data['slug'] );
+               $this->assertEquals( $term->description, $data['description'] );
+               $this->assertEquals( get_term_link( $term ),  $data['link'] );
+               $this->assertEquals( $term->count, $data['count'] );
+               $taxonomy = get_taxonomy( $term->taxonomy );
+               if ( $taxonomy->hierarchical ) {
+                       $this->assertEquals( $term->parent, $data['parent'] );
+               } else {
+                       $this->assertFalse( isset( $data['parent'] ) );
+               }
+               $expected_links = array(
+                       'self',
+                       'collection',
+                       'about',
+                       'https://api.w.org/post_type',
+               );
+               if ( $taxonomy->hierarchical && $term->parent ) {
+                       $expected_links[] = 'up';
+               }
+               $this->assertEqualSets( $expected_links, array_keys( $links ) );
+               $this->assertContains( 'wp/v2/taxonomies/' . $term->taxonomy, $links['about'][0]['href'] );
+               $this->assertEquals( add_query_arg( 'tags', $term->term_id, rest_url( 'wp/v2/posts' ) ), $links['https://api.w.org/post_type'][0]['href'] );
+       }
+
+       protected function check_get_taxonomy_term_response( $response, $id ) {
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $tag = get_term( $id, 'post_tag' );
+               $this->check_taxonomy_term( $tag, $data, $response->get_links() );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapiresttaxonomiescontrollerphp"></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-taxonomies-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php                         (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php   2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,198 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Taxonomies_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcase {
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+
+               $this->assertArrayHasKey( '/wp/v2/taxonomies', $routes );
+               $this->assertArrayHasKey( '/wp/v2/taxonomies/(?P<taxonomy>[\w-]+)', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/taxonomies' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/taxonomies/post_tag' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_get_items() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $taxonomies = $this->get_public_taxonomies( get_taxonomies( '', 'objects' ) );
+               $this->assertEquals( count( $taxonomies ), count( $data ) );
+               $this->assertEquals( 'Categories', $data['category']['name'] );
+               $this->assertEquals( 'category', $data['category']['slug'] );
+               $this->assertEquals( true, $data['category']['hierarchical'] );
+               $this->assertEquals( 'Tags', $data['post_tag']['name'] );
+               $this->assertEquals( 'post_tag', $data['post_tag']['slug'] );
+               $this->assertEquals( false, $data['post_tag']['hierarchical'] );
+       }
+
+       public function test_get_items_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
+       }
+
+       public function test_get_taxonomies_for_type() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
+               $request->set_param( 'type', 'post' );
+               $response = $this->server->dispatch( $request );
+               $this->check_taxonomies_for_type_response( 'post', $response );
+       }
+
+       public function test_get_taxonomies_for_invalid_type() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
+               $request->set_param( 'type', 'wingding' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( '{}', json_encode( $data ) );
+       }
+
+       public function test_get_item() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' );
+               $response = $this->server->dispatch( $request );
+               $this->check_taxonomy_object_response( 'view', $response );
+       }
+
+       public function test_get_item_edit_context() {
+               $editor_id = $this->factory->user->create( array( 'role' => 'editor' ) );
+               wp_set_current_user( $editor_id );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->check_taxonomy_object_response( 'edit', $response );
+       }
+
+       public function test_get_item_invalid_permission_for_context() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
+       }
+
+       public function test_get_invalid_taxonomy() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/invalid' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_taxonomy_invalid', $response, 404 );
+       }
+
+       public function test_get_non_public_taxonomy() {
+               register_taxonomy( 'api-private', 'post', array( 'public' => false ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/api-private' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden', $response, 403 );
+       }
+
+       public function test_create_item() {
+               /** Taxonomies can't be created **/
+       }
+
+       public function test_update_item() {
+               /** Taxonomies can't be updated **/
+       }
+
+       public function test_delete_item() {
+               /** Taxonomies can't be deleted **/
+       }
+
+       public function test_prepare_item() {
+               $tax = get_taxonomy( 'category' );
+               $endpoint = new WP_REST_Taxonomies_Controller;
+               $request = new WP_REST_Request;
+               $request->set_param( 'context', 'edit' );
+               $response = $endpoint->prepare_item_for_response( $tax, $request );
+               $this->check_taxonomy_object( 'edit', $tax, $response->get_data(), $response->get_links() );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/taxonomies' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+               $this->assertEquals( 8, count( $properties ) );
+               $this->assertArrayHasKey( 'capabilities', $properties );
+               $this->assertArrayHasKey( 'description', $properties );
+               $this->assertArrayHasKey( 'hierarchical', $properties );
+               $this->assertArrayHasKey( 'labels', $properties );
+               $this->assertArrayHasKey( 'name', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'show_cloud', $properties );
+               $this->assertArrayHasKey( 'types', $properties );
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+       }
+
+       /**
+        * Utility function for use in get_public_taxonomies
+        */
+       private function is_public( $taxonomy ) {
+               return ! empty( $taxonomy->show_in_rest );
+       }
+       /**
+        * Utility function to filter down to only public taxonomies
+        */
+       private function get_public_taxonomies( $taxonomies ) {
+               // Pass through array_values to re-index after filtering
+               return array_values( array_filter( $taxonomies, array( $this, 'is_public' ) ) );
+       }
+
+       protected function check_taxonomy_object( $context, $tax_obj, $data, $links ) {
+               $this->assertEquals( $tax_obj->label, $data['name'] );
+               $this->assertEquals( $tax_obj->name, $data['slug'] );
+               $this->assertEquals( $tax_obj->description, $data['description'] );
+               $this->assertEquals( $tax_obj->hierarchical, $data['hierarchical'] );
+               $this->assertEquals( rest_url( 'wp/v2/taxonomies' ), $links['collection'][0]['href'] );
+               $this->assertArrayHasKey( 'https://api.w.org/items', $links );
+               if ( 'edit' === $context ) {
+                       $this->assertEquals( $tax_obj->cap, $data['capabilities'] );
+                       $this->assertEquals( $tax_obj->labels, $data['labels'] );
+                       $this->assertEquals( $tax_obj->show_tagcloud, $data['show_cloud'] );
+               } else {
+                       $this->assertFalse( isset( $data['capabilities'] ) );
+                       $this->assertFalse( isset( $data['labels'] ) );
+                       $this->assertFalse( isset( $data['show_cloud'] ) );
+               }
+       }
+
+       protected function check_taxonomy_object_response( $context, $response ) {
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $category = get_taxonomy( 'category' );
+               $this->check_taxonomy_object( $context, $category, $data, $response->get_links() );
+       }
+
+       protected function check_taxonomies_for_type_response( $type, $response ) {
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $taxonomies = $this->get_public_taxonomies( get_object_taxonomies( $type, 'objects' ) );
+               $this->assertEquals( count( $taxonomies ), count( $data ) );
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapiresttestcontrollerphp"></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-test-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-test-controller.php                               (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-test-controller.php 2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,78 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Controller functionality
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_REST_Test_Controller extends WP_REST_Controller {
+
+       /**
+        * Get the Post type's schema, conforming to JSON Schema
+        *
+        * @return array
+        */
+       public function get_item_schema() {
+               $schema = array(
+                       '$schema'              => 'http://json-schema.org/draft-04/schema#',
+                       'title'                => 'type',
+                       'type'                 => 'object',
+                       'properties'           => array(
+                               'somestring'       => array(
+                                       'type'         => 'string',
+                                       'description'  => 'A pretty string.',
+                                       'context'      => array( 'view' ),
+                               ),
+                               'someinteger'      => array(
+                                       'type'         => 'integer',
+                                       'context'      => array( 'view' ),
+                               ),
+                               'someboolean'      => array(
+                                       'type'         => 'boolean',
+                                       'context'      => array( 'view' ),
+                               ),
+                               'someurl'          => array(
+                                       'type'         => 'string',
+                                       'format'       => 'uri',
+                                       'context'      => array( 'view' ),
+                               ),
+                               'somedate'             => array(
+                                       'type'         => 'string',
+                                       'format'       => 'date-time',
+                                       'context'      => array( 'view' ),
+                               ),
+                               'someemail'             => array(
+                                       'type'         => 'string',
+                                       'format'       => 'email',
+                                       'context'      => array( 'view' ),
+                               ),
+                               'someenum'         => array(
+                                       'type'         => 'string',
+                                       'enum'         => array( 'a', 'b', 'c' ),
+                                       'context'      => array( 'view' ),
+                               ),
+                               'someargoptions'   => array(
+                                       'type'         => 'integer',
+                                       'required'     => true,
+                                       'arg_options'  => array(
+                                               'required'          => false,
+                                               'sanitize_callback' => '__return_true',
+                                       ),
+                               ),
+                               'somedefault'      => array(
+                                       'type'         => 'string',
+                                       'enum'         => array( 'a', 'b', 'c' ),
+                                       'context'      => array( 'view' ),
+                                       'default'      => 'a',
+                               ),
+                       ),
+               );
+
+               return $schema;
+       }
+
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapirestuserscontrollerphp"></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-users-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-users-controller.php                              (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-users-controller.php        2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1320 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Users_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+/**
+ * @group restapi
+ */
+class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase {
+       /**
+        * This function is run before each method
+        */
+       public function setUp() {
+               parent::setUp();
+
+               $this->user = $this->factory->user->create( array(
+                       'role' => 'administrator',
+               ) );
+
+               $this->editor = $this->factory->user->create( array(
+                       'role'       => 'editor',
+                       'user_email' => 'editor@example.com',
+               ) );
+
+               $this->endpoint = new WP_REST_Users_Controller();
+       }
+
+       public function test_register_routes() {
+               $routes = $this->server->get_routes();
+
+               $this->assertArrayHasKey( '/wp/v2/users', $routes );
+               $this->assertCount( 2, $routes['/wp/v2/users'] );
+               $this->assertArrayHasKey( '/wp/v2/users/(?P<id>[\d]+)', $routes );
+               $this->assertCount( 3, $routes['/wp/v2/users/(?P<id>[\d]+)'] );
+               $this->assertArrayHasKey( '/wp/v2/users/me', $routes );
+       }
+
+       public function test_context_param() {
+               // Collection
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+               // Single
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users/' . $this->user );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
+               $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
+       }
+
+       public function test_registered_query_params() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $keys = array_keys( $data['endpoints'][0]['args'] );
+               sort( $keys );
+               $this->assertEquals( array(
+                       'context',
+                       'exclude',
+                       'include',
+                       'offset',
+                       'order',
+                       'orderby',
+                       'page',
+                       'per_page',
+                       'roles',
+                       'search',
+                       'slug',
+                       ), $keys );
+       }
+
+       public function test_get_items() {
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'context', 'view' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $all_data = $response->get_data();
+               $data = $all_data[0];
+               $userdata = get_userdata( $data['id'] );
+               $this->check_user_data( $userdata, $data, 'view', $data['_links'] );
+       }
+
+       public function test_get_items_with_edit_context() {
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               $all_data = $response->get_data();
+               $data = $all_data[0];
+               $userdata = get_userdata( $data['id'] );
+               $this->check_user_data( $userdata, $data, 'edit', $data['_links'] );
+       }
+
+       public function test_get_items_with_edit_context_without_permission() {
+               //test with a user not logged in
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 401, $response->get_status() );
+
+               //test with a user logged in but without sufficient capabilities; capability in question: 'list_users'
+               wp_set_current_user( $this->editor );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 403, $response->get_status() );
+       }
+
+       public function test_get_items_unauthenticated_only_shows_public_users() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( array(), $response->get_data() );
+
+               $this->factory->post->create( array( 'post_author' => $this->editor ) );
+               $this->factory->post->create( array( 'post_author' => $this->user, 'post_status' => 'draft' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $users = $response->get_data();
+
+               foreach ( $users as $user ) {
+                       $this->assertTrue( count_user_posts( $user['id'] ) > 0 );
+
+                       // Ensure we don't expose non-public data
+                       $this->assertArrayNotHasKey( 'capabilities', $user );
+                       $this->assertArrayNotHasKey( 'email', $user );
+                       $this->assertArrayNotHasKey( 'roles', $user );
+               }
+       }
+
+       /**
+        * @group test
+        */
+       public function test_get_items_pagination_headers() {
+               wp_set_current_user( $this->user );
+               // Start of the index, including the three existing users
+               for ( $i = 0; $i < 47; $i++ ) {
+                       $this->factory->user->create( array(
+                               'name'   => "User {$i}",
+                               ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 50, $headers['X-WP-Total'] );
+               $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( 'wp/v2/users' ) );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // 3rd page
+               $this->factory->user->create( array(
+                               'name'   => 'User 51',
+                               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 2,
+                       ), rest_url( 'wp/v2/users' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $next_link = add_query_arg( array(
+                       'page'    => 4,
+                       ), rest_url( 'wp/v2/users' ) );
+               $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
+               // Last page
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'page', 6 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 5,
+                       ), rest_url( 'wp/v2/users' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+               // Out of bounds
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'page', 8 );
+               $response = $this->server->dispatch( $request );
+               $headers = $response->get_headers();
+               $this->assertEquals( 51, $headers['X-WP-Total'] );
+               $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
+               $prev_link = add_query_arg( array(
+                       'page'    => 6,
+                       ), rest_url( 'wp/v2/users' ) );
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+               $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
+       }
+
+       public function test_get_items_per_page() {
+               wp_set_current_user( $this->user );
+               for ( $i = 0; $i < 20; $i++ ) {
+                       $this->factory->user->create( array( 'display_name' => "User {$i}" ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 10, count( $response->get_data() ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'per_page', 5 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 5, count( $response->get_data() ) );
+       }
+
+       public function test_get_items_page() {
+               wp_set_current_user( $this->user );
+               for ( $i = 0; $i < 20; $i++ ) {
+                       $this->factory->user->create( array( 'display_name' => "User {$i}" ) );
+               }
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'per_page', 5 );
+               $request->set_param( 'page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 5, count( $response->get_data() ) );
+               $prev_link = add_query_arg( array(
+                       'per_page'  => 5,
+                       'page'      => 1,
+                       ), rest_url( 'wp/v2/users' ) );
+               $headers = $response->get_headers();
+               $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
+       }
+
+       public function test_get_items_orderby_name() {
+               wp_set_current_user( $this->user );
+               $low_id = $this->factory->user->create( array( 'display_name' => 'AAAAA' ) );
+               $mid_id = $this->factory->user->create( array( 'display_name' => 'NNNNN' ) );
+               $high_id = $this->factory->user->create( array( 'display_name' => 'ZZZZ' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'name' );
+               $request->set_param( 'order', 'desc' );
+               $request->set_param( 'per_page', 1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $high_id, $data[0]['id'] );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'name' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'per_page', 1 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $low_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_orderby_url() {
+               wp_set_current_user( $this->user );
+
+               $low_id = $this->factory->user->create( array( 'user_url' => 'http://a.com' ) );
+               $high_id = $this->factory->user->create( array( 'user_url' => 'http://b.com' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'url' );
+               $request->set_param( 'order', 'desc' );
+               $request->set_param( 'per_page', 1 );
+               $request->set_param( 'include', array( $low_id, $high_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( $high_id, $data[0]['id'] );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'url' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'per_page', 1 );
+               $request->set_param( 'include', array( $low_id, $high_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $low_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_orderby_slug() {
+               wp_set_current_user( $this->user );
+
+               $high_id = $this->factory->user->create( array( 'user_nicename' => 'blogin' ) );
+               $low_id = $this->factory->user->create( array( 'user_nicename' => 'alogin' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'slug' );
+               $request->set_param( 'order', 'desc' );
+               $request->set_param( 'per_page', 1 );
+               $request->set_param( 'include', array( $low_id, $high_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( $high_id, $data[0]['id'] );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'slug' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'per_page', 1 );
+               $request->set_param( 'include', array( $low_id, $high_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $low_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_orderby_email() {
+               wp_set_current_user( $this->user );
+
+               $high_id = $this->factory->user->create( array( 'user_email' => 'bemail@gmail.com' ) );
+               $low_id = $this->factory->user->create( array( 'user_email' => 'aemail@gmail.com' ) );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'email' );
+               $request->set_param( 'order', 'desc' );
+               $request->set_param( 'per_page', 1 );
+               $request->set_param( 'include', array( $low_id, $high_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $high_id, $data[0]['id'] );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'email' );
+               $request->set_param( 'order', 'asc' );
+               $request->set_param( 'per_page', 1 );
+               $request->set_param( 'include', array( $low_id, $high_id ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( $low_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_orderby_email_unauthenticated() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'email' );
+               $request->set_param( 'order', 'desc' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_orderby', $response, 401 );
+       }
+
+       public function test_get_items_orderby_registered_date_unauthenticated() {
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'orderby', 'registered_date' );
+               $request->set_param( 'order', 'desc' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_forbidden_orderby', $response, 401 );
+       }
+
+       public function test_get_items_offset() {
+               wp_set_current_user( $this->user );
+               // 2 users created in __construct(), plus default user
+               $this->factory->user->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'offset', 1 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 3, $response->get_data() );
+               // 'offset' works with 'per_page'
+               $request->set_param( 'per_page', 2 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+               // 'offset' takes priority over 'page'
+               $request->set_param( 'page', 3 );
+               $response = $this->server->dispatch( $request );
+               $this->assertCount( 2, $response->get_data() );
+       }
+
+       public function test_get_items_include_query() {
+               wp_set_current_user( $this->user );
+               $id1 = $this->factory->user->create();
+               $id2 = $this->factory->user->create();
+               $id3 = $this->factory->user->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               // Orderby=>asc
+               $request->set_param( 'include', array( $id3, $id1 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id1, $data[0]['id'] );
+               // Orderby=>include
+               $request->set_param( 'orderby', 'include' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $id3, $data[0]['id'] );
+               // No privileges
+               wp_set_current_user( 0 );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 0, count( $data ) );
+
+       }
+
+       public function test_get_items_exclude_query() {
+               wp_set_current_user( $this->user );
+               $id1 = $this->factory->user->create();
+               $id2 = $this->factory->user->create();
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+               $request->set_param( 'exclude', array( $id2 ) );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
+               $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
+       }
+
+       public function test_get_items_search() {
+               wp_set_current_user( $this->user );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'search', 'yololololo' );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 0, count( $response->get_data() ) );
+               $yolo_id = $this->factory->user->create( array( 'display_name' => 'yololololo' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'search', (string) $yolo_id );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 1, count( $response->get_data() ) );
+               // default to wildcard search
+               $adam_id = $this->factory->user->create( array(
+                       'role'          => 'author',
+                       'user_nicename' => 'adam',
+               ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'search', 'ada' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $adam_id, $data[0]['id'] );
+       }
+
+       public function test_get_items_slug_query() {
+               wp_set_current_user( $this->user );
+               $this->factory->user->create( array( 'display_name' => 'foo', 'user_login' => 'bar' ) );
+               $id2 = $this->factory->user->create( array( 'display_name' => 'Moo', 'user_login' => 'foo' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'slug', 'foo' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $id2, $data[0]['id'] );
+       }
+
+       // Note: Do not test using editor role as there is an editor role created in testing and it makes it hard to test this functionality.
+       public function test_get_items_roles() {
+               wp_set_current_user( $this->user );
+               $tango = $this->factory->user->create( array( 'display_name' => 'tango', 'role' => 'subscriber' ) );
+               $yolo  = $this->factory->user->create( array( 'display_name' => 'yolo', 'role' => 'author' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'roles', 'author,subscriber' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 2, count( $data ) );
+               $this->assertEquals( $tango, $data[0]['id'] );
+               $this->assertEquals( $yolo, $data[1]['id'] );
+               $request->set_param( 'roles', 'author' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $yolo, $data[0]['id'] );
+               wp_set_current_user( 0 );
+               $request->set_param( 'roles', 'author' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 );
+               wp_set_current_user( $this->editor );
+               $request->set_param( 'roles', 'author' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_user_cannot_view', $response, 403 );
+       }
+
+       public function test_get_items_invalid_roles() {
+               wp_set_current_user( $this->user );
+               $lolz = $this->factory->user->create( array( 'display_name' => 'lolz', 'role' => 'author' ) );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'roles', 'ilovesteak,author' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 1, count( $data ) );
+               $this->assertEquals( $lolz, $data[0]['id'] );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
+               $request->set_param( 'roles', 'steakisgood' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 0, count( $data ) );
+               $this->assertEquals( array(), $data );
+       }
+
+       public function test_get_item() {
+               $user_id = $this->factory->user->create();
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $user_id ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->check_get_user_response( $response, 'embed' );
+       }
+
+       public function test_prepare_item() {
+               wp_set_current_user( $this->user );
+               $request = new WP_REST_Request;
+               $request->set_param( 'context', 'edit' );
+               $user = get_user_by( 'id', get_current_user_id() );
+               $data = $this->endpoint->prepare_item_for_response( $user, $request );
+               $this->check_get_user_response( $data, 'edit' );
+       }
+
+       public function test_get_user_avatar_urls() {
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->editor ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $data = $response->get_data();
+               $this->assertArrayHasKey( 24,  $data['avatar_urls'] );
+               $this->assertArrayHasKey( 48,  $data['avatar_urls'] );
+               $this->assertArrayHasKey( 96,  $data['avatar_urls'] );
+
+               $user = get_user_by( 'id', $this->editor );
+               /**
+                * Ignore the subdomain, since 'get_avatar_url randomly sets the Gravatar
+                * server when building the url string.
+                */
+               $this->assertEquals( substr( get_avatar_url( $user->user_email ), 9 ), substr( $data['avatar_urls'][96], 9 ) );
+       }
+
+       public function test_get_user_invalid_id() {
+               wp_set_current_user( $this->user );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users/100' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
+       }
+
+       public function test_get_user_empty_capabilities() {
+               wp_set_current_user( $this->user );
+               $this->allow_user_to_manage_multisite();
+
+               $lolz = $this->factory->user->create( array( 'display_name' => 'lolz', 'roles' => '' ) );
+               delete_user_option( $lolz, 'capabilities' );
+               delete_user_option( $lolz, 'user_level' );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users/' . $lolz );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertEquals( $data['capabilities'], new stdClass() );
+               $this->assertEquals( $data['extra_capabilities'], new stdClass() );
+       }
+
+       public function test_get_item_without_permission() {
+               wp_set_current_user( $this->editor );
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->user ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_cannot_view', $response, 403 );
+       }
+
+       public function test_get_item_published_author_post() {
+               $this->author_id = $this->factory->user->create( array(
+                       'role' => 'author',
+               ) );
+               $this->post_id = $this->factory->post->create( array(
+                       'post_author' => $this->author_id,
+               ));
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->check_get_user_response( $response, 'embed' );
+       }
+
+       public function test_get_item_published_author_pages() {
+               $this->author_id = $this->factory->user->create( array(
+                       'role' => 'author',
+               ) );
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 401, $response->get_status() );
+               $this->post_id = $this->factory->post->create( array(
+                       'post_author' => $this->author_id,
+                       'post_type'   => 'page',
+               ));
+               $response = $this->server->dispatch( $request );
+               $this->check_get_user_response( $response, 'embed' );
+       }
+
+       public function test_get_user_with_edit_context() {
+               $user_id = $this->factory->user->create();
+               $this->allow_user_to_manage_multisite();
+
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request->set_param( 'context', 'edit' );
+
+               $response = $this->server->dispatch( $request );
+               $this->check_get_user_response( $response, 'edit' );
+       }
+
+       public function test_get_item_published_author_wrong_context() {
+               $this->author_id = $this->factory->user->create( array(
+                       'role' => 'author',
+               ) );
+               $this->post_id = $this->factory->post->create( array(
+                       'post_author' => $this->author_id,
+               ));
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) );
+               $request->set_param( 'context', 'edit' );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 );
+       }
+
+       public function test_get_current_user() {
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users/me' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 302, $response->get_status() );
+
+               $headers = $response->get_headers();
+               $this->assertArrayHasKey( 'Location', $headers );
+               $this->assertEquals( rest_url( 'wp/v2/users/' . $this->user ), $headers['Location'] );
+       }
+
+       public function test_get_current_user_without_permission() {
+               wp_set_current_user( 0 );
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users/me' );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
+       }
+
+       public function test_create_item() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'username'    => 'testuser',
+                       'password'    => 'testpassword',
+                       'email'       => 'test@example.com',
+                       'name'        => 'Test User',
+                       'nickname'    => 'testuser',
+                       'slug'        => 'test-user',
+                       'role'        => 'editor',
+                       'description' => 'New API User',
+                       'url'         => 'http://example.com',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $this->assertEquals( 'http://example.com', $data['url'] );
+               $this->check_add_edit_user_response( $response );
+       }
+
+       public function test_json_create_user() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'username' => 'testjsonuser',
+                       'password' => 'testjsonpassword',
+                       'email'    => 'testjson@example.com',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->check_add_edit_user_response( $response );
+       }
+
+       public function test_create_user_without_permission() {
+               wp_set_current_user( $this->editor );
+
+               $params = array(
+                       'username' => 'homersimpson',
+                       'password' => 'stupidsexyflanders',
+                       'email'    => 'chunkylover53@aol.com',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_create_user', $response, 403 );
+       }
+
+       public function test_create_user_invalid_id() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'id'       => '156',
+                       'username' => 'lisasimpson',
+                       'password' => 'DavidHasselhoff',
+                       'email'    => 'smartgirl63_@yahoo.com',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_exists', $response, 400 );
+       }
+
+       public function test_create_user_invalid_email() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'username' => 'lisasimpson',
+                       'password' => 'DavidHasselhoff',
+                       'email'    => 'something',
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       public function test_create_user_invalid_role() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'username' => 'maggiesimpson',
+                       'password' => 'i_shot_mrburns',
+                       'email'    => 'packingheat@example.com',
+                       'roles'    => array( 'baby' ),
+               );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_role', $response, 400 );
+       }
+
+       public function test_update_item() {
+               $user_id = $this->factory->user->create( array(
+                       'user_email' => 'test@example.com',
+                       'user_pass' => 'sjflsfls',
+                       'user_login' => 'test_update',
+                       'first_name' => 'Old Name',
+                       'user_url' => 'http://apple.com',
+               ));
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $userdata = get_userdata( $user_id );
+               $pw_before = $userdata->user_pass;
+
+               $_POST['email'] = $userdata->user_email;
+               $_POST['username'] = $userdata->user_login;
+               $_POST['first_name'] = 'New Name';
+               $_POST['url'] = 'http://google.com';
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $_POST );
+
+               $response = $this->server->dispatch( $request );
+               $this->check_add_edit_user_response( $response, true );
+
+               // Check that the name has been updated correctly
+               $new_data = $response->get_data();
+               $this->assertEquals( 'New Name', $new_data['first_name'] );
+               $user = get_userdata( $user_id );
+               $this->assertEquals( 'New Name', $user->first_name );
+
+               $this->assertEquals( 'http://google.com', $new_data['url'] );
+               $this->assertEquals( 'http://google.com', $user->user_url );
+
+               // Check that we haven't inadvertently changed the user's password,
+               // as per https://core.trac.wordpress.org/ticket/21429
+               $this->assertEquals( $pw_before, $user->user_pass );
+       }
+
+       public function test_update_item_existing_email() {
+               $user1 = $this->factory->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com' ) );
+               $user2 = $this->factory->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com' ) );
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/users/' . $user2 );
+               $request->set_param( 'email', 'testjson@example.com' );
+               $response = $this->server->dispatch( $request );
+               $this->assertInstanceOf( 'WP_Error', $response->as_error() );
+               $this->assertEquals( 'rest_user_invalid_email', $response->as_error()->get_error_code() );
+       }
+
+       public function test_update_item_username_attempt() {
+               $user1 = $this->factory->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com' ) );
+               $user2 = $this->factory->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com' ) );
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/users/' . $user2 );
+               $request->set_param( 'username', 'test_json_user' );
+               $response = $this->server->dispatch( $request );
+               $this->assertInstanceOf( 'WP_Error', $response->as_error() );
+               $this->assertEquals( 'rest_user_invalid_argument', $response->as_error()->get_error_code() );
+       }
+
+       public function test_update_item_existing_nicename() {
+               $user1 = $this->factory->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com' ) );
+               $user2 = $this->factory->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com' ) );
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'PUT', '/wp/v2/users/' . $user2 );
+               $request->set_param( 'slug', 'test_json_user' );
+               $response = $this->server->dispatch( $request );
+               $this->assertInstanceOf( 'WP_Error', $response->as_error() );
+               $this->assertEquals( 'rest_user_invalid_slug', $response->as_error()->get_error_code() );
+       }
+
+       public function test_json_update_user() {
+               $user_id = $this->factory->user->create( array(
+                       'user_email' => 'testjson2@example.com',
+                       'user_pass'  => 'sjflsfl3sdjls',
+                       'user_login' => 'test_json_update',
+                       'first_name' => 'Old Name',
+                       'last_name'  => 'Original Last',
+               ));
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'username'   => 'test_json_update',
+                       'email'      => 'testjson2@example.com',
+                       'first_name' => 'JSON Name',
+                       'last_name'  => 'New Last',
+               );
+
+               $userdata = get_userdata( $user_id );
+               $pw_before = $userdata->user_pass;
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request->add_header( 'content-type', 'application/json' );
+               $request->set_body( wp_json_encode( $params ) );
+
+               $response = $this->server->dispatch( $request );
+               $this->check_add_edit_user_response( $response, true );
+
+               // Check that the name has been updated correctly
+               $new_data = $response->get_data();
+               $this->assertEquals( 'JSON Name', $new_data['first_name'] );
+               $this->assertEquals( 'New Last', $new_data['last_name'] );
+               $user = get_userdata( $user_id );
+               $this->assertEquals( 'JSON Name', $user->first_name );
+               $this->assertEquals( 'New Last', $user->last_name );
+
+               // Check that we haven't inadvertently changed the user's password,
+               // as per https://core.trac.wordpress.org/ticket/21429
+               $this->assertEquals( $pw_before, $user->user_pass );
+       }
+
+       public function test_update_user_role() {
+               $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+               wp_set_current_user( $this->user );
+               $this->allow_user_to_manage_multisite();
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request->set_param( 'roles', array( 'editor' ) );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+
+               $this->assertEquals( 'editor', $new_data['roles'][0] );
+               $this->assertNotEquals( 'administrator', $new_data['roles'][0] );
+
+               $user = get_userdata( $user_id );
+               $this->assertArrayHasKey( 'editor', $user->caps );
+               $this->assertArrayNotHasKey( 'administrator', $user->caps );
+       }
+
+       public function test_update_user_role_invalid_privilege_escalation() {
+               wp_set_current_user( $this->editor );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->editor ) );
+               $request->set_param( 'roles', array( 'administrator' ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_edit_roles', $response, 403 );
+               $user = get_userdata( $this->editor );
+               $this->assertArrayHasKey( 'editor', $user->caps );
+               $this->assertArrayNotHasKey( 'administrator', $user->caps );
+       }
+
+       public function test_update_user_role_invalid_privilege_deescalation() {
+               if ( is_multisite() ) {
+                       return $this->markTestSkipped( 'Test only intended for single site.' );
+               }
+
+               $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+               wp_set_current_user( $user_id );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request->set_param( 'roles', array( 'editor' ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_role', $response, 403 );
+
+               $user = get_userdata( $user_id );
+               $this->assertArrayHasKey( 'administrator', $user->caps );
+               $this->assertArrayNotHasKey( 'editor', $user->caps );
+       }
+
+       public function test_update_user_role_privilege_deescalation_multisite() {
+               if ( ! is_multisite() ) {
+                       return $this->markTestSkipped( 'Test only intended for multisite.' );
+               }
+
+               $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+               wp_set_current_user( $user_id );
+               $user = wp_get_current_user();
+               update_site_option( 'site_admins', array( $user->user_login ) );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request->set_param( 'roles', array( 'editor' ) );
+               $response = $this->server->dispatch( $request );
+
+               $new_data = $response->get_data();
+               $this->assertEquals( 'editor', $new_data['roles'][0] );
+               $this->assertNotEquals( 'administrator', $new_data['roles'][0] );
+       }
+
+
+       public function test_update_user_role_invalid_role() {
+               wp_set_current_user( $this->user );
+               $this->allow_user_to_manage_multisite();
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->editor ) );
+               $request->set_param( 'roles', array( 'BeSharp' ) );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_role', $response, 400 );
+
+               $user = get_userdata( $this->editor );
+               $this->assertArrayHasKey( 'editor', $user->caps );
+               $this->assertArrayNotHasKey( 'BeSharp', $user->caps );
+       }
+
+       public function test_update_user_without_permission() {
+               wp_set_current_user( $this->editor );
+
+               $params = array(
+                       'username' => 'homersimpson',
+                       'password' => 'stupidsexyflanders',
+                       'email'    => 'chunkylover53@aol.com',
+               );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->user ) );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
+       }
+
+       public function test_update_user_invalid_id() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $params = array(
+                       'id'       => '156',
+                       'username' => 'lisasimpson',
+                       'password' => 'DavidHasselhoff',
+                       'email'    => 'smartgirl63_@yahoo.com',
+               );
+
+               $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->editor ) );
+               $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
+               $request->set_body_params( $params );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
+       }
+
+       public function test_delete_item() {
+               $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) );
+
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $userdata = get_userdata( $user_id ); // cache for later
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request['force'] = true;
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+               $data = $response->get_data();
+               $this->assertEquals( 'Deleted User', $data['name'] );
+       }
+
+       public function test_delete_item_no_trash() {
+               $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) );
+
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $userdata = get_userdata( $user_id ); // cache for later
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $response = $this->server->dispatch( $request );
+               $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 );
+
+               // Ensure the user still exists
+               $user = get_user_by( 'id', $user_id );
+               $this->assertNotEmpty( $user );
+       }
+
+       public function test_delete_user_without_permission() {
+               $user_id = $this->factory->user->create();
+
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->editor );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request['force'] = true;
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_cannot_delete', $response, 403 );
+       }
+
+       public function test_delete_user_invalid_id() {
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'DELETE', '/wp/v2/users/100' );
+               $request['force'] = true;
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
+       }
+
+       public function test_delete_user_reassign() {
+               $this->allow_user_to_manage_multisite();
+
+               // Test with a new user, to avoid any complications
+               $user_id = $this->factory->user->create();
+               $reassign_id = $this->factory->user->create();
+               $test_post = $this->factory->post->create(array(
+                       'post_author' => $user_id,
+               ));
+
+               // Sanity check to ensure the factory created the post correctly
+               $post = get_post( $test_post );
+               $this->assertEquals( $user_id, $post->post_author );
+
+               // Delete our test user, and reassign to the new author
+               wp_set_current_user( $this->user );
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request['force'] = true;
+               $request->set_param( 'reassign', $reassign_id );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 200, $response->get_status() );
+
+               // Check that the post has been updated correctly
+               $post = get_post( $test_post );
+               $this->assertEquals( $reassign_id, $post->post_author );
+       }
+
+       public function test_delete_user_invalid_reassign_id() {
+               $user_id = $this->factory->user->create();
+
+               $this->allow_user_to_manage_multisite();
+               wp_set_current_user( $this->user );
+
+               $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
+               $request['force'] = true;
+               $request->set_param( 'reassign', 100 );
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_user_invalid_reassign', $response, 400 );
+       }
+
+       public function test_get_item_schema() {
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+
+               $this->assertEquals( 18, count( $properties ) );
+               $this->assertArrayHasKey( 'avatar_urls', $properties );
+               $this->assertArrayHasKey( 'capabilities', $properties );
+               $this->assertArrayHasKey( 'description', $properties );
+               $this->assertArrayHasKey( 'email', $properties );
+               $this->assertArrayHasKey( 'extra_capabilities', $properties );
+               $this->assertArrayHasKey( 'first_name', $properties );
+               $this->assertArrayHasKey( 'id', $properties );
+               $this->assertArrayHasKey( 'last_name', $properties );
+               $this->assertArrayHasKey( 'link', $properties );
+               $this->assertArrayHasKey( 'meta', $properties );
+               $this->assertArrayHasKey( 'name', $properties );
+               $this->assertArrayHasKey( 'nickname', $properties );
+               $this->assertArrayHasKey( 'registered_date', $properties );
+               $this->assertArrayHasKey( 'slug', $properties );
+               $this->assertArrayHasKey( 'password', $properties );
+               $this->assertArrayHasKey( 'url', $properties );
+               $this->assertArrayHasKey( 'username', $properties );
+               $this->assertArrayHasKey( 'roles', $properties );
+
+       }
+
+       public function test_get_item_schema_show_avatar() {
+               update_option( 'show_avatars', false );
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+               $properties = $data['schema']['properties'];
+
+               $this->assertArrayNotHasKey( 'avatar_urls', $properties );
+       }
+
+       public function test_get_additional_field_registration() {
+
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'embed', 'view', 'edit' ),
+               );
+
+               register_rest_field( 'user', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
+
+               $response = $this->server->dispatch( $request );
+               $data = $response->get_data();
+
+               $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
+               $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
+
+               wp_set_current_user( 1 );
+               if ( is_multisite() ) {
+                       $current_user = wp_get_current_user( 1 );
+                       update_site_option( 'site_admins', array( $current_user->user_login ) );
+               }
+
+               $request = new WP_REST_Request( 'GET', '/wp/v2/users/1' );
+
+               $response = $this->server->dispatch( $request );
+               $this->assertArrayHasKey( 'my_custom_int', $response->data );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users/1' );
+               $request->set_body_params(array(
+                       'my_custom_int' => 123,
+               ));
+
+               $response = $this->server->dispatch( $request );
+               $this->assertEquals( 123, get_user_meta( 1, 'my_custom_int', true ) );
+
+               $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
+               $request->set_body_params(array(
+                       'my_custom_int' => 123,
+                       'email' => 'joe@foobar.com',
+                       'username' => 'abc123',
+                       'password' => 'hello',
+               ));
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertEquals( 123, $response->data['my_custom_int'] );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function test_additional_field_update_errors() {
+               $schema = array(
+                       'type'        => 'integer',
+                       'description' => 'Some integer of mine',
+                       'enum'        => array( 1, 2, 3, 4 ),
+                       'context'     => array( 'view', 'edit' ),
+               );
+
+               register_rest_field( 'user', 'my_custom_int', array(
+                       'schema'          => $schema,
+                       'get_callback'    => array( $this, 'additional_field_get_callback' ),
+                       'update_callback' => array( $this, 'additional_field_update_callback' ),
+               ) );
+
+               wp_set_current_user( 1 );
+               if ( is_multisite() ) {
+                       $current_user = wp_get_current_user( 1 );
+                       update_site_option( 'site_admins', array( $current_user->user_login ) );
+               }
+
+               // Check for error on update.
+               $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', $this->user ) );
+               $request->set_body_params( array(
+                       'my_custom_int' => 'returnError',
+               ) );
+
+               $response = $this->server->dispatch( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+               global $wp_rest_additional_fields;
+               $wp_rest_additional_fields = array();
+       }
+
+       public function additional_field_get_callback( $object ) {
+               return get_user_meta( $object['id'], 'my_custom_int', true );
+       }
+
+       public function additional_field_update_callback( $value, $user ) {
+               if ( 'returnError' === $value ) {
+                       return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
+               }
+               update_user_meta( $user->ID, 'my_custom_int', $value );
+       }
+
+       public function tearDown() {
+               parent::tearDown();
+       }
+
+       protected function check_user_data( $user, $data, $context, $links ) {
+               $this->assertEquals( $user->ID, $data['id'] );
+               $this->assertEquals( $user->display_name, $data['name'] );
+               $this->assertEquals( $user->user_url, $data['url'] );
+               $this->assertEquals( $user->description, $data['description'] );
+               $this->assertEquals( get_author_posts_url( $user->ID ), $data['link'] );
+               $this->assertArrayHasKey( 'avatar_urls', $data );
+               $this->assertEquals( $user->user_nicename, $data['slug'] );
+
+               if ( 'edit' === $context ) {
+                       $this->assertEquals( $user->first_name, $data['first_name'] );
+                       $this->assertEquals( $user->last_name, $data['last_name'] );
+                       $this->assertEquals( $user->nickname, $data['nickname'] );
+                       $this->assertEquals( $user->user_email, $data['email'] );
+                       $this->assertEquals( (object) $user->allcaps, $data['capabilities'] );
+                       $this->assertEquals( (object) $user->caps, $data['extra_capabilities'] );
+                       $this->assertEquals( date( 'c', strtotime( $user->user_registered ) ), $data['registered_date'] );
+                       $this->assertEquals( $user->user_login, $data['username'] );
+                       $this->assertEquals( $user->roles, $data['roles'] );
+               }
+
+               if ( 'edit' !== $context ) {
+                       $this->assertArrayNotHasKey( 'roles', $data );
+                       $this->assertArrayNotHasKey( 'capabilities', $data );
+                       $this->assertArrayNotHasKey( 'registered', $data );
+                       $this->assertArrayNotHasKey( 'first_name', $data );
+                       $this->assertArrayNotHasKey( 'last_name', $data );
+                       $this->assertArrayNotHasKey( 'nickname', $data );
+                       $this->assertArrayNotHasKey( 'extra_capabilities', $data );
+                       $this->assertArrayNotHasKey( 'username', $data );
+               }
+
+               $this->assertEqualSets( array(
+                       'self',
+                       'collection',
+               ), array_keys( $links ) );
+
+               $this->assertArrayNotHasKey( 'password', $data );
+       }
+
+       protected function check_get_user_response( $response, $context = 'view' ) {
+               $this->assertEquals( 200, $response->get_status() );
+
+               $data = $response->get_data();
+               $userdata = get_userdata( $data['id'] );
+               $this->check_user_data( $userdata, $data, $context, $response->get_links() );
+       }
+
+       protected function check_add_edit_user_response( $response, $update = false ) {
+               if ( $update ) {
+                       $this->assertEquals( 200, $response->get_status() );
+               } else {
+                       $this->assertEquals( 201, $response->get_status() );
+               }
+
+               $data = $response->get_data();
+               $userdata = get_userdata( $data['id'] );
+               $this->check_user_data( $userdata, $data, 'edit', $response->get_links() );
+       }
+
+       protected function allow_user_to_manage_multisite() {
+               wp_set_current_user( $this->user );
+               $user = wp_get_current_user();
+
+               if ( is_multisite() ) {
+                       update_site_option( 'site_admins', array( $user->user_login ) );
+               }
+
+               return;
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestsrestapiphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api.php    2016-10-19 23:02:58 UTC (rev 38831)
+++ trunk/tests/phpunit/tests/rest-api.php      2016-10-20 02:54:12 UTC (rev 38832)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -26,6 +26,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( class_exists( 'WP_REST_Server' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( class_exists( 'WP_REST_Request' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertTrue( class_exists( 'WP_REST_Response' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertTrue( class_exists( 'WP_REST_Posts_Controller' ) );
</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">@@ -36,6 +37,35 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 10, has_action( 'init', 'rest_api_init' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function test_add_extra_api_taxonomy_arguments() {
+               $taxonomy = get_taxonomy( 'category' );
+               $this->assertTrue( $taxonomy->show_in_rest );
+               $this->assertEquals( 'categories', $taxonomy->rest_base );
+               $this->assertEquals( 'WP_REST_Terms_Controller', $taxonomy->rest_controller_class );
+
+               $taxonomy = get_taxonomy( 'post_tag' );
+               $this->assertTrue( $taxonomy->show_in_rest );
+               $this->assertEquals( 'tags', $taxonomy->rest_base );
+               $this->assertEquals( 'WP_REST_Terms_Controller', $taxonomy->rest_controller_class );
+       }
+
+       public function test_add_extra_api_post_type_arguments() {
+               $post_type = get_post_type_object( 'post' );
+               $this->assertTrue( $post_type->show_in_rest );
+               $this->assertEquals( 'posts', $post_type->rest_base );
+               $this->assertEquals( 'WP_REST_Posts_Controller', $post_type->rest_controller_class );
+
+               $post_type = get_post_type_object( 'page' );
+               $this->assertTrue( $post_type->show_in_rest );
+               $this->assertEquals( 'pages', $post_type->rest_base );
+               $this->assertEquals( 'WP_REST_Posts_Controller', $post_type->rest_controller_class );
+
+               $post_type = get_post_type_object( 'attachment' );
+               $this->assertTrue( $post_type->show_in_rest );
+               $this->assertEquals( 'media', $post_type->rest_base );
+               $this->assertEquals( 'WP_REST_Attachments_Controller', $post_type->rest_controller_class );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Check that a single route is canonicalized.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span></span></pre>
</div>
</div>

</body>
</html>