Make WordPress Core

Ticket #38373: 38373.diff

File 38373.diff, 783.4 KB (added by rachelbaker, 9 years ago)

Initial pass at generating a patch from github.com/WP-API/WP-API

  • src/wp-includes/default-filters.php

     
    374374
    375375// REST API actions.
    376376add_action( 'init',          'rest_api_init' );
    377 add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
     377add_action( 'rest_api_init', 'rest_api_default_filters',   10, 1 );
     378add_action( 'rest_api_init', 'create_initial_rest_routes', 99 );
    378379add_action( 'parse_request', 'rest_api_loaded' );
    379380
    380381/**
  • src/wp-includes/functions.php

     
    34303430}
    34313431
    34323432/**
     3433 * Clean up an array, comma- or space-separated list of slugs.
     3434 *
     3435 * @since 4.7.0
     3436 *
     3437 * @param  array|string $list List of slugs.
     3438 * @return array Sanitized array of slugs.
     3439 */
     3440function wp_parse_slug_list( $list ) {
     3441        if ( ! is_array( $list ) ) {
     3442                $list = preg_split( '/[\s,]+/', $list );
     3443        }
     3444
     3445        foreach ( $list as $key => $value ) {
     3446                $list[ $key ] = sanitize_title( $value );
     3447        }
     3448
     3449        return array_unique( $list );
     3450}
     3451
     3452/**
    34333453 * Extract a slice of an array, given a list of keys.
    34343454 *
    34353455 * @since 3.1.0
  • src/wp-includes/js/wp-api.js

     
     1(function( window, undefined ) {
     2
     3        'use strict';
     4
     5        /**
     6         * Initialise the WP_API.
     7         */
     8        function WP_API() {
     9                this.models = {};
     10                this.collections = {};
     11                this.views = {};
     12        }
     13
     14        window.wp            = window.wp || {};
     15        wp.api               = wp.api || new WP_API();
     16        wp.api.versionString = wp.api.versionString || 'wp/v2/';
     17
     18        // Alias _includes to _.contains, ensuring it is available if lodash is used.
     19        if ( ! _.isFunction( _.includes ) && _.isFunction( _.contains ) ) {
     20          _.includes = _.contains;
     21        }
     22
     23})( window );
     24
     25(function( window, undefined ) {
     26
     27        'use strict';
     28
     29        var pad, r;
     30
     31        window.wp = window.wp || {};
     32        wp.api = wp.api || {};
     33        wp.api.utils = wp.api.utils || {};
     34
     35        /**
     36         * ECMAScript 5 shim, adapted from MDN.
     37         * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
     38         */
     39        if ( ! Date.prototype.toISOString ) {
     40                pad = function( number ) {
     41                        r = String( number );
     42                        if ( 1 === r.length ) {
     43                                r = '0' + r;
     44                        }
     45
     46                        return r;
     47                };
     48
     49                Date.prototype.toISOString = function() {
     50                        return this.getUTCFullYear() +
     51                                '-' + pad( this.getUTCMonth() + 1 ) +
     52                                '-' + pad( this.getUTCDate() ) +
     53                                'T' + pad( this.getUTCHours() ) +
     54                                ':' + pad( this.getUTCMinutes() ) +
     55                                ':' + pad( this.getUTCSeconds() ) +
     56                                '.' + String( ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ) ).slice( 2, 5 ) +
     57                                'Z';
     58                };
     59        }
     60
     61        /**
     62         * Parse date into ISO8601 format.
     63         *
     64         * @param {Date} date.
     65         */
     66        wp.api.utils.parseISO8601 = function( date ) {
     67                var timestamp, struct, i, k,
     68                        minutesOffset = 0,
     69                        numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
     70
     71                // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
     72                // before falling back to any implementation-specific date parsing, so that’s what we do, even if native
     73                // implementations could be faster.
     74                //              1 YYYY                2 MM       3 DD           4 HH    5 mm       6 ss        7 msec        8 Z 9 ±    10 tzHH    11 tzmm
     75                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 ) ) ) {
     76
     77                        // Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC.
     78                        for ( i = 0; ( k = numericKeys[i] ); ++i ) {
     79                                struct[k] = +struct[k] || 0;
     80                        }
     81
     82                        // Allow undefined days and months.
     83                        struct[2] = ( +struct[2] || 1 ) - 1;
     84                        struct[3] = +struct[3] || 1;
     85
     86                        if ( 'Z' !== struct[8]  && undefined !== struct[9] ) {
     87                                minutesOffset = struct[10] * 60 + struct[11];
     88
     89                                if ( '+' === struct[9] ) {
     90                                        minutesOffset = 0 - minutesOffset;
     91                                }
     92                        }
     93
     94                        timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] );
     95                } else {
     96                        timestamp = Date.parse ? Date.parse( date ) : NaN;
     97                }
     98
     99                return timestamp;
     100        };
     101
     102        /**
     103         * Helper function for getting the root URL.
     104         * @return {[type]} [description]
     105         */
     106        wp.api.utils.getRootUrl = function() {
     107                return window.location.origin ?
     108                        window.location.origin + '/' :
     109                        window.location.protocol + '/' + window.location.host + '/';
     110        };
     111
     112        /**
     113         * Helper for capitalizing strings.
     114         */
     115        wp.api.utils.capitalize = function( str ) {
     116                if ( _.isUndefined( str ) ) {
     117                        return str;
     118                }
     119                return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
     120        };
     121
     122        /**
     123         * Extract a route part based on negative index.
     124         *
     125         * @param {string} route The endpoint route.
     126         * @param {int}    part  The number of parts from the end of the route to retrieve. Default 1.
     127         *                       Example route `/a/b/c`: part 1 is `c`, part 2 is `b`, part 3 is `a`.
     128         */
     129        wp.api.utils.extractRoutePart = function( route, part ) {
     130                var routeParts;
     131
     132                part  = part || 1;
     133
     134                // Remove versions string from route to avoid returning it.
     135                route = route.replace( wp.api.versionString, '' );
     136                routeParts = route.split( '/' ).reverse();
     137                if ( _.isUndefined( routeParts[ --part ] ) ) {
     138                        return '';
     139                }
     140                return routeParts[ part ];
     141        };
     142
     143        /**
     144         * Extract a parent name from a passed route.
     145         *
     146         * @param {string} route The route to extract a name from.
     147         */
     148        wp.api.utils.extractParentName = function( route ) {
     149                var name,
     150                        lastSlash = route.lastIndexOf( '_id>[\\d]+)/' );
     151
     152                if ( lastSlash < 0 ) {
     153                        return '';
     154                }
     155                name = route.substr( 0, lastSlash - 1 );
     156                name = name.split( '/' );
     157                name.pop();
     158                name = name.pop();
     159                return name;
     160        };
     161
     162        /**
     163         * Add args and options to a model prototype from a route's endpoints.
     164         *
     165         * @param {array}  routeEndpoints Array of route endpoints.
     166         * @param {Object} modelInstance  An instance of the model (or collection)
     167         *                                to add the args to.
     168         */
     169        wp.api.utils.decorateFromRoute = function( routeEndpoints, modelInstance ) {
     170
     171                /**
     172                 * Build the args based on route endpoint data.
     173                 */
     174                _.each( routeEndpoints, function( routeEndpoint ) {
     175
     176                        // Add post and edit endpoints as model args.
     177                        if ( _.includes( routeEndpoint.methods, 'POST' ) || _.includes( routeEndpoint.methods, 'PUT' ) ) {
     178
     179                                // Add any non empty args, merging them into the args object.
     180                                if ( ! _.isEmpty( routeEndpoint.args ) ) {
     181
     182                                        // Set as defauls if no args yet.
     183                                        if ( _.isEmpty( modelInstance.prototype.args ) ) {
     184                                                modelInstance.prototype.args = routeEndpoint.args;
     185                                        } else {
     186
     187                                                // We already have args, merge these new args in.
     188                                                modelInstance.prototype.args = _.union( routeEndpoint.args, modelInstance.prototype.defaults );
     189                                        }
     190                                }
     191                        } else {
     192
     193                                // Add GET method as model options.
     194                                if ( _.includes( routeEndpoint.methods, 'GET' ) ) {
     195
     196                                        // Add any non empty args, merging them into the defaults object.
     197                                        if ( ! _.isEmpty( routeEndpoint.args ) ) {
     198
     199                                                // Set as defauls if no defaults yet.
     200                                                if ( _.isEmpty( modelInstance.prototype.options ) ) {
     201                                                        modelInstance.prototype.options = routeEndpoint.args;
     202                                                } else {
     203
     204                                                        // We already have options, merge these new args in.
     205                                                        modelInstance.prototype.options = _.union( routeEndpoint.args, modelInstance.prototype.options );
     206                                                }
     207                                        }
     208
     209                                }
     210                        }
     211
     212                } );
     213
     214        };
     215
     216        /**
     217         * Add mixins and helpers to models depending on their defaults.
     218         *
     219         * @param {Backbone Model} model          The model to attach helpers and mixins to.
     220         * @param {string}         modelClassName The classname of the constructed model.
     221         * @param {Object}             loadingObjects An object containing the models and collections we are building.
     222         */
     223        wp.api.utils.addMixinsAndHelpers = function( model, modelClassName, loadingObjects ) {
     224
     225                var hasDate = false,
     226
     227                        /**
     228                         * Array of parseable dates.
     229                         *
     230                         * @type {string[]}.
     231                         */
     232                        parseableDates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ],
     233
     234                        /**
     235                         * Mixin for all content that is time stamped.
     236                         *
     237                         * This mixin converts between mysql timestamps and JavaScript Dates when syncing a model
     238                         * to or from the server. For example, a date stored as `2015-12-27T21:22:24` on the server
     239                         * gets expanded to `Sun Dec 27 2015 14:22:24 GMT-0700 (MST)` when the model is fetched.
     240                         *
     241                         * @type {{toJSON: toJSON, parse: parse}}.
     242                         */
     243                        TimeStampedMixin = {
     244
     245                                /**
     246                                 * Prepare a JavaScript Date for transmitting to the server.
     247                                 *
     248                                 * This helper function accepts a field and Date object. It converts the passed Date
     249                                 * to an ISO string and sets that on the model field.
     250                                 *
     251                                 * @param {Date}   date   A JavaScript date object. WordPress expects dates in UTC.
     252                                 * @param {string} field  The date field to set. One of 'date', 'date_gmt', 'date_modified'
     253                                 *                        or 'date_modified_gmt'. Optional, defaults to 'date'.
     254                                 */
     255                                setDate: function( date, field ) {
     256                                        var theField = field || 'date';
     257
     258                                        // Don't alter non parsable date fields.
     259                                        if ( _.indexOf( parseableDates, theField ) < 0 ) {
     260                                                return false;
     261                                        }
     262
     263                                        this.set( theField, date.toISOString() );
     264                                },
     265
     266                                /**
     267                                 * Get a JavaScript Date from the passed field.
     268                                 *
     269                                 * WordPress returns 'date' and 'date_modified' in the timezone of the server as well as
     270                                 * UTC dates as 'date_gmt' and 'date_modified_gmt'. Draft posts do not include UTC dates.
     271                                 *
     272                                 * @param {string} field  The date field to set. One of 'date', 'date_gmt', 'date_modified'
     273                                 *                        or 'date_modified_gmt'. Optional, defaults to 'date'.
     274                                 */
     275                                getDate: function( field ) {
     276                                        var theField   = field || 'date',
     277                                                theISODate = this.get( theField );
     278
     279                                        // Only get date fields and non null values.
     280                                        if ( _.indexOf( parseableDates, theField ) < 0 || _.isNull( theISODate ) ) {
     281                                                return false;
     282                                        }
     283
     284                                        return new Date( wp.api.utils.parseISO8601( theISODate ) );
     285                                }
     286                        },
     287
     288                        /**
     289                         * Build a helper function to retrieve related model.
     290                         *
     291                         * @param  {string} parentModel      The parent model.
     292                         * @param  {int}    modelId          The model ID if the object to request
     293                         * @param  {string} modelName        The model name to use when constructing the model.
     294                         * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
     295                         * @param  {string} embedCheckField  Which model field to check to see if the model has data.
     296                         *
     297                         * @return {Deferred.promise}        A promise which resolves to the constructed model.
     298                         */
     299                        buildModelGetter = function( parentModel, modelId, modelName, embedSourcePoint, embedCheckField ) {
     300                                var getModel, embeddeds, attributes, deferred;
     301
     302                                deferred  = jQuery.Deferred();
     303                                embeddeds = parentModel.get( '_embedded' ) || {};
     304
     305                                // Verify that we have a valied object id.
     306                                if ( ! _.isNumber( modelId ) || 0 === modelId ) {
     307                                        deferred.reject();
     308                                        return deferred;
     309                                }
     310
     311                                // If we have embedded object data, use that when constructing the getModel.
     312                                if ( embeddeds[ embedSourcePoint ] ) {
     313                                        attributes = _.findWhere( embeddeds[ embedSourcePoint ], { id: modelId } );
     314                                }
     315
     316                                // Otherwise use the modelId.
     317                                if ( ! attributes ) {
     318                                        attributes = { id: modelId };
     319                                }
     320
     321                                // Create the new getModel model.
     322                                getModel = new wp.api.models[ modelName ]( attributes );
     323
     324                                // If we didn’t have an embedded getModel, fetch the getModel data.
     325                                if ( ! getModel.get( embedCheckField ) ) {
     326                                        getModel.fetch( { success: function( getModel ) {
     327                                                deferred.resolve( getModel );
     328                                        } } );
     329                                } else {
     330                                        deferred.resolve( getModel );
     331                                }
     332
     333                                // Return a promise.
     334                                return deferred.promise();
     335                        },
     336
     337                        /**
     338                         * Build a helper to retrieve a collection.
     339                         *
     340                         * @param  {string} parentModel      The parent model.
     341                         * @param  {string} collectionName   The name to use when constructing the collection.
     342                         * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
     343                         * @param  {string} embedIndex       An addiitonal optional index for the _embed data.
     344                         *
     345                         * @return {Deferred.promise}        A promise which resolves to the constructed collection.
     346                         */
     347                        buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) {
     348                                /**
     349                                 * Returns a promise that resolves to the requested collection
     350                                 *
     351                                 * Uses the embedded data if available, otherwises fetches the
     352                                 * data from the server.
     353                                 *
     354                                 * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ]
     355                                 * collection.
     356                                 */
     357                                var postId, embeddeds, getObjects,
     358                                        classProperties = '',
     359                                        properties      = '',
     360                                        deferred        = jQuery.Deferred();
     361
     362                                postId    = parentModel.get( 'id' );
     363                                embeddeds = parentModel.get( '_embedded' ) || {};
     364
     365                                // Verify that we have a valied post id.
     366                                if ( ! _.isNumber( postId ) || 0 === postId ) {
     367                                        deferred.reject();
     368                                        return deferred;
     369                                }
     370
     371                                // If we have embedded getObjects data, use that when constructing the getObjects.
     372                                if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) {
     373
     374                                        // Some embeds also include an index offset, check for that.
     375                                        if ( _.isUndefined( embedIndex ) ) {
     376
     377                                                // Use the embed source point directly.
     378                                                properties = embeddeds[ embedSourcePoint ];
     379                                        } else {
     380
     381                                                // Add the index to the embed source point.
     382                                                properties = embeddeds[ embedSourcePoint ][ embedIndex ];
     383                                        }
     384                                } else {
     385
     386                                        // Otherwise use the postId.
     387                                        classProperties = { parent: postId };
     388                                }
     389
     390                                // Create the new getObjects collection.
     391                                getObjects = new wp.api.collections[ collectionName ]( properties, classProperties );
     392
     393                                // If we didn’t have embedded getObjects, fetch the getObjects data.
     394                                if ( _.isUndefined( getObjects.models[0] ) ) {
     395                                        getObjects.fetch( { success: function( getObjects ) {
     396
     397                                                // Add a helper 'parent_post' attribute onto the model.
     398                                                setHelperParentPost( getObjects, postId );
     399                                                deferred.resolve( getObjects );
     400                                        } } );
     401                                } else {
     402
     403                                        // Add a helper 'parent_post' attribute onto the model.
     404                                        setHelperParentPost( getObjects, postId );
     405                                        deferred.resolve( getObjects );
     406                                }
     407
     408                                // Return a promise.
     409                                return deferred.promise();
     410
     411                        },
     412
     413                        /**
     414                         * Set the model post parent.
     415                         */
     416                        setHelperParentPost = function( collection, postId ) {
     417
     418                                // Attach post_parent id to the collection.
     419                                _.each( collection.models, function( model ) {
     420                                        model.set( 'parent_post', postId );
     421                                } );
     422                        },
     423
     424                        /**
     425                         * Add a helper funtion to handle post Meta.
     426                         */
     427                        MetaMixin = {
     428                                getMeta: function() {
     429                                        return buildCollectionGetter( this, 'PostMeta', 'https://api.w.org/meta' );
     430                                }
     431                        },
     432
     433                        /**
     434                         * Add a helper funtion to handle post Revisions.
     435                         */
     436                        RevisionsMixin = {
     437                                getRevisions: function() {
     438                                        return buildCollectionGetter( this, 'PostRevisions' );
     439                                }
     440                        },
     441
     442                        /**
     443                         * Add a helper funtion to handle post Tags.
     444                         */
     445                        TagsMixin = {
     446
     447                                /**
     448                                 * Get the tags for a post.
     449                                 *
     450                                 * @return {Deferred.promise} promise Resolves to an array of tags.
     451                                 */
     452                                getTags: function() {
     453                                        var tagIds = this.get( 'tags' ),
     454                                                tags  = new wp.api.collections.Tags();
     455
     456                                        // Resolve with an empty array if no tags.
     457                                        if ( _.isEmpty( tagIds ) ) {
     458                                                return jQuery.Deferred().resolve( [] );
     459                                        }
     460
     461                                        return tags.fetch( { data: { include: tagIds } } );
     462                                },
     463
     464                                /**
     465                                 * Set the tags for a post.
     466                                 *
     467                                 * Accepts an array of tag slugs, or a Tags collection.
     468                                 *
     469                                 * @param {array|Backbone.Collection} tags The tags to set on the post.
     470                                 *
     471                                 */
     472                                setTags: function( tags ) {
     473                                        var allTags, newTag,
     474                                                self = this,
     475                                                newTags = [];
     476
     477                                        if ( _.isString( tags ) ) {
     478                                                return false;
     479                                        }
     480
     481                                        // If this is an array of slugs, build a collection.
     482                                        if ( _.isArray( tags ) ) {
     483
     484                                                // Get all the tags.
     485                                                allTags = new wp.api.collections.Tags();
     486                                                allTags.fetch( {
     487                                                        data:    { per_page: 100 },
     488                                                        success: function( alltags ) {
     489
     490                                                                // Find the passed tags and set them up.
     491                                                                _.each( tags, function( tag ) {
     492                                                                        newTag = new wp.api.models.Tag( alltags.findWhere( { slug: tag } ) );
     493
     494                                                                        // Tie the new tag to the post.
     495                                                                        newTag.set( 'parent_post', self.get( 'id' ) );
     496
     497                                                                        // Add the new tag to the collection.
     498                                                                        newTags.push( newTag );
     499                                                                } );
     500                                                                tags = new wp.api.collections.Tags( newTags );
     501                                                                self.setTagsWithCollection( tags );
     502                                                        }
     503                                                } );
     504
     505                                        } else {
     506                                                this.setTagsWithCollection( tags );
     507                                        }
     508                                },
     509
     510                                /**
     511                                 * Set the tags for a post.
     512                                 *
     513                                 * Accepts a Tags collection.
     514                                 *
     515                                 * @param {array|Backbone.Collection} tags The tags to set on the post.
     516                                 *
     517                                 */
     518                                setTagsWithCollection: function( tags ) {
     519
     520                                        // Pluck out the category ids.
     521                                        this.set( 'tags', tags.pluck( 'id' ) );
     522                                        return this.save();
     523                                }
     524                        },
     525
     526                        /**
     527                         * Add a helper funtion to handle post Categories.
     528                         */
     529                        CategoriesMixin = {
     530
     531                                /**
     532                                 * Get a the categories for a post.
     533                                 *
     534                                 * @return {Deferred.promise} promise Resolves to an array of categories.
     535                                 */
     536                                getCategories: function() {
     537                                        var categoryIds = this.get( 'categories' ),
     538                                                categories  = new wp.api.collections.Categories();
     539
     540                                        // Resolve with an empty array if no categories.
     541                                        if ( _.isEmpty( categoryIds ) ) {
     542                                                return jQuery.Deferred().resolve( [] );
     543                                        }
     544
     545                                        return categories.fetch( { data: { include: categoryIds } } );
     546                                },
     547
     548                                /**
     549                                 * Set the categories for a post.
     550                                 *
     551                                 * Accepts an array of category slugs, or a Categories collection.
     552                                 *
     553                                 * @param {array|Backbone.Collection} categories The categories to set on the post.
     554                                 *
     555                                 */
     556                                setCategories: function( categories ) {
     557                                        var allCategories, newCategory,
     558                                                self = this,
     559                                                newCategories = [];
     560
     561                                        if ( _.isString( categories ) ) {
     562                                                return false;
     563                                        }
     564
     565                                        // If this is an array of slugs, build a collection.
     566                                        if ( _.isArray( categories ) ) {
     567
     568                                                // Get all the categories.
     569                                                allCategories = new wp.api.collections.Categories();
     570                                                allCategories.fetch( {
     571                                                        data:    { per_page: 100 },
     572                                                        success: function( allcats ) {
     573
     574                                                                // Find the passed categories and set them up.
     575                                                                _.each( categories, function( category ) {
     576                                                                        newCategory = new wp.api.models.Category( allcats.findWhere( { slug: category } ) );
     577
     578                                                                        // Tie the new category to the post.
     579                                                                        newCategory.set( 'parent_post', self.get( 'id' ) );
     580
     581                                                                        // Add the new category to the collection.
     582                                                                        newCategories.push( newCategory );
     583                                                                } );
     584                                                                categories = new wp.api.collections.Categories( newCategories );
     585                                                                self.setCategoriesWithCollection( categories );
     586                                                        }
     587                                                } );
     588
     589                                        } else {
     590                                                this.setCategoriesWithCollection( categories );
     591                                        }
     592
     593                                },
     594
     595                                /**
     596                                 * Set the categories for a post.
     597                                 *
     598                                 * Accepts Categories collection.
     599                                 *
     600                                 * @param {array|Backbone.Collection} categories The categories to set on the post.
     601                                 *
     602                                 */
     603                                setCategoriesWithCollection: function( categories ) {
     604
     605                                        // Pluck out the category ids.
     606                                        this.set( 'categories', categories.pluck( 'id' ) );
     607                                        return this.save();
     608                                }
     609                        },
     610
     611                        /**
     612                         * Add a helper function to retrieve the author user model.
     613                         */
     614                        AuthorMixin = {
     615                                getAuthorUser: function() {
     616                                        return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' );
     617                                }
     618                        },
     619
     620                        /**
     621                         * Add a helper function to retrieve the featured media.
     622                         */
     623                        FeaturedMediaMixin = {
     624                                getFeaturedMedia: function() {
     625                                        return buildModelGetter( this, this.get( 'featured_media' ), 'Media', 'wp:featuredmedia', 'source_url' );
     626                                }
     627                        };
     628
     629                // Exit if we don't have valid model defaults.
     630                if ( _.isUndefined( model.prototype.args ) ) {
     631                        return model;
     632                }
     633
     634                // Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin.
     635                _.each( parseableDates, function( theDateKey ) {
     636                        if ( ! _.isUndefined( model.prototype.args[ theDateKey ] ) ) {
     637                                hasDate = true;
     638                        }
     639                } );
     640
     641                // Add the TimeStampedMixin for models that contain a date field.
     642                if ( hasDate ) {
     643                        model = model.extend( TimeStampedMixin );
     644                }
     645
     646                // Add the AuthorMixin for models that contain an author.
     647                if ( ! _.isUndefined( model.prototype.args.author ) ) {
     648                        model = model.extend( AuthorMixin );
     649                }
     650
     651                // Add the FeaturedMediaMixin for models that contain a featured_media.
     652                if ( ! _.isUndefined( model.prototype.args.featured_media ) ) {
     653                        model = model.extend( FeaturedMediaMixin );
     654                }
     655
     656                // Add the CategoriesMixin for models that support categories collections.
     657                if ( ! _.isUndefined( model.prototype.args.categories ) ) {
     658                        model = model.extend( CategoriesMixin );
     659                }
     660
     661                // Add the MetaMixin for models that support meta collections.
     662                if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Meta' ] ) ) {
     663                        model = model.extend( MetaMixin );
     664                }
     665
     666                // Add the TagsMixin for models that support tags collections.
     667                if ( ! _.isUndefined( model.prototype.args.tags ) ) {
     668                        model = model.extend( TagsMixin );
     669                }
     670
     671                // Add the RevisionsMixin for models that support revisions collections.
     672                if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) {
     673                        model = model.extend( RevisionsMixin );
     674                }
     675
     676                return model;
     677        };
     678
     679})( window );
     680
     681/* global wpApiSettings:false */
     682
     683// Suppress warning about parse function's unused "options" argument:
     684/* jshint unused:false */
     685(function() {
     686
     687        'use strict';
     688
     689        var wpApiSettings = window.wpApiSettings || {};
     690
     691        /**
     692         * Backbone base model for all models.
     693         */
     694        wp.api.WPApiBaseModel = Backbone.Model.extend(
     695                /** @lends WPApiBaseModel.prototype  */
     696                {
     697                        /**
     698                         * Set nonce header before every Backbone sync.
     699                         *
     700                         * @param {string} method.
     701                         * @param {Backbone.Model} model.
     702                         * @param {{beforeSend}, *} options.
     703                         * @returns {*}.
     704                         */
     705                        sync: function( method, model, options ) {
     706                                var beforeSend;
     707
     708                                options = options || {};
     709
     710                                // Remove date_gmt if null.
     711                                if ( _.isNull( model.get( 'date_gmt' ) ) ) {
     712                                        model.unset( 'date_gmt' );
     713                                }
     714
     715                                // Remove slug if empty.
     716                                if ( _.isEmpty( model.get( 'slug' ) ) ) {
     717                                        model.unset( 'slug' );
     718                                }
     719
     720                                if ( ! _.isUndefined( wpApiSettings.nonce ) && ! _.isNull( wpApiSettings.nonce ) ) {
     721                                        beforeSend = options.beforeSend;
     722
     723                                        // @todo enable option for jsonp endpoints
     724                                        // options.dataType = 'jsonp';
     725
     726                                        options.beforeSend = function( xhr ) {
     727                                                xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
     728
     729                                                if ( beforeSend ) {
     730                                                        return beforeSend.apply( this, arguments );
     731                                                }
     732                                        };
     733                                }
     734
     735                                // Add '?force=true' to use delete method when required.
     736                                if ( this.requireForceForDelete && 'delete' === method ) {
     737                                        model.url = model.url() + '?force=true';
     738                                }
     739                                return Backbone.sync( method, model, options );
     740                        },
     741
     742                        /**
     743                         * Save is only allowed when the PUT OR POST methods are available for the endpoint.
     744                         */
     745                        save: function( attrs, options ) {
     746
     747                                // Do we have the put method, then execute the save.
     748                                if ( _.includes( this.methods, 'PUT' ) || _.includes( this.methods, 'POST' ) ) {
     749
     750                                        // Proxy the call to the original save function.
     751                                        return Backbone.Model.prototype.save.call( this, attrs, options );
     752                                } else {
     753
     754                                        // Otherwise bail, disallowing action.
     755                                        return false;
     756                                }
     757                        },
     758
     759                        /**
     760                         * Delete is only allowed when the DELETE method is available for the endpoint.
     761                         */
     762                        destroy: function( options ) {
     763
     764                                // Do we have the DELETE method, then execute the destroy.
     765                                if ( _.includes( this.methods, 'DELETE' ) ) {
     766
     767                                        // Proxy the call to the original save function.
     768                                        return Backbone.Model.prototype.destroy.call( this, options );
     769                                } else {
     770
     771                                        // Otherwise bail, disallowing action.
     772                                        return false;
     773                                }
     774                        }
     775
     776                }
     777        );
     778
     779        /**
     780         * API Schema model. Contains meta information about the API.
     781         */
     782        wp.api.models.Schema = wp.api.WPApiBaseModel.extend(
     783                /** @lends Schema.prototype  */
     784                {
     785                        defaults: {
     786                                _links: {},
     787                                namespace: null,
     788                                routes: {}
     789                        },
     790
     791                        initialize: function( attributes, options ) {
     792                                var model = this;
     793                                options = options || {};
     794
     795                                wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options );
     796
     797                                model.apiRoot = options.apiRoot || wpApiSettings.root;
     798                                model.versionString = options.versionString || wpApiSettings.versionString;
     799                        },
     800
     801                        url: function() {
     802                                return this.apiRoot + this.versionString;
     803                        }
     804                }
     805        );
     806})();
     807
     808( function() {
     809
     810        'use strict';
     811
     812        var wpApiSettings = window.wpApiSettings || {};
     813
     814        /**
     815         * Contains basic collection functionality such as pagination.
     816         */
     817        wp.api.WPApiBaseCollection = Backbone.Collection.extend(
     818                /** @lends BaseCollection.prototype  */
     819                {
     820
     821                        /**
     822                         * Setup default state.
     823                         */
     824                        initialize: function( models, options ) {
     825                                this.state = {
     826                                        data: {},
     827                                        currentPage: null,
     828                                        totalPages: null,
     829                                        totalObjects: null
     830                                };
     831                                if ( _.isUndefined( options ) ) {
     832                                        this.parent = '';
     833                                } else {
     834                                        this.parent = options.parent;
     835                                }
     836                        },
     837
     838                        /**
     839                         * Extend Backbone.Collection.sync to add nince and pagination support.
     840                         *
     841                         * Set nonce header before every Backbone sync.
     842                         *
     843                         * @param {string} method.
     844                         * @param {Backbone.Model} model.
     845                         * @param {{success}, *} options.
     846                         * @returns {*}.
     847                         */
     848                        sync: function( method, model, options ) {
     849                                var beforeSend, success,
     850                                        self = this;
     851
     852                                options    = options || {};
     853                                beforeSend = options.beforeSend;
     854
     855                                // If we have a localized nonce, pass that along with each sync.
     856                                if ( 'undefined' !== typeof wpApiSettings.nonce ) {
     857                                        options.beforeSend = function( xhr ) {
     858                                                xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
     859
     860                                                if ( beforeSend ) {
     861                                                        return beforeSend.apply( self, arguments );
     862                                                }
     863                                        };
     864                                }
     865
     866                                // When reading, add pagination data.
     867                                if ( 'read' === method ) {
     868                                        if ( options.data ) {
     869                                                self.state.data = _.clone( options.data );
     870
     871                                                delete self.state.data.page;
     872                                        } else {
     873                                                self.state.data = options.data = {};
     874                                        }
     875
     876                                        if ( 'undefined' === typeof options.data.page ) {
     877                                                self.state.currentPage  = null;
     878                                                self.state.totalPages   = null;
     879                                                self.state.totalObjects = null;
     880                                        } else {
     881                                                self.state.currentPage = options.data.page - 1;
     882                                        }
     883
     884                                        success = options.success;
     885                                        options.success = function( data, textStatus, request ) {
     886                                                if ( ! _.isUndefined( request ) ) {
     887                                                        self.state.totalPages   = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 );
     888                                                        self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 );
     889                                                }
     890
     891                                                if ( null === self.state.currentPage ) {
     892                                                        self.state.currentPage = 1;
     893                                                } else {
     894                                                        self.state.currentPage++;
     895                                                }
     896
     897                                                if ( success ) {
     898                                                        return success.apply( this, arguments );
     899                                                }
     900                                        };
     901                                }
     902
     903                                // Continue by calling Bacckbone's sync.
     904                                return Backbone.sync( method, model, options );
     905                        },
     906
     907                        /**
     908                         * Fetches the next page of objects if a new page exists.
     909                         *
     910                         * @param {data: {page}} options.
     911                         * @returns {*}.
     912                         */
     913                        more: function( options ) {
     914                                options = options || {};
     915                                options.data = options.data || {};
     916
     917                                _.extend( options.data, this.state.data );
     918
     919                                if ( 'undefined' === typeof options.data.page ) {
     920                                        if ( ! this.hasMore() ) {
     921                                                return false;
     922                                        }
     923
     924                                        if ( null === this.state.currentPage || this.state.currentPage <= 1 ) {
     925                                                options.data.page = 2;
     926                                        } else {
     927                                                options.data.page = this.state.currentPage + 1;
     928                                        }
     929                                }
     930
     931                                return this.fetch( options );
     932                        },
     933
     934                        /**
     935                         * Returns true if there are more pages of objects available.
     936                         *
     937                         * @returns null|boolean.
     938                         */
     939                        hasMore: function() {
     940                                if ( null === this.state.totalPages ||
     941                                         null === this.state.totalObjects ||
     942                                         null === this.state.currentPage ) {
     943                                        return null;
     944                                } else {
     945                                        return ( this.state.currentPage < this.state.totalPages );
     946                                }
     947                        }
     948                }
     949        );
     950
     951} )();
     952
     953( function() {
     954
     955        'use strict';
     956
     957        var Endpoint, initializedDeferreds = {},
     958                wpApiSettings = window.wpApiSettings || {};
     959        window.wp = window.wp || {};
     960        wp.api    = wp.api || {};
     961
     962        // If wpApiSettings is unavailable, try the default.
     963        if ( _.isEmpty( wpApiSettings ) ) {
     964                wpApiSettings.root = window.location.origin + '/wp-json/';
     965        }
     966
     967        Endpoint = Backbone.Model.extend( {
     968                defaults: {
     969                        apiRoot: wpApiSettings.root,
     970                        versionString: wp.api.versionString,
     971                        schema: null,
     972                        models: {},
     973                        collections: {}
     974                },
     975
     976                /**
     977                 * Initialize the Endpoint model.
     978                 */
     979                initialize: function() {
     980                        var model = this, deferred;
     981
     982                        Backbone.Model.prototype.initialize.apply( model, arguments );
     983
     984                        deferred = jQuery.Deferred();
     985                        model.schemaConstructed = deferred.promise();
     986
     987                        model.schemaModel = new wp.api.models.Schema( null, {
     988                                apiRoot: model.get( 'apiRoot' ),
     989                                versionString: model.get( 'versionString' )
     990                        } );
     991
     992                        // When the model loads, resolve the promise.
     993                        model.schemaModel.once( 'change', function() {
     994                                model.constructFromSchema();
     995                                deferred.resolve( model );
     996                        } );
     997
     998                        if ( model.get( 'schema' ) ) {
     999
     1000                                // Use schema supplied as model attribute.
     1001                                model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) );
     1002                        } else if (
     1003                                ! _.isUndefined( sessionStorage ) &&
     1004                                ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) &&
     1005                                sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) )
     1006                        ) {
     1007
     1008                                // Used a cached copy of the schema model if available.
     1009                                model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) );
     1010                        } else {
     1011                                model.schemaModel.fetch( {
     1012                                        /**
     1013                                         * When the server returns the schema model data, store the data in a sessionCache so we don't
     1014                                         * have to retrieve it again for this session. Then, construct the models and collections based
     1015                                         * on the schema model data.
     1016                                         */
     1017                                        success: function( newSchemaModel ) {
     1018
     1019                                                // Store a copy of the schema model in the session cache if available.
     1020                                                if ( ! _.isUndefined( sessionStorage ) && wpApiSettings.cacheSchema ) {
     1021                                                        try {
     1022                                                                sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) );
     1023                                                        } catch ( error ) {
     1024
     1025                                                                // Fail silently, fixes errors in safari private mode.
     1026                                                        }
     1027                                                }
     1028                                        },
     1029
     1030                                        // Log the error condition.
     1031                                        error: function( err ) {
     1032                                                window.console.log( err );
     1033                                        }
     1034                                } );
     1035                        }
     1036                },
     1037
     1038                constructFromSchema: function() {
     1039                        var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects,
     1040
     1041                        /**
     1042                         * Set up the model and collection name mapping options. As the schema is built, the
     1043                         * model and collection names will be adjusted if they are found in the mapping object.
     1044                         *
     1045                         * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options.
     1046                         *
     1047                         */
     1048                        mapping = wpApiSettings.mapping || {
     1049                                models: {
     1050                                        'Categories':      'Category',
     1051                                        'Comments':        'Comment',
     1052                                        'Pages':           'Page',
     1053                                        'PagesMeta':       'PageMeta',
     1054                                        'PagesRevisions':  'PageRevision',
     1055                                        'Posts':           'Post',
     1056                                        'PostsCategories': 'PostCategory',
     1057                                        'PostsRevisions':  'PostRevision',
     1058                                        'PostsTags':       'PostTag',
     1059                                        'Schema':          'Schema',
     1060                                        'Statuses':        'Status',
     1061                                        'Tags':            'Tag',
     1062                                        'Taxonomies':      'Taxonomy',
     1063                                        'Types':           'Type',
     1064                                        'Users':           'User'
     1065                                },
     1066                                collections: {
     1067                                        'PagesMeta':       'PageMeta',
     1068                                        'PagesRevisions':  'PageRevisions',
     1069                                        'PostsCategories': 'PostCategories',
     1070                                        'PostsMeta':       'PostMeta',
     1071                                        'PostsRevisions':  'PostRevisions',
     1072                                        'PostsTags':       'PostTags'
     1073                                }
     1074                        };
     1075
     1076                        /**
     1077                         * Iterate thru the routes, picking up models and collections to build. Builds two arrays,
     1078                         * one for models and one for collections.
     1079                         */
     1080                        modelRoutes      = [];
     1081                        collectionRoutes = [];
     1082                        schemaRoot       = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' );
     1083                        loadingObjects   = {};
     1084
     1085                        /**
     1086                         * Tracking objects for models and collections.
     1087                         */
     1088                        loadingObjects.models      = routeModel.get( 'models' );
     1089                        loadingObjects.collections = routeModel.get( 'collections' );
     1090
     1091                        _.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) {
     1092
     1093                                // Skip the schema root if included in the schema.
     1094                                if ( index !== routeModel.get( ' versionString' ) &&
     1095                                                index !== schemaRoot &&
     1096                                                index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) )
     1097                                ) {
     1098
     1099                                        // Single items end with a regex (or the special case 'me').
     1100                                        if ( /(?:.*[+)]|\/me)$/.test( index ) ) {
     1101                                                modelRoutes.push( { index: index, route: route } );
     1102                                        } else {
     1103
     1104                                                // Collections end in a name.
     1105                                                collectionRoutes.push( { index: index, route: route } );
     1106                                        }
     1107                                }
     1108                        } );
     1109
     1110                        /**
     1111                         * Construct the models.
     1112                         *
     1113                         * Base the class name on the route endpoint.
     1114                         */
     1115                        _.each( modelRoutes, function( modelRoute ) {
     1116
     1117                                // Extract the name and any parent from the route.
     1118                                var modelClassName,
     1119                                                routeName  = wp.api.utils.extractRoutePart( modelRoute.index, 2 ),
     1120                                                parentName = wp.api.utils.extractRoutePart( modelRoute.index, 4 ),
     1121                                                routeEnd   = wp.api.utils.extractRoutePart( modelRoute.index, 1 );
     1122
     1123                                // Handle the special case of the 'me' route.
     1124                                if ( 'me' === routeEnd ) {
     1125                                        routeName = 'me';
     1126                                }
     1127
     1128                                // If the model has a parent in its route, add that to its class name.
     1129                                if ( '' !== parentName && parentName !== routeName ) {
     1130                                        modelClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName );
     1131                                        modelClassName = mapping.models[ modelClassName ] || modelClassName;
     1132                                        loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
     1133
     1134                                                // Return a constructed url based on the parent and id.
     1135                                                url: function() {
     1136                                                        var url = routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
     1137                                                                        parentName +  '/' +
     1138                                                                        ( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ?
     1139                                                                                this.get( 'parent_post' ) :
     1140                                                                                this.get( 'parent' ) ) + '/' +
     1141                                                                        routeName;
     1142                                                        if ( ! _.isUndefined( this.get( 'id' ) ) ) {
     1143                                                                url +=  '/' + this.get( 'id' );
     1144                                                        }
     1145                                                        return url;
     1146                                                },
     1147
     1148                                                // Include a reference to the original route object.
     1149                                                route: modelRoute,
     1150
     1151                                                // Include a reference to the original class name.
     1152                                                name: modelClassName,
     1153
     1154                                                // Include the array of route methods for easy reference.
     1155                                                methods: modelRoute.route.methods,
     1156
     1157                                                initialize: function() {
     1158
     1159                                                        /**
     1160                                                         * Posts and pages support trashing, other types don't support a trash
     1161                                                         * and require that you pass ?force=true to actually delete them.
     1162                                                         *
     1163                                                         * @todo we should be getting trashability from the Schema, not hard coding types here.
     1164                                                         */
     1165                                                        if (
     1166                                                                'Posts' !== this.name &&
     1167                                                                'Pages' !== this.name &&
     1168                                                                _.includes( this.methods, 'DELETE' )
     1169                                                        ) {
     1170                                                                this.requireForceForDelete = true;
     1171                                                        }
     1172                                                }
     1173                                        } );
     1174                                } else {
     1175
     1176                                        // This is a model without a parent in its route
     1177                                        modelClassName = wp.api.utils.capitalize( routeName );
     1178                                        modelClassName = mapping.models[ modelClassName ] || modelClassName;
     1179                                        loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
     1180
     1181                                                // Function that returns a constructed url based on the id.
     1182                                                url: function() {
     1183                                                        var url = routeModel.get( 'apiRoot' ) +
     1184                                                                routeModel.get( 'versionString' ) +
     1185                                                                ( ( 'me' === routeName ) ? 'users/me' : routeName );
     1186
     1187                                                        if ( ! _.isUndefined( this.get( 'id' ) ) ) {
     1188                                                                url +=  '/' + this.get( 'id' );
     1189                                                        }
     1190                                                        return url;
     1191                                                },
     1192
     1193                                                // Include a reference to the original route object.
     1194                                                route: modelRoute,
     1195
     1196                                                // Include a reference to the original class name.
     1197                                                name: modelClassName,
     1198
     1199                                                // Include the array of route methods for easy reference.
     1200                                                methods: modelRoute.route.methods
     1201                                        } );
     1202                                }
     1203
     1204                                // Add defaults to the new model, pulled form the endpoint.
     1205                                wp.api.utils.decorateFromRoute( modelRoute.route.endpoints, loadingObjects.models[ modelClassName ] );
     1206
     1207                        } );
     1208
     1209                        /**
     1210                         * Construct the collections.
     1211                         *
     1212                         * Base the class name on the route endpoint.
     1213                         */
     1214                        _.each( collectionRoutes, function( collectionRoute ) {
     1215
     1216                                // Extract the name and any parent from the route.
     1217                                var collectionClassName, modelClassName,
     1218                                                routeName  = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ),
     1219                                                parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 3 );
     1220
     1221                                // If the collection has a parent in its route, add that to its class name.
     1222                                if ( '' !== parentName && parentName !== routeName ) {
     1223
     1224                                        collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName );
     1225                                        modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
     1226                                        collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
     1227                                        loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
     1228
     1229                                                // Function that returns a constructed url passed on the parent.
     1230                                                url: function() {
     1231                                                        return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
     1232                                                                        parentName + '/' + this.parent + '/' +
     1233                                                                        routeName;
     1234                                                },
     1235
     1236                                                // Specify the model that this collection contains.
     1237                                                model: loadingObjects.models[ modelClassName ],
     1238
     1239                                                // Include a reference to the original class name.
     1240                                                name: collectionClassName,
     1241
     1242                                                // Include a reference to the original route object.
     1243                                                route: collectionRoute,
     1244
     1245                                                // Include the array of route methods for easy reference.
     1246                                                methods: collectionRoute.route.methods
     1247                                        } );
     1248                                } else {
     1249
     1250                                        // This is a collection without a parent in its route.
     1251                                        collectionClassName = wp.api.utils.capitalize( routeName );
     1252                                        modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
     1253                                        collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
     1254                                        loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
     1255
     1256                                                // For the url of a root level collection, use a string.
     1257                                                url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName,
     1258
     1259                                                // Specify the model that this collection contains.
     1260                                                model: loadingObjects.models[ modelClassName ],
     1261
     1262                                                // Include a reference to the original class name.
     1263                                                name: collectionClassName,
     1264
     1265                                                // Include a reference to the original route object.
     1266                                                route: collectionRoute,
     1267
     1268                                                // Include the array of route methods for easy reference.
     1269                                                methods: collectionRoute.route.methods
     1270                                        } );
     1271                                }
     1272
     1273                                // Add defaults to the new model, pulled form the endpoint.
     1274                                wp.api.utils.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] );
     1275                        } );
     1276
     1277                        // Add mixins and helpers for each of the models.
     1278                        _.each( loadingObjects.models, function( model, index ) {
     1279                                loadingObjects.models[ index ] = wp.api.utils.addMixinsAndHelpers( model, index, loadingObjects );
     1280                        } );
     1281
     1282                }
     1283
     1284        } );
     1285
     1286        wp.api.endpoints = new Backbone.Collection( {
     1287                model: Endpoint
     1288        } );
     1289
     1290        /**
     1291         * Initialize the wp-api, optionally passing the API root.
     1292         *
     1293         * @param {object} [args]
     1294         * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root.
     1295         * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root.
     1296         * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided.
     1297         */
     1298        wp.api.init = function( args ) {
     1299                var endpoint, attributes = {}, deferred, promise;
     1300
     1301                args                     = args || {};
     1302                attributes.apiRoot       = args.apiRoot || wpApiSettings.root;
     1303                attributes.versionString = args.versionString || wpApiSettings.versionString;
     1304                attributes.schema        = args.schema || null;
     1305                if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) {
     1306                        attributes.schema = wpApiSettings.schema;
     1307                }
     1308
     1309                if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) {
     1310                        endpoint = wp.api.endpoints.findWhere( { apiRoot: attributes.apiRoot, versionString: attributes.versionString } );
     1311                        if ( ! endpoint ) {
     1312                                endpoint = new Endpoint( attributes );
     1313                                wp.api.endpoints.add( endpoint );
     1314                        }
     1315                        deferred = jQuery.Deferred();
     1316                        promise = deferred.promise();
     1317
     1318                        endpoint.schemaConstructed.done( function( endpoint ) {
     1319
     1320                                // Map the default endpoints, extending any already present items (including Schema model).
     1321                                wp.api.models      = _.extend( endpoint.get( 'models' ), wp.api.models );
     1322                                wp.api.collections = _.extend( endpoint.get( 'collections' ), wp.api.collections );
     1323                                deferred.resolveWith( wp.api, [ endpoint ] );
     1324                        } );
     1325                        initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise;
     1326                }
     1327                return initializedDeferreds[ attributes.apiRoot + attributes.versionString ];
     1328        };
     1329
     1330        /**
     1331         * Construct the default endpoints and add to an endpoints collection.
     1332         */
     1333
     1334        // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready.
     1335        wp.api.loadPromise = wp.api.init();
     1336
     1337} )();
  • src/wp-includes/option.php

     
    17081708}
    17091709
    17101710/**
     1711 * Register default settings available in WordPress.
     1712 *
     1713 * The settings registered here are primarily useful for the REST API, so this
     1714 * does not encompass all settings available in WordPress.
     1715 *
     1716 * @since 4.7.0
     1717 */
     1718function register_initial_settings() {
     1719        register_setting( 'general', 'blogname', array(
     1720                'show_in_rest' => array(
     1721                        'name' => 'title',
     1722                ),
     1723                'type'         => 'string',
     1724                'description'  => __( 'Site title.' ),
     1725        ) );
     1726
     1727        register_setting( 'general', 'blogdescription', array(
     1728                'show_in_rest' => array(
     1729                        'name' => 'description',
     1730                ),
     1731                'type'         => 'string',
     1732                'description'  => __( 'Site description.' ),
     1733        ) );
     1734
     1735        register_setting( 'general', 'siteurl', array(
     1736                'show_in_rest' => array(
     1737                        'name'    => 'url',
     1738                        'schema'  => array(
     1739                                'format' => 'uri',
     1740                        ),
     1741                ),
     1742                'type'         => 'string',
     1743                'description'  => __( 'Site URL.' ),
     1744        ) );
     1745
     1746        register_setting( 'general', 'admin_email', array(
     1747                'show_in_rest' => array(
     1748                        'name'    => 'email',
     1749                        'schema'  => array(
     1750                                'format' => 'email',
     1751                        ),
     1752                ),
     1753                'type'         => 'string',
     1754                '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.' ),
     1755        ) );
     1756
     1757        register_setting( 'general', 'timezone_string', array(
     1758                'show_in_rest' => array(
     1759                        'name' => 'timezone',
     1760                ),
     1761                'type'         => 'string',
     1762                'description'  => __( 'A city in the same timezone as you.' ),
     1763        ) );
     1764
     1765        register_setting( 'general', 'date_format', array(
     1766                'show_in_rest' => true,
     1767                'type'         => 'string',
     1768                'description'  => __( 'A date format for all date strings.' ),
     1769        ) );
     1770
     1771        register_setting( 'general', 'time_format', array(
     1772                'show_in_rest' => true,
     1773                'type'         => 'string',
     1774                'description'  => __( 'A time format for all time strings.' ),
     1775        ) );
     1776
     1777        register_setting( 'general', 'start_of_week', array(
     1778                'show_in_rest' => true,
     1779                'type'         => 'number',
     1780                'description'  => __( 'A day number of the week that the week should start on.' ),
     1781        ) );
     1782
     1783        register_setting( 'general', 'WPLANG', array(
     1784                'show_in_rest' => array(
     1785                        'name' => 'language',
     1786                ),
     1787                'type'         => 'string',
     1788                'description'  => __( 'WordPress locale code.' ),
     1789                'default'      => 'en_US',
     1790        ) );
     1791
     1792        register_setting( 'writing', 'use_smilies', array(
     1793                'show_in_rest' => true,
     1794                'type'         => 'boolean',
     1795                'description'  => __( 'Convert emoticons like :-) and :-P to graphics on display.' ),
     1796                'default'      => true,
     1797        ) );
     1798
     1799        register_setting( 'writing', 'default_category', array(
     1800                'show_in_rest' => true,
     1801                'type'         => 'number',
     1802                'description'  => __( 'Default category.' ),
     1803        ) );
     1804
     1805        register_setting( 'writing', 'default_post_format', array(
     1806                'show_in_rest' => true,
     1807                'type'         => 'string',
     1808                'description'  => __( 'Default post format.' ),
     1809        ) );
     1810
     1811        register_setting( 'reading', 'posts_per_page', array(
     1812                'show_in_rest' => true,
     1813                'type'         => 'number',
     1814                'description'  => __( 'Blog pages show at most.' ),
     1815                'default'      => 10,
     1816        ) );
     1817}
     1818
     1819/**
    17111820 * Register a setting and its data.
    17121821 *
    17131822 * @since 2.7.0
  • src/wp-includes/post.php

     
    3333                'query_var' => false,
    3434                'delete_with_user' => true,
    3535                'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ),
     36                'show_in_rest' => true,
     37                'rest_base' => 'posts',
     38                'rest_controller_class' => 'WP_REST_Posts_Controller',
    3639        ) );
    3740
    3841        register_post_type( 'page', array(
     
    5154                'query_var' => false,
    5255                'delete_with_user' => true,
    5356                'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'page-attributes', 'custom-fields', 'comments', 'revisions' ),
     57                'show_in_rest' => true,
     58                'rest_base' => 'pages',
     59                'rest_controller_class' => 'WP_REST_Posts_Controller',
    5460        ) );
    5561
    5662        register_post_type( 'attachment', array(
     
    7682                'show_in_nav_menus' => false,
    7783                'delete_with_user' => true,
    7884                'supports' => array( 'title', 'author', 'comments' ),
     85                'show_in_rest' => true,
     86                'rest_base' => 'media',
     87                'rest_controller_class' => 'WP_REST_Attachments_Controller',
    7988        ) );
    8089        add_post_type_support( 'attachment:audio', 'thumbnail' );
    8190        add_post_type_support( 'attachment:video', 'thumbnail' );
  • src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

     
     1<?php
     2
     3class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
     4
     5        /**
     6         * Determine the allowed query_vars for a get_items() response and
     7         * prepare for WP_Query.
     8         *
     9         * @param array           $prepared_args Optional. Array of prepared arguments.
     10         * @param WP_REST_Request $request       Optional. Request to prepare items for.
     11         * @return array Array of query arguments.
     12         */
     13        protected function prepare_items_query( $prepared_args = array(), $request = null ) {
     14                $query_args = parent::prepare_items_query( $prepared_args, $request );
     15                if ( empty( $query_args['post_status'] ) || ! in_array( $query_args['post_status'], array( 'inherit', 'private', 'trash' ), true ) ) {
     16                        $query_args['post_status'] = 'inherit';
     17                }
     18                $media_types = $this->get_media_types();
     19                if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
     20                        $query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
     21                }
     22                if ( ! empty( $request['mime_type'] ) ) {
     23                        $parts = explode( '/', $request['mime_type'] );
     24                        if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
     25                                $query_args['post_mime_type'] = $request['mime_type'];
     26                        }
     27                }
     28                return $query_args;
     29        }
     30
     31        /**
     32         * Check if a given request has access to create an attachment.
     33         *
     34         * @param  WP_REST_Request $request Full details about the request.
     35         * @return WP_Error|true Boolean true if the attachment may be created, or a WP_Error if not.
     36         */
     37        public function create_item_permissions_check( $request ) {
     38                $ret = parent::create_item_permissions_check( $request );
     39                if ( ! $ret || is_wp_error( $ret ) ) {
     40                        return $ret;
     41                }
     42
     43                if ( ! current_user_can( 'upload_files' ) ) {
     44                        return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to upload media on this site.' ), array( 'status' => 400 ) );
     45                }
     46
     47                // Attaching media to a post requires ability to edit said post.
     48                if ( ! empty( $request['post'] ) ) {
     49                        $parent = $this->get_post( (int) $request['post'] );
     50                        $post_parent_type = get_post_type_object( $parent->post_type );
     51                        if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
     52                                return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to upload media to this resource.' ), array( 'status' => rest_authorization_required_code() ) );
     53                        }
     54                }
     55
     56                return true;
     57        }
     58
     59        /**
     60         * Create a single attachment.
     61         *
     62         * @param WP_REST_Request $request Full details about the request.
     63         * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
     64         */
     65        public function create_item( $request ) {
     66
     67                if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
     68                        return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
     69                }
     70
     71                // Get the file via $_FILES or raw data
     72                $files = $request->get_file_params();
     73                $headers = $request->get_headers();
     74                if ( ! empty( $files ) ) {
     75                        $file = $this->upload_from_file( $files, $headers );
     76                } else {
     77                        $file = $this->upload_from_data( $request->get_body(), $headers );
     78                }
     79
     80                if ( is_wp_error( $file ) ) {
     81                        return $file;
     82                }
     83
     84                $name       = basename( $file['file'] );
     85                $name_parts = pathinfo( $name );
     86                $name       = trim( substr( $name, 0, -(1 + strlen( $name_parts['extension'] ) ) ) );
     87
     88                $url     = $file['url'];
     89                $type    = $file['type'];
     90                $file    = $file['file'];
     91
     92                // use image exif/iptc data for title and caption defaults if possible
     93                // @codingStandardsIgnoreStart
     94                $image_meta = @wp_read_image_metadata( $file );
     95                // @codingStandardsIgnoreEnd
     96                if ( ! empty( $image_meta ) ) {
     97                        if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
     98                                $request['title'] = $image_meta['title'];
     99                        }
     100
     101                        if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
     102                                $request['caption'] = $image_meta['caption'];
     103                        }
     104                }
     105
     106                $attachment = $this->prepare_item_for_database( $request );
     107                $attachment->file = $file;
     108                $attachment->post_mime_type = $type;
     109                $attachment->guid = $url;
     110
     111                if ( empty( $attachment->post_title ) ) {
     112                        $attachment->post_title = preg_replace( '/\.[^.]+$/', '', basename( $file ) );
     113                }
     114
     115                $id = wp_insert_post( $attachment, true );
     116                if ( is_wp_error( $id ) ) {
     117                        if ( 'db_update_error' === $id->get_error_code() ) {
     118                                $id->add_data( array( 'status' => 500 ) );
     119                        } else {
     120                                $id->add_data( array( 'status' => 400 ) );
     121                        }
     122                        return $id;
     123                }
     124                $attachment = $this->get_post( $id );
     125
     126                // Include admin functions to get access to wp_generate_attachment_metadata().
     127                require_once ABSPATH . 'wp-admin/includes/admin.php';
     128
     129                wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
     130
     131                if ( isset( $request['alt_text'] ) ) {
     132                        update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
     133                }
     134
     135                $fields_update = $this->update_additional_fields_for_object( $attachment, $request );
     136                if ( is_wp_error( $fields_update ) ) {
     137                        return $fields_update;
     138                }
     139
     140                $request->set_param( 'context', 'edit' );
     141                $response = $this->prepare_item_for_response( $attachment, $request );
     142                $response = rest_ensure_response( $response );
     143                $response->set_status( 201 );
     144                $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );
     145
     146                /**
     147                 * Fires after a single attachment is created or updated via the REST API.
     148                 *
     149                 * @param object          $attachment Inserted attachment.
     150                 * @param WP_REST_Request $request    The request sent to the API.
     151                 * @param boolean         $creating   True when creating an attachment, false when updating.
     152                 */
     153                do_action( 'rest_insert_attachment', $attachment, $request, true );
     154
     155                return $response;
     156
     157        }
     158
     159        /**
     160         * Update a single post.
     161         *
     162         * @param WP_REST_Request $request Full details about the request.
     163         * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
     164         */
     165        public function update_item( $request ) {
     166                if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
     167                        return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
     168                }
     169                $response = parent::update_item( $request );
     170                if ( is_wp_error( $response ) ) {
     171                        return $response;
     172                }
     173
     174                $response = rest_ensure_response( $response );
     175                $data = $response->get_data();
     176
     177                if ( isset( $request['alt_text'] ) ) {
     178                        update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
     179                }
     180
     181                $attachment = $this->get_post( $request['id'] );
     182
     183                $fields_update = $this->update_additional_fields_for_object( $attachment, $request );
     184                if ( is_wp_error( $fields_update ) ) {
     185                        return $fields_update;
     186                }
     187
     188                $request->set_param( 'context', 'edit' );
     189                $response = $this->prepare_item_for_response( $attachment, $request );
     190                $response = rest_ensure_response( $response );
     191
     192                /* This action is documented in lib/endpoints/class-wp-rest-attachments-controller.php */
     193                do_action( 'rest_insert_attachment', $data, $request, false );
     194
     195                return $response;
     196        }
     197
     198        /**
     199         * Prepare a single attachment for create or update.
     200         *
     201         * @param WP_REST_Request $request Request object.
     202         * @return WP_Error|stdClass $prepared_attachment Post object.
     203         */
     204        protected function prepare_item_for_database( $request ) {
     205                $prepared_attachment = parent::prepare_item_for_database( $request );
     206
     207                if ( isset( $request['caption'] ) ) {
     208                        $prepared_attachment->post_excerpt = $request['caption'];
     209                }
     210
     211                if ( isset( $request['description'] ) ) {
     212                        $prepared_attachment->post_content = $request['description'];
     213                }
     214
     215                if ( isset( $request['post'] ) ) {
     216                        $prepared_attachment->post_parent = (int) $request['post'];
     217                }
     218
     219                return $prepared_attachment;
     220        }
     221
     222        /**
     223         * Prepare a single attachment output for response.
     224         *
     225         * @param WP_Post         $post    Post object.
     226         * @param WP_REST_Request $request Request object.
     227         * @return WP_REST_Response Response object.
     228         */
     229        public function prepare_item_for_response( $post, $request ) {
     230                $response = parent::prepare_item_for_response( $post, $request );
     231                $data = $response->get_data();
     232
     233                $data['alt_text']      = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
     234                $data['caption']       = $post->post_excerpt;
     235                $data['description']   = $post->post_content;
     236                $data['media_type']    = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
     237                $data['mime_type']     = $post->post_mime_type;
     238                $data['media_details'] = wp_get_attachment_metadata( $post->ID );
     239                $data['post']          = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
     240                $data['source_url']    = wp_get_attachment_url( $post->ID );
     241
     242                // Ensure empty details is an empty object.
     243                if ( empty( $data['media_details'] ) ) {
     244                        $data['media_details'] = new stdClass;
     245                } elseif ( ! empty( $data['media_details']['sizes'] ) ) {
     246
     247                        foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
     248
     249                                if ( isset( $size_data['mime-type'] ) ) {
     250                                        $size_data['mime_type'] = $size_data['mime-type'];
     251                                        unset( $size_data['mime-type'] );
     252                                }
     253
     254                                // Use the same method image_downsize() does.
     255                                $image_src = wp_get_attachment_image_src( $post->ID, $size );
     256                                if ( ! $image_src ) {
     257                                        continue;
     258                                }
     259
     260                                $size_data['source_url'] = $image_src[0];
     261                        }
     262
     263                        $full_src = wp_get_attachment_image_src( $post->ID, 'full' );
     264                        if ( ! empty( $full_src ) ) {
     265                                $data['media_details']['sizes']['full'] = array(
     266                                        'file'          => wp_basename( $full_src[0] ),
     267                                        'width'         => $full_src[1],
     268                                        'height'        => $full_src[2],
     269                                        'mime_type'     => $post->post_mime_type,
     270                                        'source_url'    => $full_src[0],
     271                                        );
     272                        }
     273                } else {
     274                        $data['media_details']['sizes'] = new stdClass;
     275                }
     276
     277                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     278
     279                $data = $this->filter_response_by_context( $data, $context );
     280
     281                // Wrap the data in a response object.
     282                $response = rest_ensure_response( $data );
     283
     284                $response->add_links( $this->prepare_links( $post ) );
     285
     286                /**
     287                 * Filter an attachment returned from the API.
     288                 *
     289                 * Allows modification of the attachment right before it is returned.
     290                 *
     291                 * @param WP_REST_Response  $response   The response object.
     292                 * @param WP_Post           $post       The original attachment post.
     293                 * @param WP_REST_Request   $request    Request used to generate the response.
     294                 */
     295                return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
     296        }
     297
     298        /**
     299         * Get the Attachment's schema, conforming to JSON Schema.
     300         *
     301         * @return array Item schema as an array.
     302         */
     303        public function get_item_schema() {
     304
     305                $schema = parent::get_item_schema();
     306
     307                $schema['properties']['alt_text'] = array(
     308                        'description'     => __( 'Alternative text to display when resource is not displayed.' ),
     309                        'type'            => 'string',
     310                        'context'         => array( 'view', 'edit', 'embed' ),
     311                        'arg_options'     => array(
     312                                'sanitize_callback' => 'sanitize_text_field',
     313                        ),
     314                );
     315                $schema['properties']['caption'] = array(
     316                        'description'     => __( 'The caption for the resource.' ),
     317                        'type'            => 'string',
     318                        'context'         => array( 'view', 'edit' ),
     319                        'arg_options'     => array(
     320                                'sanitize_callback' => 'wp_filter_post_kses',
     321                        ),
     322                );
     323                $schema['properties']['description'] = array(
     324                        'description'     => __( 'The description for the resource.' ),
     325                        'type'            => 'string',
     326                        'context'         => array( 'view', 'edit' ),
     327                        'arg_options'     => array(
     328                                'sanitize_callback' => 'wp_filter_post_kses',
     329                        ),
     330                );
     331                $schema['properties']['media_type'] = array(
     332                        'description'     => __( 'Type of resource.' ),
     333                        'type'            => 'string',
     334                        'enum'            => array( 'image', 'file' ),
     335                        'context'         => array( 'view', 'edit', 'embed' ),
     336                        'readonly'        => true,
     337                );
     338                $schema['properties']['mime_type'] = array(
     339                        'description'     => __( 'MIME type of resource.' ),
     340                        'type'            => 'string',
     341                        'context'         => array( 'view', 'edit', 'embed' ),
     342                        'readonly'        => true,
     343                );
     344                $schema['properties']['media_details'] = array(
     345                        'description'     => __( 'Details about the resource file, specific to its type.' ),
     346                        'type'            => 'object',
     347                        'context'         => array( 'view', 'edit', 'embed' ),
     348                        'readonly'        => true,
     349                );
     350                $schema['properties']['post'] = array(
     351                        'description'     => __( 'The id for the associated post of the resource.' ),
     352                        'type'            => 'integer',
     353                        'context'         => array( 'view', 'edit' ),
     354                );
     355                $schema['properties']['source_url'] = array(
     356                        'description'     => __( 'URL to the original resource file.' ),
     357                        'type'            => 'string',
     358                        'format'          => 'uri',
     359                        'context'         => array( 'view', 'edit', 'embed' ),
     360                        'readonly'        => true,
     361                );
     362                return $schema;
     363        }
     364
     365        /**
     366         * Handle an upload via raw POST data.
     367         *
     368         * @param array $data    Supplied file data.
     369         * @param array $headers HTTP headers from the request.
     370         * @return array|WP_Error Data from {@see wp_handle_sideload()}.
     371         */
     372        protected function upload_from_data( $data, $headers ) {
     373                if ( empty( $data ) ) {
     374                        return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
     375                }
     376
     377                if ( empty( $headers['content_type'] ) ) {
     378                        return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) );
     379                }
     380
     381                if ( empty( $headers['content_disposition'] ) ) {
     382                        return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) );
     383                }
     384
     385                $filename = self::get_filename_from_disposition( $headers['content_disposition'] );
     386
     387                if ( empty( $filename ) ) {
     388                        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 ) );
     389                }
     390
     391                if ( ! empty( $headers['content_md5'] ) ) {
     392                        $content_md5 = array_shift( $headers['content_md5'] );
     393                        $expected = trim( $content_md5 );
     394                        $actual   = md5( $data );
     395
     396                        if ( $expected !== $actual ) {
     397                                return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
     398                        }
     399                }
     400
     401                // Get the content-type.
     402                $type = array_shift( $headers['content_type'] );
     403
     404                /** Include admin functions to get access to wp_tempnam() and wp_handle_sideload() */
     405                require_once ABSPATH . 'wp-admin/includes/admin.php';
     406
     407                // Save the file.
     408                $tmpfname = wp_tempnam( $filename );
     409
     410                $fp = fopen( $tmpfname, 'w+' );
     411
     412                if ( ! $fp ) {
     413                        return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) );
     414                }
     415
     416                fwrite( $fp, $data );
     417                fclose( $fp );
     418
     419                // Now, sideload it in.
     420                $file_data = array(
     421                        'error'    => null,
     422                        'tmp_name' => $tmpfname,
     423                        'name'     => $filename,
     424                        'type'     => $type,
     425                );
     426                $overrides = array(
     427                        'test_form' => false,
     428                );
     429                $sideloaded = wp_handle_sideload( $file_data, $overrides );
     430
     431                if ( isset( $sideloaded['error'] ) ) {
     432                        // @codingStandardsIgnoreStart
     433                        @unlink( $tmpfname );
     434                        // @codingStandardsIgnoreEnd
     435                        return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) );
     436                }
     437
     438                return $sideloaded;
     439        }
     440
     441        /**
     442         * Parse filename from a Content-Disposition header value.
     443         *
     444         * As per RFC6266:
     445         *
     446         *     content-disposition = "Content-Disposition" ":"
     447         *                            disposition-type *( ";" disposition-parm )
     448         *
     449         *     disposition-type    = "inline" | "attachment" | disp-ext-type
     450         *                         ; case-insensitive
     451         *     disp-ext-type       = token
     452         *
     453         *     disposition-parm    = filename-parm | disp-ext-parm
     454         *
     455         *     filename-parm       = "filename" "=" value
     456         *                         | "filename*" "=" ext-value
     457         *
     458         *     disp-ext-parm       = token "=" value
     459         *                         | ext-token "=" ext-value
     460         *     ext-token           = <the characters in token, followed by "*">
     461         *
     462         * @see http://tools.ietf.org/html/rfc2388
     463         * @see http://tools.ietf.org/html/rfc6266
     464         *
     465         * @param string[] $disposition_header List of Content-Disposition header values.
     466         * @return string|null Filename if available, or null if not found.
     467         */
     468        public static function get_filename_from_disposition( $disposition_header ) {
     469                // Get the filename.
     470                $filename = null;
     471
     472                foreach ( $disposition_header as $value ) {
     473                        $value = trim( $value );
     474
     475                        if ( strpos( $value, ';' ) === false ) {
     476                                continue;
     477                        }
     478
     479                        list( $type, $attr_parts ) = explode( ';', $value, 2 );
     480                        $attr_parts = explode( ';', $attr_parts );
     481                        $attributes = array();
     482                        foreach ( $attr_parts as $part ) {
     483                                if ( strpos( $part, '=' ) === false ) {
     484                                        continue;
     485                                }
     486
     487                                list( $key, $value ) = explode( '=', $part, 2 );
     488                                $attributes[ trim( $key ) ] = trim( $value );
     489                        }
     490
     491                        if ( empty( $attributes['filename'] ) ) {
     492                                continue;
     493                        }
     494
     495                        $filename = trim( $attributes['filename'] );
     496
     497                        // Unquote quoted filename, but after trimming.
     498                        if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
     499                                $filename = substr( $filename, 1, -1 );
     500                        }
     501                }
     502
     503                return $filename;
     504        }
     505
     506        /**
     507         * Get the query params for collections of attachments.
     508         *
     509         * @return array Query parameters for the attachment collection as an array.
     510         */
     511        public function get_collection_params() {
     512                $params = parent::get_collection_params();
     513                $params['status']['default'] = 'inherit';
     514                $params['status']['enum'] = array( 'inherit', 'private', 'trash' );
     515                $media_types = $this->get_media_types();
     516                $params['media_type'] = array(
     517                        'default'            => null,
     518                        'description'        => __( 'Limit result set to attachments of a particular media type.' ),
     519                        'type'               => 'string',
     520                        'enum'               => array_keys( $media_types ),
     521                        'validate_callback'  => 'rest_validate_request_arg',
     522                );
     523                $params['mime_type'] = array(
     524                        'default'            => null,
     525                        'description'        => __( 'Limit result set to attachments of a particular MIME type.' ),
     526                        'type'               => 'string',
     527                );
     528                return $params;
     529        }
     530
     531        /**
     532         * Validate whether the user can query private statuses
     533         *
     534         * @param  mixed           $value     Status value.
     535         * @param  WP_REST_Request $request   Request object.
     536         * @param  string          $parameter Additional parameter to pass to validation.
     537         * @return WP_Error|boolean Boolean true if the user may query, WP_Error if not.
     538         */
     539        public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
     540                if ( 'inherit' === $value ) {
     541                        return true;
     542                }
     543                return parent::validate_user_can_query_private_statuses( $value, $request, $parameter );
     544        }
     545
     546        /**
     547         * Handle an upload via multipart/form-data ($_FILES).
     548         *
     549         * @param array $files   Data from $_FILES.
     550         * @param array $headers HTTP headers from the request.
     551         * @return array|WP_Error Data from {@see wp_handle_upload()}.
     552         */
     553        protected function upload_from_file( $files, $headers ) {
     554                if ( empty( $files ) ) {
     555                        return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
     556                }
     557
     558                // Verify hash, if given.
     559                if ( ! empty( $headers['content_md5'] ) ) {
     560                        $content_md5 = array_shift( $headers['content_md5'] );
     561                        $expected = trim( $content_md5 );
     562                        $actual = md5_file( $files['file']['tmp_name'] );
     563                        if ( $expected !== $actual ) {
     564                                return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
     565                        }
     566                }
     567
     568                // Pass off to WP to handle the actual upload.
     569                $overrides = array(
     570                        'test_form'   => false,
     571                );
     572                // Bypasses is_uploaded_file() when running unit tests.
     573                if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
     574                        $overrides['action'] = 'wp_handle_mock_upload';
     575                }
     576
     577                // Include admin functions to get access to wp_handle_upload().
     578                require_once ABSPATH . 'wp-admin/includes/admin.php';
     579                $file = wp_handle_upload( $files['file'], $overrides );
     580
     581                if ( isset( $file['error'] ) ) {
     582                        return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
     583                }
     584
     585                return $file;
     586        }
     587
     588        /**
     589         * Get the supported media types.
     590         *
     591         * Media types are considered the MIME type category.
     592         *
     593         * @return array
     594         */
     595        protected function get_media_types() {
     596                $media_types = array();
     597                foreach ( get_allowed_mime_types() as $mime_type ) {
     598                        $parts = explode( '/', $mime_type );
     599                        if ( ! isset( $media_types[ $parts[0] ] ) ) {
     600                                $media_types[ $parts[0] ] = array();
     601                        }
     602                        $media_types[ $parts[0] ][] = $mime_type;
     603                }
     604                return $media_types;
     605        }
     606
     607}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3/**
     4 * Access comments
     5 */
     6class WP_REST_Comments_Controller extends WP_REST_Controller {
     7
     8        /**
     9         * Instance of a comment meta fields object.
     10         *
     11         * @access protected
     12         * @var WP_REST_Comment_Meta_Fields
     13         */
     14        protected $meta;
     15
     16        public function __construct() {
     17                $this->namespace = 'wp/v2';
     18                $this->rest_base = 'comments';
     19
     20                $this->meta = new WP_REST_Comment_Meta_Fields();
     21        }
     22
     23        /**
     24         * Register the routes for the objects of the controller.
     25         */
     26        public function register_routes() {
     27
     28                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     29                        array(
     30                                'methods'   => WP_REST_Server::READABLE,
     31                                'callback'  => array( $this, 'get_items' ),
     32                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     33                                'args'      => $this->get_collection_params(),
     34                        ),
     35                        array(
     36                                'methods'  => WP_REST_Server::CREATABLE,
     37                                'callback' => array( $this, 'create_item' ),
     38                                'permission_callback' => array( $this, 'create_item_permissions_check' ),
     39                                'args'     => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
     40                        ),
     41                        'schema' => array( $this, 'get_public_item_schema' ),
     42                ) );
     43
     44                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
     45                        array(
     46                                'methods'  => WP_REST_Server::READABLE,
     47                                'callback' => array( $this, 'get_item' ),
     48                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     49                                'args'     => array(
     50                                        'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
     51                                ),
     52                        ),
     53                        array(
     54                                'methods'  => WP_REST_Server::EDITABLE,
     55                                'callback' => array( $this, 'update_item' ),
     56                                'permission_callback' => array( $this, 'update_item_permissions_check' ),
     57                                'args'     => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
     58                        ),
     59                        array(
     60                                'methods'  => WP_REST_Server::DELETABLE,
     61                                'callback' => array( $this, 'delete_item' ),
     62                                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
     63                                'args'     => array(
     64                                        'force'    => array(
     65                                                'default'     => false,
     66                                                'description' => __( 'Whether to bypass trash and force deletion.' ),
     67                                        ),
     68                                ),
     69                        ),
     70                        'schema' => array( $this, 'get_public_item_schema' ),
     71                ) );
     72        }
     73
     74        /**
     75         * Check if a given request has access to read comments
     76         *
     77         * @param  WP_REST_Request $request Full details about the request.
     78         * @return WP_Error|boolean
     79         */
     80        public function get_items_permissions_check( $request ) {
     81
     82                if ( ! empty( $request['post'] ) ) {
     83                        foreach ( (array) $request['post'] as $post_id ) {
     84                                $post = $this->get_post( $post_id );
     85                                if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post ) ) {
     86                                        return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
     87                                } elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
     88                                        return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot read comments without a post.' ), array( 'status' => rest_authorization_required_code() ) );
     89                                }
     90                        }
     91                }
     92
     93                if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
     94                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view comments with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     95                }
     96
     97                if ( ! current_user_can( 'edit_posts' ) ) {
     98                        $protected_params = array( 'author', 'author_exclude', 'karma', 'author_email', 'type', 'status' );
     99                        $forbidden_params = array();
     100                        foreach ( $protected_params as $param ) {
     101                                if ( 'status' === $param ) {
     102                                        if ( 'approve' !== $request[ $param ] ) {
     103                                                $forbidden_params[] = $param;
     104                                        }
     105                                } elseif ( 'type' === $param ) {
     106                                        if ( 'comment' !== $request[ $param ] ) {
     107                                                $forbidden_params[] = $param;
     108                                        }
     109                                } elseif ( ! empty( $request[ $param ] ) ) {
     110                                        $forbidden_params[] = $param;
     111                                }
     112                        }
     113                        if ( ! empty( $forbidden_params ) ) {
     114                                return new WP_Error( 'rest_forbidden_param', sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ), array( 'status' => rest_authorization_required_code() ) );
     115                        }
     116                }
     117
     118                return true;
     119        }
     120
     121        /**
     122         * Get a list of comments.
     123         *
     124         * @param  WP_REST_Request $request Full details about the request.
     125         * @return WP_Error|WP_REST_Response
     126         */
     127        public function get_items( $request ) {
     128
     129                // Retrieve the list of registered collection query parameters.
     130                $registered = $this->get_collection_params();
     131
     132                // This array defines mappings between public API query parameters whose
     133                // values are accepted as-passed, and their internal WP_Query parameter
     134                // name equivalents (some are the same). Only values which are also
     135                // present in $registered will be set.
     136                $parameter_mappings = array(
     137                        'author'         => 'author__in',
     138                        'author_email'   => 'author_email',
     139                        'author_exclude' => 'author__not_in',
     140                        'exclude'        => 'comment__not_in',
     141                        'include'        => 'comment__in',
     142                        'karma'          => 'karma',
     143                        'offset'         => 'offset',
     144                        'order'          => 'order',
     145                        'parent'         => 'parent__in',
     146                        'parent_exclude' => 'parent__not_in',
     147                        'per_page'       => 'number',
     148                        'post'           => 'post__in',
     149                        'search'         => 'search',
     150                        'status'         => 'status',
     151                        'type'           => 'type',
     152                );
     153
     154                $prepared_args = array();
     155
     156                // For each known parameter which is both registered and present in the request,
     157                // set the parameter's value on the query $prepared_args.
     158                foreach ( $parameter_mappings as $api_param => $wp_param ) {
     159                        if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
     160                                $prepared_args[ $wp_param ] = $request[ $api_param ];
     161                        }
     162                }
     163
     164                // Ensure certain parameter values default to empty strings.
     165                foreach ( array( 'author_email', 'karma', 'search' ) as $param ) {
     166                        if ( ! isset( $prepared_args[ $param ] ) ) {
     167                                $prepared_args[ $param ] = '';
     168                        }
     169                }
     170
     171                if ( isset( $registered['orderby'] ) ) {
     172                        $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
     173                }
     174
     175                $prepared_args['no_found_rows'] = false;
     176
     177                $prepared_args['date_query'] = array();
     178                // Set before into date query. Date query must be specified as an array of an array.
     179                if ( isset( $registered['before'], $request['before'] ) ) {
     180                        $prepared_args['date_query'][0]['before'] = $request['before'];
     181                }
     182
     183                // Set after into date query. Date query must be specified as an array of an array.
     184                if ( isset( $registered['after'], $request['after'] ) ) {
     185                        $prepared_args['date_query'][0]['after'] = $request['after'];
     186                }
     187
     188                if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
     189                        $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
     190                }
     191
     192                /**
     193                 * Filter arguments, before passing to WP_Comment_Query, when querying comments via the REST API.
     194                 *
     195                 * @see https://developer.wordpress.org/reference/classes/wp_comment_query/
     196                 *
     197                 * @param array           $prepared_args Array of arguments for WP_Comment_Query.
     198                 * @param WP_REST_Request $request       The current request.
     199                 */
     200                $prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request );
     201
     202                $query = new WP_Comment_Query;
     203                $query_result = $query->query( $prepared_args );
     204
     205                $comments = array();
     206                foreach ( $query_result as $comment ) {
     207                        if ( ! $this->check_read_permission( $comment ) ) {
     208                                continue;
     209                        }
     210
     211                        $data = $this->prepare_item_for_response( $comment, $request );
     212                        $comments[] = $this->prepare_response_for_collection( $data );
     213                }
     214
     215                $total_comments = (int) $query->found_comments;
     216                $max_pages = (int) $query->max_num_pages;
     217                if ( $total_comments < 1 ) {
     218                        // Out-of-bounds, run the query again without LIMIT for total count
     219                        unset( $prepared_args['number'], $prepared_args['offset'] );
     220                        $query = new WP_Comment_Query;
     221                        $prepared_args['count'] = true;
     222
     223                        $total_comments = $query->query( $prepared_args );
     224                        $max_pages = ceil( $total_comments / $request['per_page'] );
     225                }
     226
     227                $response = rest_ensure_response( $comments );
     228                $response->header( 'X-WP-Total', $total_comments );
     229                $response->header( 'X-WP-TotalPages', $max_pages );
     230
     231                $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
     232                if ( $request['page'] > 1 ) {
     233                        $prev_page = $request['page'] - 1;
     234                        if ( $prev_page > $max_pages ) {
     235                                $prev_page = $max_pages;
     236                        }
     237                        $prev_link = add_query_arg( 'page', $prev_page, $base );
     238                        $response->link_header( 'prev', $prev_link );
     239                }
     240                if ( $max_pages > $request['page'] ) {
     241                        $next_page = $request['page'] + 1;
     242                        $next_link = add_query_arg( 'page', $next_page, $base );
     243                        $response->link_header( 'next', $next_link );
     244                }
     245
     246                return $response;
     247        }
     248
     249        /**
     250         * Check if a given request has access to read the comment
     251         *
     252         * @param  WP_REST_Request $request Full details about the request.
     253         * @return WP_Error|boolean
     254         */
     255        public function get_item_permissions_check( $request ) {
     256                $id = (int) $request['id'];
     257
     258                $comment = get_comment( $id );
     259
     260                if ( ! $comment ) {
     261                        return true;
     262                }
     263
     264                if ( ! $this->check_read_permission( $comment ) ) {
     265                        return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot read this comment.' ), array( 'status' => rest_authorization_required_code() ) );
     266                }
     267
     268                $post = $this->get_post( $comment->comment_post_ID );
     269
     270                if ( $post && ! $this->check_read_post_permission( $post ) ) {
     271                        return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
     272                }
     273
     274                if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
     275                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this comment with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     276                }
     277
     278                return true;
     279        }
     280
     281        /**
     282         * Get a comment.
     283         *
     284         * @param  WP_REST_Request $request Full details about the request.
     285         * @return WP_Error|WP_REST_Response
     286         */
     287        public function get_item( $request ) {
     288                $id = (int) $request['id'];
     289
     290                $comment = get_comment( $id );
     291                if ( empty( $comment ) ) {
     292                        return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
     293                }
     294
     295                if ( ! empty( $comment->comment_post_ID ) ) {
     296                        $post = $this->get_post( $comment->comment_post_ID );
     297                        if ( empty( $post ) ) {
     298                                return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
     299                        }
     300                }
     301
     302                $data = $this->prepare_item_for_response( $comment, $request );
     303                $response = rest_ensure_response( $data );
     304
     305                return $response;
     306        }
     307
     308        /**
     309         * Check if a given request has access to create a comment
     310         *
     311         * @param  WP_REST_Request $request Full details about the request.
     312         * @return WP_Error|boolean
     313         */
     314        public function create_item_permissions_check( $request ) {
     315
     316                if ( ! is_user_logged_in() && get_option( 'comment_registration' ) ) {
     317                        return new WP_Error( 'rest_comment_login_required', __( 'Sorry, you must be logged in to comment.' ), array( 'status' => 401 ) );
     318                }
     319
     320                // Limit who can set comment `author`, `karma` or `status` to anything other than the default.
     321                if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
     322                        return new WP_Error( 'rest_comment_invalid_author', __( 'Comment author invalid.' ), array( 'status' => rest_authorization_required_code() ) );
     323                }
     324                if ( isset( $request['karma'] ) && $request['karma'] > 0 && ! current_user_can( 'moderate_comments' ) ) {
     325                        return new WP_Error( 'rest_comment_invalid_karma', __( 'Sorry, you cannot set karma for comments.' ), array( 'status' => rest_authorization_required_code() ) );
     326                }
     327                if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
     328                        return new WP_Error( 'rest_comment_invalid_status', __( 'Sorry, you cannot set status for comments.' ), array( 'status' => rest_authorization_required_code() ) );
     329                }
     330
     331                if ( empty( $request['post'] ) && ! current_user_can( 'moderate_comments' ) ) {
     332                        return new WP_Error( 'rest_comment_invalid_post_id', __( 'Sorry, you cannot create this comment without a post.' ), array( 'status' => rest_authorization_required_code() ) );
     333                }
     334
     335                if ( ! empty( $request['post'] ) && $post = $this->get_post( (int) $request['post'] ) ) {
     336                        if ( 'draft' === $post->post_status ) {
     337                                return new WP_Error( 'rest_comment_draft_post', __( 'Sorry, you cannot create a comment on this post.' ), array( 'status' => 403 ) );
     338                        }
     339
     340                        if ( 'trash' === $post->post_status ) {
     341                                return new WP_Error( 'rest_comment_trash_post', __( 'Sorry, you cannot create a comment on this post.' ), array( 'status' => 403 ) );
     342                        }
     343
     344                        if ( ! $this->check_read_post_permission( $post ) ) {
     345                                return new WP_Error( 'rest_cannot_read_post', __( 'Sorry, you cannot read the post for this comment.' ), array( 'status' => rest_authorization_required_code() ) );
     346                        }
     347
     348                        if ( ! comments_open( $post->ID ) ) {
     349                                return new WP_Error( 'rest_comment_closed', __( 'Sorry, comments are closed on this post.' ), array( 'status' => 403 ) );
     350                        }
     351                }
     352
     353                return true;
     354        }
     355
     356        /**
     357         * Create a comment.
     358         *
     359         * @param  WP_REST_Request $request Full details about the request.
     360         * @return WP_Error|WP_REST_Response
     361         */
     362        public function create_item( $request ) {
     363                if ( ! empty( $request['id'] ) ) {
     364                        return new WP_Error( 'rest_comment_exists', __( 'Cannot create existing comment.' ), array( 'status' => 400 ) );
     365                }
     366
     367                $prepared_comment = $this->prepare_item_for_database( $request );
     368                if ( is_wp_error( $prepared_comment ) ) {
     369                        return $prepared_comment;
     370                }
     371
     372                /**
     373                 * Do not allow a comment to be created with an empty string for
     374                 * comment_content.
     375                 * See `wp_handle_comment_submission()`.
     376                 */
     377                if ( '' === $prepared_comment['comment_content'] ) {
     378                        return new WP_Error( 'rest_comment_content_invalid', __( 'Comment content is invalid.' ), array( 'status' => 400 ) );
     379                }
     380
     381                // Setting remaining values before wp_insert_comment so we can
     382                // use wp_allow_comment().
     383                if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
     384                        $prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
     385                }
     386
     387                // Set author data if the user's logged in
     388                $missing_author = empty( $prepared_comment['user_id'] )
     389                        && empty( $prepared_comment['comment_author'] )
     390                        && empty( $prepared_comment['comment_author_email'] )
     391                        && empty( $prepared_comment['comment_author_url'] );
     392
     393                if ( is_user_logged_in() && $missing_author ) {
     394                        $user = wp_get_current_user();
     395                        $prepared_comment['user_id'] = $user->ID;
     396                        $prepared_comment['comment_author'] = $user->display_name;
     397                        $prepared_comment['comment_author_email'] = $user->user_email;
     398                        $prepared_comment['comment_author_url'] = $user->user_url;
     399                }
     400
     401                // Honor the discussion setting that requires a name and email address
     402                // of the comment author.
     403                if ( get_option( 'require_name_email' ) ) {
     404                        if ( ! isset( $prepared_comment['comment_author'] ) && ! isset( $prepared_comment['comment_author_email'] ) ) {
     405                                return new WP_Error( 'rest_comment_author_data_required', __( 'Creating a comment requires valid author name and email values.' ), array( 'status' => 400 ) );
     406                        }
     407                        if ( ! isset( $prepared_comment['comment_author'] ) ) {
     408                                return new WP_Error( 'rest_comment_author_required', __( 'Creating a comment requires a valid author name.' ), array( 'status' => 400 ) );
     409                        }
     410                        if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
     411                                return new WP_Error( 'rest_comment_author_email_required', __( 'Creating a comment requires a valid author email.' ), array( 'status' => 400 ) );
     412                        }
     413                }
     414
     415                if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
     416                        $prepared_comment['comment_author_email'] = '';
     417                }
     418                if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
     419                        $prepared_comment['comment_author_url'] = '';
     420                }
     421
     422                $prepared_comment['comment_agent'] = '';
     423                $prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true );
     424
     425                if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
     426                        $error_code = $prepared_comment['comment_approved']->get_error_code();
     427                        $error_message = $prepared_comment['comment_approved']->get_error_message();
     428
     429                        if ( 'comment_duplicate' === $error_code ) {
     430                                return new WP_Error( $error_code, $error_message, array( 'status' => 409 ) );
     431                        }
     432
     433                        if ( 'comment_flood' === $error_code ) {
     434                                return new WP_Error( $error_code, $error_message, array( 'status' => 400 ) );
     435                        }
     436
     437                        return $prepared_comment['comment_approved'];
     438                }
     439
     440                /**
     441                 * Filter a comment before it is inserted via the REST API.
     442                 *
     443                 * Allows modification of the comment right before it is inserted via `wp_insert_comment`.
     444                 *
     445                 * @param array           $prepared_comment The prepared comment data for `wp_insert_comment`.
     446                 * @param WP_REST_Request $request          Request used to insert the comment.
     447                 */
     448                $prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
     449
     450                $comment_id = wp_insert_comment( $prepared_comment );
     451                if ( ! $comment_id ) {
     452                        return new WP_Error( 'rest_comment_failed_create', __( 'Creating comment failed.' ), array( 'status' => 500 ) );
     453                }
     454
     455                if ( isset( $request['status'] ) ) {
     456                        $comment = get_comment( $comment_id );
     457                        $this->handle_status_param( $request['status'], $comment );
     458                }
     459
     460                $schema = $this->get_item_schema();
     461                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     462                        $meta_update = $this->meta->update_value( $request['meta'], $comment_id );
     463                        if ( is_wp_error( $meta_update ) ) {
     464                                return $meta_update;
     465                        }
     466                }
     467
     468                $comment = get_comment( $comment_id );
     469                $fields_update = $this->update_additional_fields_for_object( $comment, $request );
     470                if ( is_wp_error( $fields_update ) ) {
     471                        return $fields_update;
     472                }
     473
     474                $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
     475                $request->set_param( 'context', $context );
     476                $response = $this->prepare_item_for_response( $comment, $request );
     477                $response = rest_ensure_response( $response );
     478                $response->set_status( 201 );
     479                $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );
     480
     481                /**
     482                 * Fires after a comment is created or updated via the REST API.
     483                 *
     484                 * @param array           $comment  Comment as it exists in the database.
     485                 * @param WP_REST_Request $request  The request sent to the API.
     486                 * @param boolean         $creating True when creating a comment, false when updating.
     487                 */
     488                do_action( 'rest_insert_comment', $comment, $request, true );
     489
     490                return $response;
     491        }
     492
     493        /**
     494         * Check if a given request has access to update a comment
     495         *
     496         * @param  WP_REST_Request $request Full details about the request.
     497         * @return WP_Error|boolean
     498         */
     499        public function update_item_permissions_check( $request ) {
     500
     501                $id = (int) $request['id'];
     502
     503                $comment = get_comment( $id );
     504
     505                if ( $comment && ! $this->check_edit_permission( $comment ) ) {
     506                        return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you can not edit this comment.' ), array( 'status' => rest_authorization_required_code() ) );
     507                }
     508
     509                return true;
     510        }
     511
     512        /**
     513         * Edit a comment
     514         *
     515         * @param  WP_REST_Request $request Full details about the request.
     516         * @return WP_Error|WP_REST_Response
     517         */
     518        public function update_item( $request ) {
     519                $id = (int) $request['id'];
     520
     521                $comment = get_comment( $id );
     522                if ( empty( $comment ) ) {
     523                        return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
     524                }
     525
     526                if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) {
     527                        return new WP_Error( 'rest_comment_invalid_type', __( 'Sorry, you cannot change the comment type.' ), array( 'status' => 404 ) );
     528                }
     529
     530                $prepared_args = $this->prepare_item_for_database( $request );
     531                if ( is_wp_error( $prepared_args ) ) {
     532                        return $prepared_args;
     533                }
     534
     535                if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
     536                        // Only the comment status is being changed.
     537                        $change = $this->handle_status_param( $request['status'], $comment );
     538                        if ( ! $change ) {
     539                                return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), array( 'status' => 500 ) );
     540                        }
     541                } else {
     542                        if ( is_wp_error( $prepared_args ) ) {
     543                                return $prepared_args;
     544                        }
     545
     546                        $prepared_args['comment_ID'] = $id;
     547
     548                        $updated = wp_update_comment( $prepared_args );
     549                        if ( 0 === $updated ) {
     550                                return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment failed.' ), array( 'status' => 500 ) );
     551                        }
     552
     553                        if ( isset( $request['status'] ) ) {
     554                                $this->handle_status_param( $request['status'], $comment );
     555                        }
     556                }
     557
     558                $schema = $this->get_item_schema();
     559                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     560                        $meta_update = $this->meta->update_value( $request['meta'], $id );
     561                        if ( is_wp_error( $meta_update ) ) {
     562                                return $meta_update;
     563                        }
     564                }
     565
     566                $comment = get_comment( $id );
     567                $fields_update = $this->update_additional_fields_for_object( $comment, $request );
     568                if ( is_wp_error( $fields_update ) ) {
     569                        return $fields_update;
     570                }
     571
     572                $request->set_param( 'context', 'edit' );
     573                $response = $this->prepare_item_for_response( $comment, $request );
     574
     575                /* This action is documented in lib/endpoints/class-wp-rest-comments-controller.php */
     576                do_action( 'rest_insert_comment', $comment, $request, false );
     577
     578                return rest_ensure_response( $response );
     579        }
     580
     581        /**
     582         * Check if a given request has access to delete a comment
     583         *
     584         * @param  WP_REST_Request $request Full details about the request.
     585         * @return WP_Error|boolean
     586         */
     587        public function delete_item_permissions_check( $request ) {
     588                $id = (int) $request['id'];
     589                $comment = get_comment( $id );
     590                if ( ! $comment ) {
     591                        return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
     592                }
     593                if ( ! $this->check_edit_permission( $comment ) ) {
     594                        return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you can not delete this comment.' ), array( 'status' => rest_authorization_required_code() ) );
     595                }
     596                return true;
     597        }
     598
     599        /**
     600         * Delete a comment.
     601         *
     602         * @param  WP_REST_Request $request Full details about the request.
     603         * @return WP_Error|WP_REST_Response
     604         */
     605        public function delete_item( $request ) {
     606                $id = (int) $request['id'];
     607                $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
     608
     609                $comment = get_comment( $id );
     610                if ( empty( $comment ) ) {
     611                        return new WP_Error( 'rest_comment_invalid_id', __( 'Invalid comment id.' ), array( 'status' => 404 ) );
     612                }
     613
     614                /**
     615                 * Filter whether a comment is trashable.
     616                 *
     617                 * Return false to disable trash support for the post.
     618                 *
     619                 * @param boolean $supports_trash Whether the post type support trashing.
     620                 * @param WP_Post $comment        The comment object being considered for trashing support.
     621                 */
     622                $supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
     623
     624                $request->set_param( 'context', 'edit' );
     625                $response = $this->prepare_item_for_response( $comment, $request );
     626
     627                if ( $force ) {
     628                        $result = wp_delete_comment( $comment->comment_ID, true );
     629                } else {
     630                        // If we don't support trashing for this type, error out
     631                        if ( ! $supports_trash ) {
     632                                return new WP_Error( 'rest_trash_not_supported', __( 'The comment does not support trashing.' ), array( 'status' => 501 ) );
     633                        }
     634
     635                        if ( 'trash' === $comment->comment_approved ) {
     636                                return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.' ), array( 'status' => 410 ) );
     637                        }
     638
     639                        $result = wp_trash_comment( $comment->comment_ID );
     640                }
     641
     642                if ( ! $result ) {
     643                        return new WP_Error( 'rest_cannot_delete', __( 'The comment cannot be deleted.' ), array( 'status' => 500 ) );
     644                }
     645
     646                /**
     647                 * Fires after a comment is deleted via the REST API.
     648                 *
     649                 * @param object           $comment  The deleted comment data.
     650                 * @param WP_REST_Response $response The response returned from the API.
     651                 * @param WP_REST_Request  $request  The request sent to the API.
     652                 */
     653                do_action( 'rest_delete_comment', $comment, $response, $request );
     654
     655                return $response;
     656        }
     657
     658        /**
     659         * Prepare a single comment output for response.
     660         *
     661         * @param  object          $comment Comment object.
     662         * @param  WP_REST_Request $request Request object.
     663         * @return WP_REST_Response $response
     664         */
     665        public function prepare_item_for_response( $comment, $request ) {
     666                $data = array(
     667                        'id'                 => (int) $comment->comment_ID,
     668                        'post'               => (int) $comment->comment_post_ID,
     669                        'parent'             => (int) $comment->comment_parent,
     670                        'author'             => (int) $comment->user_id,
     671                        'author_name'        => $comment->comment_author,
     672                        'author_email'       => $comment->comment_author_email,
     673                        'author_url'         => $comment->comment_author_url,
     674                        'author_ip'          => $comment->comment_author_IP,
     675                        'author_user_agent'  => $comment->comment_agent,
     676                        'date'               => mysql_to_rfc3339( $comment->comment_date ),
     677                        'date_gmt'           => mysql_to_rfc3339( $comment->comment_date_gmt ),
     678                        'content'            => array(
     679                                'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment ),
     680                                'raw'      => $comment->comment_content,
     681                        ),
     682                        'karma'              => (int) $comment->comment_karma,
     683                        'link'               => get_comment_link( $comment ),
     684                        'status'             => $this->prepare_status_response( $comment->comment_approved ),
     685                        'type'               => get_comment_type( $comment->comment_ID ),
     686                );
     687
     688                $schema = $this->get_item_schema();
     689
     690                if ( ! empty( $schema['properties']['author_avatar_urls'] ) ) {
     691                        $data['author_avatar_urls'] = rest_get_avatar_urls( $comment->comment_author_email );
     692                }
     693
     694                if ( ! empty( $schema['properties']['meta'] ) ) {
     695                        $data['meta'] = $this->meta->get_value( $comment->comment_ID, $request );
     696                }
     697
     698                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     699                $data = $this->add_additional_fields_to_object( $data, $request );
     700                $data = $this->filter_response_by_context( $data, $context );
     701
     702                // Wrap the data in a response object
     703                $response = rest_ensure_response( $data );
     704
     705                $response->add_links( $this->prepare_links( $comment ) );
     706
     707                /**
     708                 * Filter a comment returned from the API.
     709                 *
     710                 * Allows modification of the comment right before it is returned.
     711                 *
     712                 * @param WP_REST_Response  $response   The response object.
     713                 * @param object            $comment    The original comment object.
     714                 * @param WP_REST_Request   $request    Request used to generate the response.
     715                 */
     716                return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
     717        }
     718
     719        /**
     720         * Prepare links for the request.
     721         *
     722         * @param object $comment Comment object.
     723         * @return array Links for the given comment.
     724         */
     725        protected function prepare_links( $comment ) {
     726                $links = array(
     727                        'self' => array(
     728                                'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ),
     729                        ),
     730                        'collection' => array(
     731                                'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
     732                        ),
     733                );
     734
     735                if ( 0 !== (int) $comment->user_id ) {
     736                        $links['author'] = array(
     737                                'href'       => rest_url( 'wp/v2/users/' . $comment->user_id ),
     738                                'embeddable' => true,
     739                        );
     740                }
     741
     742                if ( 0 !== (int) $comment->comment_post_ID ) {
     743                        $post = $this->get_post( $comment->comment_post_ID );
     744                        if ( ! empty( $post->ID ) ) {
     745                                $obj = get_post_type_object( $post->post_type );
     746                                $base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
     747
     748                                $links['up'] = array(
     749                                        'href'       => rest_url( 'wp/v2/' . $base . '/' . $comment->comment_post_ID ),
     750                                        'embeddable' => true,
     751                                        'post_type'  => $post->post_type,
     752                                );
     753                        }
     754                }
     755
     756                if ( 0 !== (int) $comment->comment_parent ) {
     757                        $links['in-reply-to'] = array(
     758                                'href'       => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ),
     759                                'embeddable' => true,
     760                        );
     761                }
     762
     763                // Only grab one comment to verify the comment has children.
     764                $comment_children = $comment->get_children( array( 'number' => 1, 'count' => true ) );
     765                if ( ! empty( $comment_children ) ) {
     766                        $args = array( 'parent' => $comment->comment_ID );
     767                        $rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
     768
     769                        $links['children'] = array(
     770                                'href' => $rest_url,
     771                        );
     772                }
     773
     774                return $links;
     775        }
     776
     777        /**
     778         * Prepend internal property prefix to query parameters to match our response fields.
     779         *
     780         * @param  string $query_param
     781         * @return string $normalized
     782         */
     783        protected function normalize_query_param( $query_param ) {
     784                $prefix = 'comment_';
     785
     786                switch ( $query_param ) {
     787                        case 'id':
     788                                $normalized = $prefix . 'ID';
     789                                break;
     790                        case 'post':
     791                                $normalized = $prefix . 'post_ID';
     792                                break;
     793                        case 'parent':
     794                                $normalized = $prefix . 'parent';
     795                                break;
     796                        case 'include':
     797                                $normalized = 'comment__in';
     798                                break;
     799                        default:
     800                                $normalized = $prefix . $query_param;
     801                                break;
     802                }
     803
     804                return $normalized;
     805        }
     806
     807        /**
     808         * Check comment_approved to set comment status for single comment output.
     809         *
     810         * @param  string|int $comment_approved
     811         * @return string     $status
     812         */
     813        protected function prepare_status_response( $comment_approved ) {
     814
     815                switch ( $comment_approved ) {
     816                        case 'hold':
     817                        case '0':
     818                                $status = 'hold';
     819                                break;
     820
     821                        case 'approve':
     822                        case '1':
     823                                $status = 'approved';
     824                                break;
     825
     826                        case 'spam':
     827                        case 'trash':
     828                        default:
     829                                $status = $comment_approved;
     830                                break;
     831                }
     832
     833                return $status;
     834        }
     835
     836        /**
     837         * Prepare a single comment to be inserted into the database.
     838         *
     839         * @param  WP_REST_Request $request Request object.
     840         * @return array|WP_Error  $prepared_comment
     841         */
     842        protected function prepare_item_for_database( $request ) {
     843                $prepared_comment = array();
     844
     845                /**
     846                 * Allow the comment_content to be set via the 'content' or
     847                 * the 'content.raw' properties of the Request object.
     848                 */
     849                if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
     850                        $prepared_comment['comment_content'] = wp_filter_kses( $request['content'] );
     851                } elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
     852                        $prepared_comment['comment_content'] = wp_filter_kses( $request['content']['raw'] );
     853                }
     854
     855                if ( isset( $request['post'] ) ) {
     856                        $prepared_comment['comment_post_ID'] = (int) $request['post'];
     857                }
     858
     859                if ( isset( $request['parent'] ) ) {
     860                        $prepared_comment['comment_parent'] = $request['parent'];
     861                }
     862
     863                if ( isset( $request['author'] ) ) {
     864                        $user = new WP_User( $request['author'] );
     865                        if ( $user->exists() ) {
     866                                $prepared_comment['user_id'] = $user->ID;
     867                                $prepared_comment['comment_author'] = $user->display_name;
     868                                $prepared_comment['comment_author_email'] = $user->user_email;
     869                                $prepared_comment['comment_author_url'] = $user->user_url;
     870                        } else {
     871                                return new WP_Error( 'rest_comment_author_invalid', __( 'Invalid comment author id.' ), array( 'status' => 400 ) );
     872                        }
     873                }
     874
     875                if ( isset( $request['author_name'] ) ) {
     876                        $prepared_comment['comment_author'] = $request['author_name'];
     877                }
     878
     879                if ( isset( $request['author_email'] ) ) {
     880                        $prepared_comment['comment_author_email'] = $request['author_email'];
     881                }
     882
     883                if ( isset( $request['author_url'] ) ) {
     884                        $prepared_comment['comment_author_url'] = $request['author_url'];
     885                }
     886
     887                if ( isset( $request['author_ip'] ) ) {
     888                        $prepared_comment['comment_author_IP'] = $request['author_ip'];
     889                }
     890
     891                if ( isset( $request['type'] ) ) {
     892                        // Comment type "comment" needs to be created as an empty string.
     893                        $prepared_comment['comment_type'] = 'comment' === $request['type'] ? '' : $request['type'];
     894                }
     895
     896                if ( isset( $request['karma'] ) ) {
     897                        $prepared_comment['comment_karma'] = $request['karma'] ;
     898                }
     899
     900                if ( ! empty( $request['date'] ) ) {
     901                        $date_data = rest_get_date_with_gmt( $request['date'] );
     902
     903                        if ( ! empty( $date_data ) ) {
     904                                list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
     905                        }
     906                } elseif ( ! empty( $request['date_gmt'] ) ) {
     907                        $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
     908
     909                        if ( ! empty( $date_data ) ) {
     910                                list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
     911                        }
     912                }
     913
     914                // Require 'comment_content' unless only the 'comment_status' is being
     915                // updated.
     916                if ( ! empty( $prepared_comment ) && ! isset( $prepared_comment['comment_content'] ) ) {
     917                        return new WP_Error( 'rest_comment_content_required', __( 'Missing comment content.' ), array( 'status' => 400 ) );
     918                }
     919
     920                return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
     921        }
     922
     923        /**
     924         * Get the Comment's schema, conforming to JSON Schema
     925         *
     926         * @return array
     927         */
     928        public function get_item_schema() {
     929                $schema = array(
     930                        '$schema'              => 'http://json-schema.org/draft-04/schema#',
     931                        'title'                => 'comment',
     932                        'type'                 => 'object',
     933                        'properties'           => array(
     934                                'id'               => array(
     935                                        'description'  => __( 'Unique identifier for the object.' ),
     936                                        'type'         => 'integer',
     937                                        'context'      => array( 'view', 'edit', 'embed' ),
     938                                        'readonly'     => true,
     939                                ),
     940                                'author'           => array(
     941                                        'description'  => __( 'The id of the user object, if author was a user.' ),
     942                                        'type'         => 'integer',
     943                                        'context'      => array( 'view', 'edit', 'embed' ),
     944                                ),
     945                                'author_email'     => array(
     946                                        'description'  => __( 'Email address for the object author.' ),
     947                                        'type'         => 'string',
     948                                        'format'       => 'email',
     949                                        'context'      => array( 'edit' ),
     950                                ),
     951                                'author_ip'     => array(
     952                                        'description'  => __( 'IP address for the object author.' ),
     953                                        'type'         => 'string',
     954                                        'format'       => 'ipv4',
     955                                        'context'      => array( 'edit' ),
     956                                        'arg_options'  => array(
     957                                                'default'           => '127.0.0.1',
     958                                        ),
     959                                ),
     960                                'author_name'     => array(
     961                                        'description'  => __( 'Display name for the object author.' ),
     962                                        'type'         => 'string',
     963                                        'context'      => array( 'view', 'edit', 'embed' ),
     964                                        'arg_options'  => array(
     965                                                'sanitize_callback' => 'sanitize_text_field',
     966                                        ),
     967                                ),
     968                                'author_url'       => array(
     969                                        'description'  => __( 'URL for the object author.' ),
     970                                        'type'         => 'string',
     971                                        'format'       => 'uri',
     972                                        'context'      => array( 'view', 'edit', 'embed' ),
     973                                ),
     974                                'author_user_agent'     => array(
     975                                        'description'  => __( 'User agent for the object author.' ),
     976                                        'type'         => 'string',
     977                                        'context'      => array( 'edit' ),
     978                                        'readonly'     => true,
     979                                ),
     980                                'content'          => array(
     981                                        'description'     => __( 'The content for the object.' ),
     982                                        'type'            => 'object',
     983                                        'context'         => array( 'view', 'edit', 'embed' ),
     984                                        'properties'      => array(
     985                                                'raw'         => array(
     986                                                        'description'     => __( 'Content for the object, as it exists in the database.' ),
     987                                                        'type'            => 'string',
     988                                                        'context'         => array( 'edit' ),
     989                                                ),
     990                                                'rendered'    => array(
     991                                                        'description'     => __( 'HTML content for the object, transformed for display.' ),
     992                                                        'type'            => 'string',
     993                                                        'context'         => array( 'view', 'edit', 'embed' ),
     994                                                ),
     995                                        ),
     996                                ),
     997                                'date'             => array(
     998                                        'description'  => __( 'The date the object was published.' ),
     999                                        'type'         => 'string',
     1000                                        'format'       => 'date-time',
     1001                                        'context'      => array( 'view', 'edit', 'embed' ),
     1002                                ),
     1003                                'date_gmt'         => array(
     1004                                        'description'  => __( 'The date the object was published as GMT.' ),
     1005                                        'type'         => 'string',
     1006                                        'format'       => 'date-time',
     1007                                        'context'      => array( 'view', 'edit' ),
     1008                                ),
     1009                                'karma'             => array(
     1010                                        'description'  => __( 'Karma for the object.' ),
     1011                                        'type'         => 'integer',
     1012                                        'context'      => array( 'edit' ),
     1013                                ),
     1014                                'link'             => array(
     1015                                        'description'  => __( 'URL to the object.' ),
     1016                                        'type'         => 'string',
     1017                                        'format'       => 'uri',
     1018                                        'context'      => array( 'view', 'edit', 'embed' ),
     1019                                        'readonly'     => true,
     1020                                ),
     1021                                'parent'           => array(
     1022                                        'description'  => __( 'The id for the parent of the object.' ),
     1023                                        'type'         => 'integer',
     1024                                        'context'      => array( 'view', 'edit', 'embed' ),
     1025                                        'arg_options'  => array(
     1026                                                'default'           => 0,
     1027                                        ),
     1028                                ),
     1029                                'post'             => array(
     1030                                        'description'  => __( 'The id of the associated post object.' ),
     1031                                        'type'         => 'integer',
     1032                                        'context'      => array( 'view', 'edit' ),
     1033                                        'arg_options'  => array(
     1034                                                'default'           => 0,
     1035                                        ),
     1036                                ),
     1037                                'status'           => array(
     1038                                        'description'  => __( 'State of the object.' ),
     1039                                        'type'         => 'string',
     1040                                        'context'      => array( 'view', 'edit' ),
     1041                                        'arg_options'  => array(
     1042                                                'sanitize_callback' => 'sanitize_key',
     1043                                        ),
     1044                                ),
     1045                                'type'             => array(
     1046                                        'description'  => __( 'Type of Comment for the object.' ),
     1047                                        'type'         => 'string',
     1048                                        'context'      => array( 'view', 'edit', 'embed' ),
     1049                                        'default'      => 'comment',
     1050                                        'arg_options'  => array(
     1051                                                'sanitize_callback' => 'sanitize_key',
     1052                                        ),
     1053                                ),
     1054                        ),
     1055                );
     1056
     1057                if ( get_option( 'show_avatars' ) ) {
     1058                        $avatar_properties = array();
     1059
     1060                        $avatar_sizes = rest_get_avatar_sizes();
     1061                        foreach ( $avatar_sizes as $size ) {
     1062                                $avatar_properties[ $size ] = array(
     1063                                        'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
     1064                                        'type'        => 'string',
     1065                                        'format'      => 'uri',
     1066                                        'context'     => array( 'embed', 'view', 'edit' ),
     1067                                );
     1068                        }
     1069
     1070                        $schema['properties']['author_avatar_urls'] = array(
     1071                                'description'   => __( 'Avatar URLs for the object author.' ),
     1072                                'type'          => 'object',
     1073                                'context'       => array( 'view', 'edit', 'embed' ),
     1074                                'readonly'      => true,
     1075                                'properties'    => $avatar_properties,
     1076                        );
     1077                }
     1078
     1079                $schema['properties']['meta'] = $this->meta->get_field_schema();
     1080
     1081                return $this->add_additional_fields_schema( $schema );
     1082        }
     1083
     1084        /**
     1085         * Get the query params for collections
     1086         *
     1087         * @return array
     1088         */
     1089        public function get_collection_params() {
     1090                $query_params = parent::get_collection_params();
     1091
     1092                $query_params['context']['default'] = 'view';
     1093
     1094                $query_params['after'] = array(
     1095                        'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.' ),
     1096                        'type'              => 'string',
     1097                        'format'            => 'date-time',
     1098                        'validate_callback' => 'rest_validate_request_arg',
     1099                );
     1100                $query_params['author'] = array(
     1101                        'description'       => __( 'Limit result set to comments assigned to specific user ids. Requires authorization.' ),
     1102                        'sanitize_callback' => 'wp_parse_id_list',
     1103                        'type'              => 'array',
     1104                );
     1105                $query_params['author_exclude'] = array(
     1106                        'description'       => __( 'Ensure result set excludes comments assigned to specific user ids. Requires authorization.' ),
     1107                        'sanitize_callback' => 'wp_parse_id_list',
     1108                        'type'              => 'array',
     1109                );
     1110                $query_params['author_email'] = array(
     1111                        'default'           => null,
     1112                        'description'       => __( 'Limit result set to that from a specific author email. Requires authorization.' ),
     1113                        'format'            => 'email',
     1114                        'sanitize_callback' => 'sanitize_email',
     1115                        'type'              => 'string',
     1116                );
     1117                $query_params['before'] = array(
     1118                        'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.' ),
     1119                        'type'              => 'string',
     1120                        'format'            => 'date-time',
     1121                        'validate_callback' => 'rest_validate_request_arg',
     1122                );
     1123                $query_params['exclude'] = array(
     1124                        'description'        => __( 'Ensure result set excludes specific ids.' ),
     1125                        'type'               => 'array',
     1126                        'default'            => array(),
     1127                        'sanitize_callback'  => 'wp_parse_id_list',
     1128                );
     1129                $query_params['include'] = array(
     1130                        'description'        => __( 'Limit result set to specific ids.' ),
     1131                        'type'               => 'array',
     1132                        'default'            => array(),
     1133                        'sanitize_callback'  => 'wp_parse_id_list',
     1134                );
     1135                $query_params['karma'] = array(
     1136                        'default'           => null,
     1137                        'description'       => __( 'Limit result set to that of a particular comment karma. Requires authorization.' ),
     1138                        'sanitize_callback' => 'absint',
     1139                        'type'              => 'integer',
     1140                        'validate_callback'  => 'rest_validate_request_arg',
     1141                );
     1142                $query_params['offset'] = array(
     1143                        'description'        => __( 'Offset the result set by a specific number of comments.' ),
     1144                        'type'               => 'integer',
     1145                        'sanitize_callback'  => 'absint',
     1146                        'validate_callback'  => 'rest_validate_request_arg',
     1147                );
     1148                $query_params['order']      = array(
     1149                        'description'           => __( 'Order sort attribute ascending or descending.' ),
     1150                        'type'                  => 'string',
     1151                        'sanitize_callback'     => 'sanitize_key',
     1152                        'validate_callback'     => 'rest_validate_request_arg',
     1153                        'default'               => 'desc',
     1154                        'enum'                  => array(
     1155                                'asc',
     1156                                'desc',
     1157                        ),
     1158                );
     1159                $query_params['orderby']    = array(
     1160                        'description'           => __( 'Sort collection by object attribute.' ),
     1161                        'type'                  => 'string',
     1162                        'sanitize_callback'     => 'sanitize_key',
     1163                        'validate_callback'     => 'rest_validate_request_arg',
     1164                        'default'               => 'date_gmt',
     1165                        'enum'                  => array(
     1166                                'date',
     1167                                'date_gmt',
     1168                                'id',
     1169                                'include',
     1170                                'post',
     1171                                'parent',
     1172                                'type',
     1173                        ),
     1174                );
     1175                $query_params['parent'] = array(
     1176                        'default'           => array(),
     1177                        'description'       => __( 'Limit result set to resources of specific parent ids.' ),
     1178                        'sanitize_callback' => 'wp_parse_id_list',
     1179                        'type'              => 'array',
     1180                );
     1181                $query_params['parent_exclude'] = array(
     1182                        'default'           => array(),
     1183                        'description'       => __( 'Ensure result set excludes specific parent ids.' ),
     1184                        'sanitize_callback' => 'wp_parse_id_list',
     1185                        'type'              => 'array',
     1186                );
     1187                $query_params['post']   = array(
     1188                        'default'           => array(),
     1189                        'description'       => __( 'Limit result set to resources assigned to specific post ids.' ),
     1190                        'type'              => 'array',
     1191                        'sanitize_callback' => 'wp_parse_id_list',
     1192                );
     1193                $query_params['status'] = array(
     1194                        'default'           => 'approve',
     1195                        'description'       => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ),
     1196                        'sanitize_callback' => 'sanitize_key',
     1197                        'type'              => 'string',
     1198                        'validate_callback' => 'rest_validate_request_arg',
     1199                );
     1200                $query_params['type'] = array(
     1201                        'default'           => 'comment',
     1202                        'description'       => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ),
     1203                        'sanitize_callback' => 'sanitize_key',
     1204                        'type'              => 'string',
     1205                        'validate_callback' => 'rest_validate_request_arg',
     1206                );
     1207                return $query_params;
     1208        }
     1209
     1210        /**
     1211         * Set the comment_status of a given comment object when creating or updating a comment.
     1212         *
     1213         * @param string|int $new_status
     1214         * @param object     $comment
     1215         * @return boolean   $changed
     1216         */
     1217        protected function handle_status_param( $new_status, $comment ) {
     1218                $old_status = wp_get_comment_status( $comment->comment_ID );
     1219
     1220                if ( $new_status === $old_status ) {
     1221                        return false;
     1222                }
     1223
     1224                switch ( $new_status ) {
     1225                        case 'approved' :
     1226                        case 'approve':
     1227                        case '1':
     1228                                $changed = wp_set_comment_status( $comment->comment_ID, 'approve' );
     1229                                break;
     1230                        case 'hold':
     1231                        case '0':
     1232                                $changed = wp_set_comment_status( $comment->comment_ID, 'hold' );
     1233                                break;
     1234                        case 'spam' :
     1235                                $changed = wp_spam_comment( $comment->comment_ID );
     1236                                break;
     1237                        case 'unspam' :
     1238                                $changed = wp_unspam_comment( $comment->comment_ID );
     1239                                break;
     1240                        case 'trash' :
     1241                                $changed = wp_trash_comment( $comment->comment_ID );
     1242                                break;
     1243                        case 'untrash' :
     1244                                $changed = wp_untrash_comment( $comment->comment_ID );
     1245                                break;
     1246                        default :
     1247                                $changed = false;
     1248                                break;
     1249                }
     1250
     1251                return $changed;
     1252        }
     1253
     1254        /**
     1255         * Check if we can read a post.
     1256         *
     1257         * Correctly handles posts with the inherit status.
     1258         *
     1259         * @param  WP_Post $post Post Object.
     1260         * @return boolean Can we read it?
     1261         */
     1262        protected function check_read_post_permission( $post ) {
     1263                $posts_controller = new WP_REST_Posts_Controller( $post->post_type );
     1264
     1265                return $posts_controller->check_read_permission( $post );
     1266        }
     1267
     1268        /**
     1269         * Check if we can read a comment.
     1270         *
     1271         * @param  object  $comment Comment object.
     1272         * @return boolean Can we read it?
     1273         */
     1274        protected function check_read_permission( $comment ) {
     1275                if ( ! empty( $comment->comment_post_ID ) ) {
     1276                        $post = get_post( $comment->comment_post_ID );
     1277                        if ( $post ) {
     1278                                if ( $this->check_read_post_permission( $post ) && 1 === (int) $comment->comment_approved ) {
     1279                                        return true;
     1280                                }
     1281                        }
     1282                }
     1283
     1284                if ( 0 === get_current_user_id() ) {
     1285                        return false;
     1286                }
     1287
     1288                if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
     1289                        return false;
     1290                }
     1291
     1292                if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
     1293                        return true;
     1294                }
     1295
     1296                return current_user_can( 'edit_comment', $comment->comment_ID );
     1297        }
     1298
     1299        /**
     1300         * Check if we can edit or delete a comment.
     1301         *
     1302         * @param  object  $comment Comment object.
     1303         * @return boolean Can we edit or delete it?
     1304         */
     1305        protected function check_edit_permission( $comment ) {
     1306                if ( 0 === (int) get_current_user_id() ) {
     1307                        return false;
     1308                }
     1309
     1310                if ( ! current_user_can( 'moderate_comments' ) ) {
     1311                        return false;
     1312                }
     1313
     1314                return current_user_can( 'edit_comment', $comment->comment_ID );
     1315        }
     1316}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3
     4abstract class WP_REST_Controller {
     5
     6        /**
     7         * The namespace of this controller's route.
     8         *
     9         * @var string
     10         */
     11        protected $namespace;
     12
     13        /**
     14         * The base of this controller's route.
     15         *
     16         * @var string
     17         */
     18        protected $rest_base;
     19
     20        /**
     21         * Register the routes for the objects of the controller.
     22         */
     23        public function register_routes() {
     24                _doing_it_wrong( 'WP_REST_Controller::register_routes', __( 'The register_routes() method must be overridden' ), 'WPAPI-2.0' );
     25        }
     26
     27        /**
     28         * Check if a given request has access to get items.
     29         *
     30         * @param WP_REST_Request $request Full data about the request.
     31         * @return WP_Error|boolean
     32         */
     33        public function get_items_permissions_check( $request ) {
     34                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     35        }
     36
     37        /**
     38         * Get a collection of items.
     39         *
     40         * @param WP_REST_Request $request Full data about the request.
     41         * @return WP_Error|WP_REST_Response
     42         */
     43        public function get_items( $request ) {
     44                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     45        }
     46
     47        /**
     48         * Check if a given request has access to get a specific item.
     49         *
     50         * @param WP_REST_Request $request Full data about the request.
     51         * @return WP_Error|boolean
     52         */
     53        public function get_item_permissions_check( $request ) {
     54                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     55        }
     56
     57        /**
     58         * Get one item from the collection.
     59         *
     60         * @param WP_REST_Request $request Full data about the request.
     61         * @return WP_Error|WP_REST_Response
     62         */
     63        public function get_item( $request ) {
     64                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     65        }
     66
     67        /**
     68         * Check if a given request has access to create items.
     69         *
     70         * @param WP_REST_Request $request Full data about the request.
     71         * @return WP_Error|boolean
     72         */
     73        public function create_item_permissions_check( $request ) {
     74                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     75        }
     76
     77        /**
     78         * Create one item from the collection.
     79         *
     80         * @param WP_REST_Request $request Full data about the request.
     81         * @return WP_Error|WP_REST_Response
     82         */
     83        public function create_item( $request ) {
     84                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     85        }
     86
     87        /**
     88         * Check if a given request has access to update a specific item.
     89         *
     90         * @param WP_REST_Request $request Full data about the request.
     91         * @return WP_Error|boolean
     92         */
     93        public function update_item_permissions_check( $request ) {
     94                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     95        }
     96
     97        /**
     98         * Update one item from the collection.
     99         *
     100         * @param WP_REST_Request $request Full data about the request.
     101         * @return WP_Error|WP_REST_Response
     102         */
     103        public function update_item( $request ) {
     104                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     105        }
     106
     107        /**
     108         * Check if a given request has access to delete a specific item.
     109         *
     110         * @param WP_REST_Request $request Full data about the request.
     111         * @return WP_Error|boolean
     112         */
     113        public function delete_item_permissions_check( $request ) {
     114                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     115        }
     116
     117        /**
     118         * Delete one item from the collection.
     119         *
     120         * @param WP_REST_Request $request Full data about the request.
     121         * @return WP_Error|WP_REST_Response
     122         */
     123        public function delete_item( $request ) {
     124                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     125        }
     126
     127        /**
     128         * Prepare the item for create or update operation.
     129         *
     130         * @param WP_REST_Request $request Request object.
     131         * @return WP_Error|object $prepared_item
     132         */
     133        protected function prepare_item_for_database( $request ) {
     134                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     135        }
     136
     137        /**
     138         * Prepare the item for the REST response.
     139         *
     140         * @param mixed $item WordPress representation of the item.
     141         * @param WP_REST_Request $request Request object.
     142         * @return WP_Error|WP_REST_Response $response
     143         */
     144        public function prepare_item_for_response( $item, $request ) {
     145                return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
     146        }
     147
     148        /**
     149         * Prepare a response for inserting into a collection.
     150         *
     151         * @param WP_REST_Response $response Response object.
     152         * @return array Response data, ready for insertion into collection data.
     153         */
     154        public function prepare_response_for_collection( $response ) {
     155                if ( ! ( $response instanceof WP_REST_Response ) ) {
     156                        return $response;
     157                }
     158
     159                $data = (array) $response->get_data();
     160                $server = rest_get_server();
     161
     162                if ( method_exists( $server, 'get_compact_response_links' ) ) {
     163                        $links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
     164                } else {
     165                        $links = call_user_func( array( $server, 'get_response_links' ), $response );
     166                }
     167
     168                if ( ! empty( $links ) ) {
     169                        $data['_links'] = $links;
     170                }
     171
     172                return $data;
     173        }
     174
     175        /**
     176         * Filter a response based on the context defined in the schema.
     177         *
     178         * @param array $data
     179         * @param string $context
     180         * @return array
     181         */
     182        public function filter_response_by_context( $data, $context ) {
     183
     184                $schema = $this->get_item_schema();
     185                foreach ( $data as $key => $value ) {
     186                        if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
     187                                continue;
     188                        }
     189
     190                        if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) {
     191                                unset( $data[ $key ] );
     192                                continue;
     193                        }
     194
     195                        if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
     196                                foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
     197                                        if ( empty( $details['context'] ) ) {
     198                                                continue;
     199                                        }
     200                                        if ( ! in_array( $context, $details['context'], true ) ) {
     201                                                if ( isset( $data[ $key ][ $attribute ] ) ) {
     202                                                        unset( $data[ $key ][ $attribute ] );
     203                                                }
     204                                        }
     205                                }
     206                        }
     207                }
     208
     209                return $data;
     210        }
     211
     212        /**
     213         * Get the item's schema, conforming to JSON Schema.
     214         *
     215         * @return array
     216         */
     217        public function get_item_schema() {
     218                return $this->add_additional_fields_schema( array() );
     219        }
     220
     221        /**
     222         * Get the item's schema for display / public consumption purposes.
     223         *
     224         * @return array
     225         */
     226        public function get_public_item_schema() {
     227
     228                $schema = $this->get_item_schema();
     229
     230                foreach ( $schema['properties'] as &$property ) {
     231                        unset( $property['arg_options'] );
     232                }
     233
     234                return $schema;
     235        }
     236
     237        /**
     238         * Get the query params for collections.
     239         *
     240         * @return array
     241         */
     242        public function get_collection_params() {
     243                return array(
     244                        'context'                => $this->get_context_param(),
     245                        'page'                   => array(
     246                                'description'        => __( 'Current page of the collection.' ),
     247                                'type'               => 'integer',
     248                                'default'            => 1,
     249                                'sanitize_callback'  => 'absint',
     250                                'validate_callback'  => 'rest_validate_request_arg',
     251                                'minimum'            => 1,
     252                        ),
     253                        'per_page'               => array(
     254                                'description'        => __( 'Maximum number of items to be returned in result set.' ),
     255                                'type'               => 'integer',
     256                                'default'            => 10,
     257                                'minimum'            => 1,
     258                                'maximum'            => 100,
     259                                'sanitize_callback'  => 'absint',
     260                                'validate_callback'  => 'rest_validate_request_arg',
     261                        ),
     262                        'search'                 => array(
     263                                'description'        => __( 'Limit results to those matching a string.' ),
     264                                'type'               => 'string',
     265                                'sanitize_callback'  => 'sanitize_text_field',
     266                                'validate_callback'  => 'rest_validate_request_arg',
     267                        ),
     268                );
     269        }
     270
     271        /**
     272         * Get the magical context param.
     273         *
     274         * Ensures consistent description between endpoints, and populates enum from schema.
     275         *
     276         * @param array     $args
     277         * @return array
     278         */
     279        public function get_context_param( $args = array() ) {
     280                $param_details = array(
     281                        'description'        => __( 'Scope under which the request is made; determines fields present in response.' ),
     282                        'type'               => 'string',
     283                        'sanitize_callback'  => 'sanitize_key',
     284                        'validate_callback'  => 'rest_validate_request_arg',
     285                );
     286                $schema = $this->get_item_schema();
     287                if ( empty( $schema['properties'] ) ) {
     288                        return array_merge( $param_details, $args );
     289                }
     290                $contexts = array();
     291                foreach ( $schema['properties'] as $attributes ) {
     292                        if ( ! empty( $attributes['context'] ) ) {
     293                                $contexts = array_merge( $contexts, $attributes['context'] );
     294                        }
     295                }
     296                if ( ! empty( $contexts ) ) {
     297                        $param_details['enum'] = array_unique( $contexts );
     298                        rsort( $param_details['enum'] );
     299                }
     300                return array_merge( $param_details, $args );
     301        }
     302
     303        /**
     304         * Add the values from additional fields to a data object.
     305         *
     306         * @param array  $object
     307         * @param WP_REST_Request $request
     308         * @return array modified object with additional fields.
     309         */
     310        protected function add_additional_fields_to_object( $object, $request ) {
     311
     312                $additional_fields = $this->get_additional_fields();
     313
     314                foreach ( $additional_fields as $field_name => $field_options ) {
     315
     316                        if ( ! $field_options['get_callback'] ) {
     317                                continue;
     318                        }
     319
     320                        $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
     321                }
     322
     323                return $object;
     324        }
     325
     326        /**
     327         * Update the values of additional fields added to a data object.
     328         *
     329         * @param array  $object
     330         * @param WP_REST_Request $request
     331         * @return bool|WP_Error True on success, WP_Error object if a field cannot be updated.
     332         */
     333        protected function update_additional_fields_for_object( $object, $request ) {
     334                $additional_fields = $this->get_additional_fields();
     335
     336                foreach ( $additional_fields as $field_name => $field_options ) {
     337                        if ( ! $field_options['update_callback'] ) {
     338                                continue;
     339                        }
     340
     341                        // Don't run the update callbacks if the data wasn't passed in the request.
     342                        if ( ! isset( $request[ $field_name ] ) ) {
     343                                continue;
     344                        }
     345
     346                        $result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
     347                        if ( is_wp_error( $result ) ) {
     348                                return $result;
     349                        }
     350                }
     351
     352                return true;
     353        }
     354
     355        /**
     356         * Add the schema from additional fields to an schema array.
     357         *
     358         * The type of object is inferred from the passed schema.
     359         *
     360         * @param array $schema Schema array.
     361         * @return array Modified Schema array.
     362         */
     363        protected function add_additional_fields_schema( $schema ) {
     364                if ( empty( $schema['title'] ) ) {
     365                        return $schema;
     366                }
     367
     368                /**
     369                 * Can't use $this->get_object_type otherwise we cause an inf loop.
     370                 */
     371                $object_type = $schema['title'];
     372
     373                $additional_fields = $this->get_additional_fields( $object_type );
     374
     375                foreach ( $additional_fields as $field_name => $field_options ) {
     376                        if ( ! $field_options['schema'] ) {
     377                                continue;
     378                        }
     379
     380                        $schema['properties'][ $field_name ] = $field_options['schema'];
     381                }
     382
     383                return $schema;
     384        }
     385
     386        /**
     387         * Get all the registered additional fields for a given object-type.
     388         *
     389         * @param  string $object_type
     390         * @return array
     391         */
     392        protected function get_additional_fields( $object_type = null ) {
     393
     394                if ( ! $object_type ) {
     395                        $object_type = $this->get_object_type();
     396                }
     397
     398                if ( ! $object_type ) {
     399                        return array();
     400                }
     401
     402                global $wp_rest_additional_fields;
     403
     404                if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
     405                        return array();
     406                }
     407
     408                return $wp_rest_additional_fields[ $object_type ];
     409        }
     410
     411        /**
     412         * Get the object type this controller is responsible for managing.
     413         *
     414         * @return string
     415         */
     416        protected function get_object_type() {
     417                $schema = $this->get_item_schema();
     418
     419                if ( ! $schema || ! isset( $schema['title'] ) ) {
     420                        return null;
     421                }
     422
     423                return $schema['title'];
     424        }
     425
     426        /**
     427         * Get an array of endpoint arguments from the item schema for the controller.
     428         *
     429         * @param string $method HTTP method of the request. The arguments
     430         *                       for `CREATABLE` requests are checked for required
     431         *                       values and may fall-back to a given default, this
     432         *                       is not done on `EDITABLE` requests. Default is
     433         *                       WP_REST_Server::CREATABLE.
     434         * @return array $endpoint_args
     435         */
     436        public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
     437
     438                $schema                = $this->get_item_schema();
     439                $schema_properties     = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
     440                $endpoint_args = array();
     441
     442                foreach ( $schema_properties as $field_id => $params ) {
     443
     444                        // Arguments specified as `readonly` are not allowed to be set.
     445                        if ( ! empty( $params['readonly'] ) ) {
     446                                continue;
     447                        }
     448
     449                        $endpoint_args[ $field_id ] = array(
     450                                'validate_callback' => 'rest_validate_request_arg',
     451                                'sanitize_callback' => 'rest_sanitize_request_arg',
     452                        );
     453
     454                        if ( isset( $params['description'] ) ) {
     455                                $endpoint_args[ $field_id ]['description'] = $params['description'];
     456                        }
     457
     458                        if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
     459                                $endpoint_args[ $field_id ]['default'] = $params['default'];
     460                        }
     461
     462                        if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
     463                                $endpoint_args[ $field_id ]['required'] = true;
     464                        }
     465
     466                        foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) {
     467                                if ( isset( $params[ $schema_prop ] ) ) {
     468                                        $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
     469                                }
     470                        }
     471
     472                        // Merge in any options provided by the schema property.
     473                        if ( isset( $params['arg_options'] ) ) {
     474
     475                                // Only use required / default from arg_options on CREATABLE endpoints.
     476                                if ( WP_REST_Server::CREATABLE !== $method ) {
     477                                        $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) );
     478                                }
     479
     480                                $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
     481                        }
     482                }
     483
     484                return $endpoint_args;
     485        }
     486
     487        /**
     488         * Retrieves post data given a post ID or post object.
     489         *
     490         * This is a subset of the functionality of the `get_post()` function, with
     491         * the additional functionality of having `the_post` action done on the
     492         * resultant post object. This is done so that plugins may manipulate the
     493         * post that is used in the REST API.
     494         *
     495         * @see get_post()
     496         * @global WP_Query $wp_query
     497         *
     498         * @param int|WP_Post $post Post ID or post object. Defaults to global $post.
     499         * @return WP_Post|null A `WP_Post` object when successful.
     500         */
     501        public function get_post( $post ) {
     502                $post_obj = get_post( $post );
     503
     504                /**
     505                 * Filter the post.
     506                 *
     507                 * Allows plugins to filter the post object as returned by `\WP_REST_Controller::get_post()`.
     508                 *
     509                 * @param WP_Post|null $post_obj  The post object as returned by `get_post()`.
     510                 * @param int|WP_Post  $post      The original value used to obtain the post object.
     511                 */
     512                $post = apply_filters( 'rest_the_post', $post_obj, $post );
     513
     514                return $post;
     515        }
     516
     517        /**
     518         * Sanitize the slug value.
     519         *
     520         * @internal We can't use {@see sanitize_title} directly, as the second
     521         * parameter is the fallback title, which would end up being set to the
     522         * request object.
     523         * @see https://github.com/WP-API/WP-API/issues/1585
     524         *
     525         * @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
     526         *
     527         * @param string $slug Slug value passed in request.
     528         * @return string Sanitized value for the slug.
     529         */
     530        public function sanitize_slug( $slug ) {
     531                return sanitize_title( $slug );
     532        }
     533}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
     4
     5        public function __construct() {
     6                $this->namespace = 'wp/v2';
     7                $this->rest_base = 'statuses';
     8        }
     9
     10        /**
     11         * Register the routes for the objects of the controller.
     12         */
     13        public function register_routes() {
     14
     15                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     16                        array(
     17                                'methods'         => WP_REST_Server::READABLE,
     18                                'callback'        => array( $this, 'get_items' ),
     19                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     20                                'args'            => $this->get_collection_params(),
     21                        ),
     22                        'schema' => array( $this, 'get_public_item_schema' ),
     23                ) );
     24
     25                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<status>[\w-]+)', array(
     26                        array(
     27                                'methods'         => WP_REST_Server::READABLE,
     28                                'callback'        => array( $this, 'get_item' ),
     29                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     30                                'args'            => array(
     31                                        'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
     32                                ),
     33                        ),
     34                        'schema' => array( $this, 'get_public_item_schema' ),
     35                ) );
     36        }
     37
     38        /**
     39         * Check whether a given request has permission to read post statuses.
     40         *
     41         * @param  WP_REST_Request $request Full details about the request.
     42         * @return WP_Error|boolean
     43         */
     44        public function get_items_permissions_check( $request ) {
     45                if ( 'edit' === $request['context'] ) {
     46                        $types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
     47                        foreach ( $types as $type ) {
     48                                if ( current_user_can( $type->cap->edit_posts ) ) {
     49                                        return true;
     50                                }
     51                        }
     52                        return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     53                }
     54                return true;
     55        }
     56
     57        /**
     58         * Get all post statuses, depending on user context
     59         *
     60         * @param WP_REST_Request $request
     61         * @return array|WP_Error
     62         */
     63        public function get_items( $request ) {
     64                $data = array();
     65                $statuses = get_post_stati( array( 'internal' => false ), 'object' );
     66                $statuses['trash'] = get_post_status_object( 'trash' );
     67                foreach ( $statuses as $slug => $obj ) {
     68                        $ret = $this->check_read_permission( $obj );
     69                        if ( ! $ret ) {
     70                                continue;
     71                        }
     72                        $status = $this->prepare_item_for_response( $obj, $request );
     73                        $data[ $obj->name ] = $this->prepare_response_for_collection( $status );
     74                }
     75                return rest_ensure_response( $data );
     76        }
     77
     78        /**
     79         * Check if a given request has access to read a post status.
     80         *
     81         * @param  WP_REST_Request $request Full details about the request.
     82         * @return WP_Error|boolean
     83         */
     84        public function get_item_permissions_check( $request ) {
     85                $status = get_post_status_object( $request['status'] );
     86                if ( empty( $status ) ) {
     87                        return new WP_Error( 'rest_status_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
     88                }
     89                $check = $this->check_read_permission( $status );
     90                if ( ! $check ) {
     91                        return new WP_Error( 'rest_cannot_read_status', __( 'Cannot view resource.' ), array( 'status' => rest_authorization_required_code() ) );
     92                }
     93                return true;
     94        }
     95
     96        /**
     97         * Check whether a given post status should be visible
     98         *
     99         * @param object $status
     100         * @return boolean
     101         */
     102        protected function check_read_permission( $status ) {
     103                if ( true === $status->public ) {
     104                        return true;
     105                }
     106                if ( false === $status->internal || 'trash' === $status->name ) {
     107                        $types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
     108                        foreach ( $types as $type ) {
     109                                if ( current_user_can( $type->cap->edit_posts ) ) {
     110                                        return true;
     111                                }
     112                        }
     113                }
     114                return false;
     115        }
     116
     117        /**
     118         * Get a specific post status
     119         *
     120         * @param WP_REST_Request $request
     121         * @return array|WP_Error
     122         */
     123        public function get_item( $request ) {
     124                $obj = get_post_status_object( $request['status'] );
     125                if ( empty( $obj ) ) {
     126                        return new WP_Error( 'rest_status_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
     127                }
     128                $data = $this->prepare_item_for_response( $obj, $request );
     129                return rest_ensure_response( $data );
     130        }
     131
     132        /**
     133         * Prepare a post status object for serialization
     134         *
     135         * @param stdClass $status Post status data
     136         * @param WP_REST_Request $request
     137         * @return WP_REST_Response Post status data
     138         */
     139        public function prepare_item_for_response( $status, $request ) {
     140
     141                $data = array(
     142                        'name'         => $status->label,
     143                        'private'      => (bool) $status->private,
     144                        'protected'    => (bool) $status->protected,
     145                        'public'       => (bool) $status->public,
     146                        'queryable'    => (bool) $status->publicly_queryable,
     147                        'show_in_list' => (bool) $status->show_in_admin_all_list,
     148                        'slug'         => $status->name,
     149                );
     150
     151                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     152                $data = $this->add_additional_fields_to_object( $data, $request );
     153                $data = $this->filter_response_by_context( $data, $context );
     154
     155                $response = rest_ensure_response( $data );
     156
     157                if ( 'publish' === $status->name ) {
     158                        $response->add_link( 'archives', rest_url( 'wp/v2/posts' ) );
     159                } else {
     160                        $response->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( 'wp/v2/posts' ) ) );
     161                }
     162
     163                /**
     164                 * Filter a status returned from the API.
     165                 *
     166                 * Allows modification of the status data right before it is returned.
     167                 *
     168                 * @param WP_REST_Response  $response The response object.
     169                 * @param object            $status   The original status object.
     170                 * @param WP_REST_Request   $request  Request used to generate the response.
     171                 */
     172                return apply_filters( 'rest_prepare_status', $response, $status, $request );
     173        }
     174
     175        /**
     176         * Get the Post status' schema, conforming to JSON Schema
     177         *
     178         * @return array
     179         */
     180        public function get_item_schema() {
     181                $schema = array(
     182                        '$schema'              => 'http://json-schema.org/draft-04/schema#',
     183                        'title'                => 'status',
     184                        'type'                 => 'object',
     185                        'properties'           => array(
     186                                'name'             => array(
     187                                        'description'  => __( 'The title for the resource.' ),
     188                                        'type'         => 'string',
     189                                        'context'      => array( 'embed', 'view', 'edit' ),
     190                                        'readonly'     => true,
     191                                ),
     192                                'private'          => array(
     193                                        'description'  => __( 'Whether posts with this resource should be private.' ),
     194                                        'type'         => 'boolean',
     195                                        'context'      => array( 'edit' ),
     196                                        'readonly'     => true,
     197                                ),
     198                                'protected'        => array(
     199                                        'description'  => __( 'Whether posts with this resource should be protected.' ),
     200                                        'type'         => 'boolean',
     201                                        'context'      => array( 'edit' ),
     202                                        'readonly'     => true,
     203                                ),
     204                                'public'           => array(
     205                                        'description'  => __( 'Whether posts of this resource should be shown in the front end of the site.' ),
     206                                        'type'         => 'boolean',
     207                                        'context'      => array( 'view', 'edit' ),
     208                                        'readonly'     => true,
     209                                ),
     210                                'queryable'        => array(
     211                                        'description'  => __( 'Whether posts with this resource should be publicly-queryable.' ),
     212                                        'type'         => 'boolean',
     213                                        'context'      => array( 'view', 'edit' ),
     214                                        'readonly'     => true,
     215                                ),
     216                                'show_in_list'     => array(
     217                                        'description'  => __( 'Whether to include posts in the edit listing for their post type.' ),
     218                                        'type'         => 'boolean',
     219                                        'context'      => array( 'edit' ),
     220                                        'readonly'     => true,
     221                                ),
     222                                'slug'             => array(
     223                                        'description'  => __( 'An alphanumeric identifier for the resource.' ),
     224                                        'type'         => 'string',
     225                                        'context'      => array( 'embed', 'view', 'edit' ),
     226                                        'readonly'     => true,
     227                                ),
     228                        ),
     229                );
     230                return $this->add_additional_fields_schema( $schema );
     231        }
     232
     233        /**
     234         * Get the query params for collections
     235         *
     236         * @return array
     237         */
     238        public function get_collection_params() {
     239                return array(
     240                        'context'        => $this->get_context_param( array( 'default' => 'view' ) ),
     241                );
     242        }
     243
     244}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3class WP_REST_Post_Types_Controller extends WP_REST_Controller {
     4
     5        public function __construct() {
     6                $this->namespace = 'wp/v2';
     7                $this->rest_base = 'types';
     8        }
     9
     10        /**
     11         * Register the routes for the objects of the controller.
     12         */
     13        public function register_routes() {
     14
     15                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     16                        array(
     17                                'methods'         => WP_REST_Server::READABLE,
     18                                'callback'        => array( $this, 'get_items' ),
     19                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     20                                'args'            => $this->get_collection_params(),
     21                        ),
     22                        'schema'          => array( $this, 'get_public_item_schema' ),
     23                ) );
     24
     25                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<type>[\w-]+)', array(
     26                        array(
     27                                'methods'         => WP_REST_Server::READABLE,
     28                                'callback'        => array( $this, 'get_item' ),
     29                                'args'            => array(
     30                                        'context'     => $this->get_context_param( array( 'default' => 'view' ) ),
     31                                ),
     32                        ),
     33                        'schema'          => array( $this, 'get_public_item_schema' ),
     34                ) );
     35        }
     36
     37        /**
     38         * Check whether a given request has permission to read types.
     39         *
     40         * @param  WP_REST_Request $request Full details about the request.
     41         * @return WP_Error|boolean
     42         */
     43        public function get_items_permissions_check( $request ) {
     44                if ( 'edit' === $request['context'] ) {
     45                        foreach ( get_post_types( array(), 'object' ) as $post_type ) {
     46                                if ( ! empty( $post_type->show_in_rest ) && current_user_can( $post_type->cap->edit_posts ) ) {
     47                                        return true;
     48                                }
     49                        }
     50                        return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     51                }
     52                return true;
     53        }
     54
     55        /**
     56         * Get all public post types
     57         *
     58         * @param WP_REST_Request $request
     59         * @return array|WP_Error
     60         */
     61        public function get_items( $request ) {
     62                $data = array();
     63                foreach ( get_post_types( array(), 'object' ) as $obj ) {
     64                        if ( empty( $obj->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) ) {
     65                                continue;
     66                        }
     67                        $post_type = $this->prepare_item_for_response( $obj, $request );
     68                        $data[ $obj->name ] = $this->prepare_response_for_collection( $post_type );
     69                }
     70                return rest_ensure_response( $data );
     71        }
     72
     73        /**
     74         * Get a specific post type
     75         *
     76         * @param WP_REST_Request $request
     77         * @return array|WP_Error
     78         */
     79        public function get_item( $request ) {
     80                $obj = get_post_type_object( $request['type'] );
     81                if ( empty( $obj ) ) {
     82                        return new WP_Error( 'rest_type_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
     83                }
     84                if ( empty( $obj->show_in_rest ) ) {
     85                        return new WP_Error( 'rest_cannot_read_type', __( 'Cannot view resource.' ), array( 'status' => rest_authorization_required_code() ) );
     86                }
     87                if ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) {
     88                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this resource.' ), array( 'status' => rest_authorization_required_code() ) );
     89                }
     90                $data = $this->prepare_item_for_response( $obj, $request );
     91                return rest_ensure_response( $data );
     92        }
     93
     94        /**
     95         * Prepare a post type object for serialization
     96         *
     97         * @param stdClass $post_type Post type data
     98         * @param WP_REST_Request $request
     99         * @return WP_REST_Response $response
     100         */
     101        public function prepare_item_for_response( $post_type, $request ) {
     102                $data = array(
     103                        'capabilities' => $post_type->cap,
     104                        'description'  => $post_type->description,
     105                        'hierarchical' => $post_type->hierarchical,
     106                        'labels'       => $post_type->labels,
     107                        'name'         => $post_type->label,
     108                        'slug'         => $post_type->name,
     109                );
     110                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     111                $data = $this->add_additional_fields_to_object( $data, $request );
     112                $data = $this->filter_response_by_context( $data, $context );
     113
     114                // Wrap the data in a response object.
     115                $response = rest_ensure_response( $data );
     116
     117                $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
     118                $response->add_links( array(
     119                        'collection'              => array(
     120                                'href'                => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
     121                        ),
     122                        'https://api.w.org/items' => array(
     123                                'href'                => rest_url( sprintf( 'wp/v2/%s', $base ) ),
     124                        ),
     125                ) );
     126
     127                /**
     128                 * Filter a post type returned from the API.
     129                 *
     130                 * Allows modification of the post type data right before it is returned.
     131                 *
     132                 * @param WP_REST_Response  $response   The response object.
     133                 * @param object            $item       The original post type object.
     134                 * @param WP_REST_Request   $request    Request used to generate the response.
     135                 */
     136                return apply_filters( 'rest_prepare_post_type', $response, $post_type, $request );
     137        }
     138
     139        /**
     140         * Get the Post type's schema, conforming to JSON Schema
     141         *
     142         * @return array
     143         */
     144        public function get_item_schema() {
     145                $schema = array(
     146                        '$schema'              => 'http://json-schema.org/draft-04/schema#',
     147                        'title'                => 'type',
     148                        'type'                 => 'object',
     149                        'properties'           => array(
     150                                'capabilities'     => array(
     151                                        'description'  => __( 'All capabilities used by the resource.' ),
     152                                        'type'         => 'array',
     153                                        'context'      => array( 'edit' ),
     154                                        'readonly'     => true,
     155                                ),
     156                                'description'      => array(
     157                                        'description'  => __( 'A human-readable description of the resource.' ),
     158                                        'type'         => 'string',
     159                                        'context'      => array( 'view', 'edit' ),
     160                                        'readonly'     => true,
     161                                ),
     162                                'hierarchical'     => array(
     163                                        'description'  => __( 'Whether or not the resource should have children.' ),
     164                                        'type'         => 'boolean',
     165                                        'context'      => array( 'view', 'edit' ),
     166                                        'readonly'     => true,
     167                                ),
     168                                'labels'           => array(
     169                                        'description'  => __( 'Human-readable labels for the resource for various contexts.' ),
     170                                        'type'         => 'object',
     171                                        'context'      => array( 'edit' ),
     172                                        'readonly'     => true,
     173                                ),
     174                                'name'             => array(
     175                                        'description'  => __( 'The title for the resource.' ),
     176                                        'type'         => 'string',
     177                                        'context'      => array( 'view', 'edit', 'embed' ),
     178                                        'readonly'     => true,
     179                                ),
     180                                'slug'             => array(
     181                                        'description'  => __( 'An alphanumeric identifier for the resource.' ),
     182                                        'type'         => 'string',
     183                                        'context'      => array( 'view', 'edit', 'embed' ),
     184                                        'readonly'     => true,
     185                                ),
     186                        ),
     187                );
     188                return $this->add_additional_fields_schema( $schema );
     189        }
     190
     191        /**
     192         * Get the query params for collections
     193         *
     194         * @return array
     195         */
     196        public function get_collection_params() {
     197                return array(
     198                        'context'      => $this->get_context_param( array( 'default' => 'view' ) ),
     199                );
     200        }
     201
     202}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3class WP_REST_Posts_Controller extends WP_REST_Controller {
     4
     5        /**
     6         * Post type.
     7         *
     8         * @access protected
     9         * @var string
     10         */
     11        protected $post_type;
     12
     13        /**
     14         * Instance of a post meta fields object.
     15         *
     16         * @access protected
     17         * @var WP_REST_Post_Meta_Fields
     18         */
     19        protected $meta;
     20
     21        /**
     22         * Constructor.
     23         *
     24         * @param string $post_type Post type.
     25         */
     26        public function __construct( $post_type ) {
     27                $this->post_type = $post_type;
     28                $this->namespace = 'wp/v2';
     29                $obj = get_post_type_object( $post_type );
     30                $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
     31
     32                $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
     33        }
     34
     35        /**
     36         * Register the routes for the objects of the controller.
     37         */
     38        public function register_routes() {
     39
     40                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     41                        array(
     42                                'methods'         => WP_REST_Server::READABLE,
     43                                'callback'        => array( $this, 'get_items' ),
     44                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     45                                'args'            => $this->get_collection_params(),
     46                        ),
     47                        array(
     48                                'methods'         => WP_REST_Server::CREATABLE,
     49                                'callback'        => array( $this, 'create_item' ),
     50                                'permission_callback' => array( $this, 'create_item_permissions_check' ),
     51                                'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
     52                        ),
     53                        'schema' => array( $this, 'get_public_item_schema' ),
     54                ) );
     55                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
     56                        array(
     57                                'methods'         => WP_REST_Server::READABLE,
     58                                'callback'        => array( $this, 'get_item' ),
     59                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     60                                'args'            => array(
     61                                        'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
     62                                        'password' => array(
     63                                                'description' => __( 'The password for the post if it is password protected.' ),
     64                                        ),
     65                                ),
     66                        ),
     67                        array(
     68                                'methods'         => WP_REST_Server::EDITABLE,
     69                                'callback'        => array( $this, 'update_item' ),
     70                                'permission_callback' => array( $this, 'update_item_permissions_check' ),
     71                                'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
     72                        ),
     73                        array(
     74                                'methods'  => WP_REST_Server::DELETABLE,
     75                                'callback' => array( $this, 'delete_item' ),
     76                                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
     77                                'args'     => array(
     78                                        'force'    => array(
     79                                                'default'      => false,
     80                                                'description'  => __( 'Whether to bypass trash and force deletion.' ),
     81                                        ),
     82                                ),
     83                        ),
     84                        'schema' => array( $this, 'get_public_item_schema' ),
     85                ) );
     86        }
     87
     88        /**
     89         * Check if a given request has access to read /posts.
     90         *
     91         * @param  WP_REST_Request $request Full details about the request.
     92         * @return WP_Error|boolean
     93         */
     94        public function get_items_permissions_check( $request ) {
     95
     96                $post_type = get_post_type_object( $this->post_type );
     97
     98                if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
     99                        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() ) );
     100                }
     101
     102                return true;
     103        }
     104
     105        /**
     106         * Get a collection of posts.
     107         *
     108         * @param WP_REST_Request $request Full details about the request.
     109         * @return WP_Error|WP_REST_Response
     110         */
     111        public function get_items( $request ) {
     112
     113                // Make sure a search string is set in case the orderby is set to 'relevance'.
     114                if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) && empty( $request['filter']['s'] ) ) {
     115                        return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.' ), array( 'status' => 400 ) );
     116                }
     117
     118                // Retrieve the list of registered collection query parameters.
     119                $registered = $this->get_collection_params();
     120                $args = array();
     121
     122                // This array defines mappings between public API query parameters whose
     123                // values are accepted as-passed, and their internal WP_Query parameter
     124                // name equivalents (some are the same). Only values which are also
     125                // present in $registered will be set.
     126                $parameter_mappings = array(
     127                        'author'         => 'author__in',
     128                        'author_exclude' => 'author__not_in',
     129                        'exclude'        => 'post__not_in',
     130                        'include'        => 'post__in',
     131                        'menu_order'     => 'menu_order',
     132                        'offset'         => 'offset',
     133                        'order'          => 'order',
     134                        'orderby'        => 'orderby',
     135                        'page'           => 'paged',
     136                        'parent'         => 'post_parent__in',
     137                        'parent_exclude' => 'post_parent__not_in',
     138                        'search'         => 's',
     139                        'slug'           => 'name',
     140                        'status'         => 'post_status',
     141                );
     142
     143                // For each known parameter which is both registered and present in the request,
     144                // set the parameter's value on the query $args.
     145                foreach ( $parameter_mappings as $api_param => $wp_param ) {
     146                        if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
     147                                $args[ $wp_param ] = $request[ $api_param ];
     148                        }
     149                }
     150
     151                // Check for & assign any parameters which require special handling or setting.
     152
     153                $args['date_query'] = array();
     154                // Set before into date query. Date query must be specified as an array of an array.
     155                if ( isset( $registered['before'], $request['before'] ) ) {
     156                        $args['date_query'][0]['before'] = $request['before'];
     157                }
     158
     159                // Set after into date query. Date query must be specified as an array of an array.
     160                if ( isset( $registered['after'], $request['after'] ) ) {
     161                        $args['date_query'][0]['after'] = $request['after'];
     162                }
     163
     164                if ( isset( $registered['filter'] ) && is_array( $request['filter'] ) ) {
     165                        $args = array_merge( $args, $request['filter'] );
     166                        unset( $args['filter'] );
     167                }
     168
     169                // Ensure our per_page parameter overrides any provided posts_per_page filter.
     170                if ( isset( $registered['per_page'] ) ) {
     171                        $args['posts_per_page'] = $request['per_page'];
     172                }
     173
     174                if ( isset( $registered['sticky'], $request['sticky'] ) ) {
     175                        $sticky_posts = get_option( 'sticky_posts', array() );
     176                        if ( $sticky_posts && $request['sticky'] ) {
     177                                // As post__in will be used to only get sticky posts,
     178                                // we have to support the case where post__in was already
     179                                // specified.
     180                                $args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
     181
     182                                // If we intersected, but there are no post ids in common,
     183                                // WP_Query won't return "no posts" for `post__in = array()`
     184                                // so we have to fake it a bit.
     185                                if ( ! $args['post__in'] ) {
     186                                        $args['post__in'] = array( -1 );
     187                                }
     188                        } elseif ( $sticky_posts ) {
     189                                // As post___not_in will be used to only get posts that
     190                                // are not sticky, we have to support the case where post__not_in
     191                                // was already specified.
     192                                $args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
     193                        }
     194                }
     195
     196                // Force the post_type argument, since it's not a user input variable.
     197                $args['post_type'] = $this->post_type;
     198
     199                /**
     200                 * Filter the query arguments for a request.
     201                 *
     202                 * Enables adding extra arguments or setting defaults for a post
     203                 * collection request.
     204                 *
     205                 * @see https://developer.wordpress.org/reference/classes/wp_query/
     206                 *
     207                 * @param array           $args    Key value array of query var to query value.
     208                 * @param WP_REST_Request $request The request used.
     209                 */
     210                $args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
     211                $query_args = $this->prepare_items_query( $args, $request );
     212
     213                $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     214                foreach ( $taxonomies as $taxonomy ) {
     215                        $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     216                        $tax_exclude = $base . '_exclude';
     217
     218                        if ( ! empty( $request[ $base ] ) ) {
     219                                $query_args['tax_query'][] = array(
     220                                        'taxonomy'         => $taxonomy->name,
     221                                        'field'            => 'term_id',
     222                                        'terms'            => $request[ $base ],
     223                                        'include_children' => false,
     224                                );
     225                        }
     226
     227                        if ( ! empty( $request[ $tax_exclude ] ) ) {
     228                                $query_args['tax_query'][] = array(
     229                                        'taxonomy'         => $taxonomy->name,
     230                                        'field'            => 'term_id',
     231                                        'terms'            => $request[ $tax_exclude ],
     232                                        'include_children' => false,
     233                                        'operator'         => 'NOT IN',
     234                                );
     235                        }
     236                }
     237
     238                $posts_query = new WP_Query();
     239                $query_result = $posts_query->query( $query_args );
     240
     241                // Allow access to all password protected posts if the context is edit.
     242                if ( 'edit' === $request['context'] ) {
     243                        add_filter( 'post_password_required', '__return_false' );
     244                }
     245
     246                $posts = array();
     247                foreach ( $query_result as $post ) {
     248                        if ( ! $this->check_read_permission( $post ) ) {
     249                                continue;
     250                        }
     251
     252                        $data = $this->prepare_item_for_response( $post, $request );
     253                        $posts[] = $this->prepare_response_for_collection( $data );
     254                }
     255
     256                // Reset filter.
     257                if ( 'edit' === $request['context'] ) {
     258                        remove_filter( 'post_password_required', '__return_false' );
     259                }
     260
     261                $page = (int) $query_args['paged'];
     262                $total_posts = $posts_query->found_posts;
     263
     264                if ( $total_posts < 1 ) {
     265                        // Out-of-bounds, run the query again without LIMIT for total count.
     266                        unset( $query_args['paged'] );
     267                        $count_query = new WP_Query();
     268                        $count_query->query( $query_args );
     269                        $total_posts = $count_query->found_posts;
     270                }
     271
     272                $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
     273
     274                $response = rest_ensure_response( $posts );
     275                $response->header( 'X-WP-Total', (int) $total_posts );
     276                $response->header( 'X-WP-TotalPages', (int) $max_pages );
     277
     278                $request_params = $request->get_query_params();
     279                if ( ! empty( $request_params['filter'] ) ) {
     280                        // Normalize the pagination params.
     281                        unset( $request_params['filter']['posts_per_page'], $request_params['filter']['paged'] );
     282                }
     283                $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
     284
     285                if ( $page > 1 ) {
     286                        $prev_page = $page - 1;
     287                        if ( $prev_page > $max_pages ) {
     288                                $prev_page = $max_pages;
     289                        }
     290                        $prev_link = add_query_arg( 'page', $prev_page, $base );
     291                        $response->link_header( 'prev', $prev_link );
     292                }
     293                if ( $max_pages > $page ) {
     294                        $next_page = $page + 1;
     295                        $next_link = add_query_arg( 'page', $next_page, $base );
     296                        $response->link_header( 'next', $next_link );
     297                }
     298
     299                return $response;
     300        }
     301
     302        /**
     303         * Check if a given request has access to read a post.
     304         *
     305         * @param  WP_REST_Request $request Full details about the request.
     306         * @return WP_Error|boolean
     307         */
     308        public function get_item_permissions_check( $request ) {
     309
     310                $post = $this->get_post( (int) $request['id'] );
     311
     312                if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
     313                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
     314                }
     315
     316                if ( $post && ! empty( $request['password'] ) ) {
     317                        // Check post password, and return error if invalid.
     318                        if ( ! hash_equals( $post->post_password, $request['password'] ) ) {
     319                                return new WP_Error( 'rest_post_incorrect_password', __( 'Incorrect post password.' ), array( 'status' => 403 ) );
     320                        }
     321                }
     322
     323                // Allow access to all password protected posts if the context is edit.
     324                if ( 'edit' === $request['context'] ) {
     325                        add_filter( 'post_password_required', '__return_false' );
     326                }
     327
     328                if ( $post ) {
     329                        return $this->check_read_permission( $post );
     330                }
     331
     332                return true;
     333        }
     334
     335        /**
     336         * Can the user access password-protected content?
     337         *
     338         * This method determines whether we need to override the regular password
     339         * check in core with a filter.
     340         *
     341         * @param WP_Post         $post    Post to check against.
     342         * @param WP_REST_Request $request Request data to check.
     343         * @return bool True if the user can access password-protected content, false otherwise.
     344         */
     345        protected function can_access_password_content( $post, $request ) {
     346                if ( empty( $post->post_password ) ) {
     347                        // No filter required.
     348                        return false;
     349                }
     350
     351                // Edit context always gets access to password-protected posts.
     352                if ( 'edit' === $request['context'] ) {
     353                        return true;
     354                }
     355
     356                // No password, no auth.
     357                if ( empty( $request['password'] ) ) {
     358                        return false;
     359                }
     360
     361                // Double-check the request password.
     362                return hash_equals( $post->post_password, $request['password'] );
     363        }
     364
     365        /**
     366         * Get a single post.
     367         *
     368         * @param WP_REST_Request $request Full details about the request.
     369         * @return WP_Error|WP_REST_Response
     370         */
     371        public function get_item( $request ) {
     372                $id = (int) $request['id'];
     373                $post = $this->get_post( $id );
     374
     375                if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
     376                        return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
     377                }
     378
     379                $data = $this->prepare_item_for_response( $post, $request );
     380                $response = rest_ensure_response( $data );
     381
     382                if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
     383                        $response->link_header( 'alternate',  get_permalink( $id ), array( 'type' => 'text/html' ) );
     384                }
     385
     386                return $response;
     387        }
     388
     389        /**
     390         * Check if a given request has access to create a post.
     391         *
     392         * @param  WP_REST_Request $request Full details about the request.
     393         * @return WP_Error|boolean
     394         */
     395        public function create_item_permissions_check( $request ) {
     396
     397                $post_type = get_post_type_object( $this->post_type );
     398
     399                if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
     400                        return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
     401                }
     402
     403                if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
     404                        return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
     405                }
     406
     407                if ( ! current_user_can( $post_type->cap->create_posts ) ) {
     408                        return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create new posts.' ), array( 'status' => rest_authorization_required_code() ) );
     409                }
     410                return true;
     411        }
     412
     413        /**
     414         * Create a single post.
     415         *
     416         * @param WP_REST_Request $request Full details about the request.
     417         * @return WP_Error|WP_REST_Response
     418         */
     419        public function create_item( $request ) {
     420                if ( ! empty( $request['id'] ) ) {
     421                        return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
     422                }
     423
     424                $post = $this->prepare_item_for_database( $request );
     425                if ( is_wp_error( $post ) ) {
     426                        return $post;
     427                }
     428
     429                $post->post_type = $this->post_type;
     430                $post_id = wp_insert_post( $post, true );
     431
     432                if ( is_wp_error( $post_id ) ) {
     433
     434                        if ( 'db_insert_error' === $post_id->get_error_code() ) {
     435                                $post_id->add_data( array( 'status' => 500 ) );
     436                        } else {
     437                                $post_id->add_data( array( 'status' => 400 ) );
     438                        }
     439                        return $post_id;
     440                }
     441                $post->ID = $post_id;
     442
     443                $schema = $this->get_item_schema();
     444
     445                if ( ! empty( $schema['properties']['sticky'] ) ) {
     446                        if ( ! empty( $request['sticky'] ) ) {
     447                                stick_post( $post_id );
     448                        } else {
     449                                unstick_post( $post_id );
     450                        }
     451                }
     452
     453                if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
     454                        $this->handle_featured_media( $request['featured_media'], $post->ID );
     455                }
     456
     457                if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
     458                        set_post_format( $post, $request['format'] );
     459                }
     460
     461                if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
     462                        $this->handle_template( $request['template'], $post->ID );
     463                }
     464                $terms_update = $this->handle_terms( $post->ID, $request );
     465                if ( is_wp_error( $terms_update ) ) {
     466                        return $terms_update;
     467                }
     468
     469                $post = $this->get_post( $post_id );
     470                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     471                        $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
     472                        if ( is_wp_error( $meta_update ) ) {
     473                                return $meta_update;
     474                        }
     475                }
     476
     477                $fields_update = $this->update_additional_fields_for_object( $post, $request );
     478                if ( is_wp_error( $fields_update ) ) {
     479                        return $fields_update;
     480                }
     481
     482                /**
     483                 * Fires after a single post is created or updated via the REST API.
     484                 *
     485                 * @param object          $post      Inserted Post object (not a WP_Post object).
     486                 * @param WP_REST_Request $request   Request object.
     487                 * @param boolean         $creating  True when creating post, false when updating.
     488                 */
     489                do_action( "rest_insert_{$this->post_type}", $post, $request, true );
     490
     491                $request->set_param( 'context', 'edit' );
     492                $response = $this->prepare_item_for_response( $post, $request );
     493                $response = rest_ensure_response( $response );
     494                $response->set_status( 201 );
     495                $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
     496
     497                return $response;
     498        }
     499
     500        /**
     501         * Check if a given request has access to update a post.
     502         *
     503         * @param  WP_REST_Request $request Full details about the request.
     504         * @return WP_Error|boolean
     505         */
     506        public function update_item_permissions_check( $request ) {
     507
     508                $post = $this->get_post( $request['id'] );
     509                $post_type = get_post_type_object( $this->post_type );
     510
     511                if ( $post && ! $this->check_update_permission( $post ) ) {
     512                        return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to update this post.' ), array( 'status' => rest_authorization_required_code() ) );
     513                }
     514
     515                if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
     516                        return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
     517                }
     518
     519                if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
     520                        return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
     521                }
     522
     523                return true;
     524        }
     525
     526        /**
     527         * Update a single post.
     528         *
     529         * @param WP_REST_Request $request Full details about the request.
     530         * @return WP_Error|WP_REST_Response
     531         */
     532        public function update_item( $request ) {
     533                $id = (int) $request['id'];
     534                $post = $this->get_post( $id );
     535
     536                if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
     537                        return new WP_Error( 'rest_post_invalid_id', __( 'Post id is invalid.' ), array( 'status' => 404 ) );
     538                }
     539
     540                $post = $this->prepare_item_for_database( $request );
     541                if ( is_wp_error( $post ) ) {
     542                        return $post;
     543                }
     544                // convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
     545                $post_id = wp_update_post( (array) $post, true );
     546                if ( is_wp_error( $post_id ) ) {
     547                        if ( 'db_update_error' === $post_id->get_error_code() ) {
     548                                $post_id->add_data( array( 'status' => 500 ) );
     549                        } else {
     550                                $post_id->add_data( array( 'status' => 400 ) );
     551                        }
     552                        return $post_id;
     553                }
     554
     555                $schema = $this->get_item_schema();
     556
     557                if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
     558                        set_post_format( $post, $request['format'] );
     559                }
     560
     561                if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
     562                        $this->handle_featured_media( $request['featured_media'], $post_id );
     563                }
     564
     565                if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
     566                        if ( ! empty( $request['sticky'] ) ) {
     567                                stick_post( $post_id );
     568                        } else {
     569                                unstick_post( $post_id );
     570                        }
     571                }
     572
     573                if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
     574                        $this->handle_template( $request['template'], $post->ID );
     575                }
     576
     577                $terms_update = $this->handle_terms( $post->ID, $request );
     578                if ( is_wp_error( $terms_update ) ) {
     579                        return $terms_update;
     580                }
     581
     582                $post = $this->get_post( $post_id );
     583
     584                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     585                        $meta_update = $this->meta->update_value( $request['meta'], $post->ID );
     586                        if ( is_wp_error( $meta_update ) ) {
     587                                return $meta_update;
     588                        }
     589                }
     590
     591                $fields_update = $this->update_additional_fields_for_object( $post, $request );
     592                if ( is_wp_error( $fields_update ) ) {
     593                        return $fields_update;
     594                }
     595
     596                /* This action is documented in lib/endpoints/class-wp-rest-controller.php */
     597                do_action( "rest_insert_{$this->post_type}", $post, $request, false );
     598
     599                $request->set_param( 'context', 'edit' );
     600                $response = $this->prepare_item_for_response( $post, $request );
     601                return rest_ensure_response( $response );
     602        }
     603
     604        /**
     605         * Check if a given request has access to delete a post.
     606         *
     607         * @param  WP_REST_Request $request Full details about the request.
     608         * @return bool|WP_Error
     609         */
     610        public function delete_item_permissions_check( $request ) {
     611
     612                $post = $this->get_post( $request['id'] );
     613
     614                if ( $post && ! $this->check_delete_permission( $post ) ) {
     615                        return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete posts.' ), array( 'status' => rest_authorization_required_code() ) );
     616                }
     617
     618                return true;
     619        }
     620
     621        /**
     622         * Delete a single post.
     623         *
     624         * @param WP_REST_Request $request Full details about the request.
     625         * @return WP_REST_Response|WP_Error
     626         */
     627        public function delete_item( $request ) {
     628                $id = (int) $request['id'];
     629                $force = (bool) $request['force'];
     630
     631                $post = $this->get_post( $id );
     632
     633                if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
     634                        return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
     635                }
     636
     637                $supports_trash = ( EMPTY_TRASH_DAYS > 0 );
     638                if ( 'attachment' === $post->post_type ) {
     639                        $supports_trash = $supports_trash && MEDIA_TRASH;
     640                }
     641
     642                /**
     643                 * Filter whether a post is trashable.
     644                 *
     645                 * Return false to disable trash support for the post.
     646                 *
     647                 * @param boolean $supports_trash Whether the post type support trashing.
     648                 * @param WP_Post $post           The Post object being considered for trashing support.
     649                 */
     650                $supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
     651
     652                if ( ! $this->check_delete_permission( $post ) ) {
     653                        return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
     654                }
     655
     656                $request->set_param( 'context', 'edit' );
     657                $response = $this->prepare_item_for_response( $post, $request );
     658
     659                // If we're forcing, then delete permanently.
     660                if ( $force ) {
     661                        $result = wp_delete_post( $id, true );
     662                } else {
     663                        // If we don't support trashing for this type, error out.
     664                        if ( ! $supports_trash ) {
     665                                return new WP_Error( 'rest_trash_not_supported', __( 'The post does not support trashing.' ), array( 'status' => 501 ) );
     666                        }
     667
     668                        // Otherwise, only trash if we haven't already.
     669                        if ( 'trash' === $post->post_status ) {
     670                                return new WP_Error( 'rest_already_trashed', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
     671                        }
     672
     673                        // (Note that internally this falls through to `wp_delete_post` if
     674                        // the trash is disabled.)
     675                        $result = wp_trash_post( $id );
     676                }
     677
     678                if ( ! $result ) {
     679                        return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
     680                }
     681
     682                /**
     683                 * Fires after a single post is deleted or trashed via the REST API.
     684                 *
     685                 * @param object           $post     The deleted or trashed post.
     686                 * @param WP_REST_Response $response The response data.
     687                 * @param WP_REST_Request  $request  The request sent to the API.
     688                 */
     689                do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
     690
     691                return $response;
     692        }
     693
     694        /**
     695         * Determine the allowed query_vars for a get_items() response and
     696         * prepare for WP_Query.
     697         *
     698         * @param array           $prepared_args Prepared WP_Query arguments.
     699         * @param WP_REST_Request $request       Full details about the request.
     700         * @return array          $query_args
     701         */
     702        protected function prepare_items_query( $prepared_args = array(), $request = null ) {
     703
     704                $valid_vars = array_flip( $this->get_allowed_query_vars( $request ) );
     705                $query_args = array();
     706                foreach ( $valid_vars as $var => $index ) {
     707                        if ( isset( $prepared_args[ $var ] ) ) {
     708                                /**
     709                                 * Filter the query_vars used in `get_items` for the constructed query.
     710                                 *
     711                                 * The dynamic portion of the hook name, $var, refers to the query_var key.
     712                                 *
     713                                 * @param mixed $prepared_args[ $var ] The query_var value.
     714                                 */
     715                                $query_args[ $var ] = apply_filters( "rest_query_var-{$var}", $prepared_args[ $var ] );
     716                        }
     717                }
     718
     719                if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
     720                        $query_args['ignore_sticky_posts'] = true;
     721                }
     722
     723                if ( 'include' === $query_args['orderby'] ) {
     724                        $query_args['orderby'] = 'post__in';
     725                }
     726
     727                return $query_args;
     728        }
     729
     730        /**
     731         * Get all the WP Query vars that are allowed for the API request.
     732         *
     733         * @param WP_REST_Request $request Full details about the request.
     734         * @return array
     735         */
     736        protected function get_allowed_query_vars( $request = null ) {
     737                global $wp;
     738
     739                /**
     740                 * Filter the publicly allowed query vars.
     741                 *
     742                 * Allows adjusting of the default query vars that are made public.
     743                 *
     744                 * @param array  Array of allowed WP_Query query vars.
     745                 */
     746                $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
     747
     748                $post_type_obj = get_post_type_object( $this->post_type );
     749                if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
     750                        /**
     751                         * Filter the allowed 'private' query vars for authorized users.
     752                         *
     753                         * If the user has the `edit_posts` capability, we also allow use of
     754                         * private query parameters, which are only undesirable on the
     755                         * frontend, but are safe for use in query strings.
     756                         *
     757                         * To disable anyway, use
     758                         * `add_filter( 'rest_private_query_vars', '__return_empty_array' );`
     759                         *
     760                         * @param array $private_query_vars Array of allowed query vars for authorized users.
     761                         * }
     762                         */
     763                        $private = apply_filters( 'rest_private_query_vars', $wp->private_query_vars );
     764                        $valid_vars = array_merge( $valid_vars, $private );
     765                }
     766                // Define our own in addition to WP's normal vars.
     767                $rest_valid = array(
     768                        'author__in',
     769                        'author__not_in',
     770                        'ignore_sticky_posts',
     771                        'menu_order',
     772                        'offset',
     773                        'post__in',
     774                        'post__not_in',
     775                        'post_parent',
     776                        'post_parent__in',
     777                        'post_parent__not_in',
     778                        'posts_per_page',
     779                        'date_query',
     780                );
     781                $valid_vars = array_merge( $valid_vars, $rest_valid );
     782
     783                /**
     784                 * Filter allowed query vars for the REST API.
     785                 *
     786                 * This filter allows you to add or remove query vars from the final allowed
     787                 * list for all requests, including unauthenticated ones. To alter the
     788                 * vars for editors only, {@see rest_private_query_vars}.
     789                 *
     790                 * @param array {
     791                 *    Array of allowed WP_Query query vars.
     792                 *
     793                 *    @param string $allowed_query_var The query var to allow.
     794                 *    @param WP_REST_Request $request Request object.
     795                 * }
     796                 */
     797                $valid_vars = apply_filters( 'rest_query_vars', $valid_vars, $request );
     798
     799                return $valid_vars;
     800        }
     801
     802        /**
     803         * Check the post_date_gmt or modified_gmt and prepare any post or
     804         * modified date for single post output.
     805         *
     806         * @param string      $date_gmt GMT publication time.
     807         * @param string|null $date     Optional, default is null. Local publication time.
     808         * @return string|null ISO8601/RFC3339 formatted datetime.
     809         */
     810        protected function prepare_date_response( $date_gmt, $date = null ) {
     811                // Use the date if passed.
     812                if ( isset( $date ) ) {
     813                        return mysql_to_rfc3339( $date );
     814                }
     815
     816                // Return null if $date_gmt is empty/zeros.
     817                if ( '0000-00-00 00:00:00' === $date_gmt ) {
     818                        return null;
     819                }
     820
     821                // Return the formatted datetime.
     822                return mysql_to_rfc3339( $date_gmt );
     823        }
     824
     825        /**
     826         * Prepare a single post for create or update.
     827         *
     828         * @param WP_REST_Request $request Request object.
     829         * @return WP_Error|stdClass $prepared_post Post object.
     830         */
     831        protected function prepare_item_for_database( $request ) {
     832                $prepared_post = new stdClass;
     833
     834                // ID.
     835                if ( isset( $request['id'] ) ) {
     836                        $prepared_post->ID = absint( $request['id'] );
     837                }
     838
     839                $schema = $this->get_item_schema();
     840
     841                // Post title.
     842                if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
     843                        if ( is_string( $request['title'] ) ) {
     844                                $prepared_post->post_title = wp_filter_post_kses( $request['title'] );
     845                        } elseif ( ! empty( $request['title']['raw'] ) ) {
     846                                $prepared_post->post_title = wp_filter_post_kses( $request['title']['raw'] );
     847                        }
     848                }
     849
     850                // Post content.
     851                if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
     852                        if ( is_string( $request['content'] ) ) {
     853                                $prepared_post->post_content = wp_filter_post_kses( $request['content'] );
     854                        } elseif ( isset( $request['content']['raw'] ) ) {
     855                                $prepared_post->post_content = wp_filter_post_kses( $request['content']['raw'] );
     856                        }
     857                }
     858
     859                // Post excerpt.
     860                if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
     861                        if ( is_string( $request['excerpt'] ) ) {
     862                                $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt'] );
     863                        } elseif ( isset( $request['excerpt']['raw'] ) ) {
     864                                $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt']['raw'] );
     865                        }
     866                }
     867
     868                // Post type.
     869                if ( empty( $request['id'] ) ) {
     870                        // Creating new post, use default type for the controller.
     871                        $prepared_post->post_type = $this->post_type;
     872                } else {
     873                        // Updating a post, use previous type.
     874                        $prepared_post->post_type = get_post_type( $request['id'] );
     875                }
     876                $post_type = get_post_type_object( $prepared_post->post_type );
     877
     878                // Post status.
     879                if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
     880                        $status = $this->handle_status_param( $request['status'], $post_type );
     881                        if ( is_wp_error( $status ) ) {
     882                                return $status;
     883                        }
     884
     885                        $prepared_post->post_status = $status;
     886                }
     887
     888                // Post date.
     889                if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
     890                        $date_data = rest_get_date_with_gmt( $request['date'] );
     891
     892                        if ( ! empty( $date_data ) ) {
     893                                list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
     894                        }
     895                } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
     896                        $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
     897
     898                        if ( ! empty( $date_data ) ) {
     899                                list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
     900                        }
     901                }
     902                // Post slug.
     903                if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
     904                        $prepared_post->post_name = $request['slug'];
     905                }
     906
     907                // Author.
     908                if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
     909                        $post_author = (int) $request['author'];
     910                        if ( get_current_user_id() !== $post_author ) {
     911                                $user_obj = get_userdata( $post_author );
     912                                if ( ! $user_obj ) {
     913                                        return new WP_Error( 'rest_invalid_author', __( 'Invalid author id.' ), array( 'status' => 400 ) );
     914                                }
     915                        }
     916                        $prepared_post->post_author = $post_author;
     917                }
     918
     919                // Post password.
     920                if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) && '' !== $request['password'] ) {
     921                        $prepared_post->post_password = $request['password'];
     922
     923                        if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
     924                                return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) );
     925                        }
     926
     927                        if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
     928                                return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) );
     929                        }
     930                }
     931
     932                if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
     933                        if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
     934                                return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) );
     935                        }
     936                }
     937
     938                // Parent.
     939                if ( ! empty( $schema['properties']['parent'] ) && ! empty( $request['parent'] ) ) {
     940                        $parent = $this->get_post( (int) $request['parent'] );
     941                        if ( empty( $parent ) ) {
     942                                return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent id.' ), array( 'status' => 400 ) );
     943                        }
     944
     945                        $prepared_post->post_parent = (int) $parent->ID;
     946                }
     947
     948                // Menu order.
     949                if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
     950                        $prepared_post->menu_order = (int) $request['menu_order'];
     951                }
     952
     953                // Comment status.
     954                if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
     955                        $prepared_post->comment_status = $request['comment_status'];
     956                }
     957
     958                // Ping status.
     959                if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
     960                        $prepared_post->ping_status = $request['ping_status'];
     961                }
     962                /**
     963                 * Filter a post before it is inserted via the REST API.
     964                 *
     965                 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
     966                 * prepared for insertion.
     967                 *
     968                 * @param stdClass        $prepared_post An object representing a single post prepared
     969                 *                                       for inserting or updating the database.
     970                 * @param WP_REST_Request $request       Request object.
     971                 */
     972                return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
     973
     974        }
     975
     976        /**
     977         * Determine validity and normalize provided status param.
     978         *
     979         * @param string $post_status Post status.
     980         * @param object $post_type   Post type.
     981         * @return WP_Error|string $post_status
     982         */
     983        protected function handle_status_param( $post_status, $post_type ) {
     984
     985                switch ( $post_status ) {
     986                        case 'draft':
     987                        case 'pending':
     988                                break;
     989                        case 'private':
     990                                if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
     991                                        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() ) );
     992                                }
     993                                break;
     994                        case 'publish':
     995                        case 'future':
     996                                if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
     997                                        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() ) );
     998                                }
     999                                break;
     1000                        default:
     1001                                if ( ! get_post_status_object( $post_status ) ) {
     1002                                        $post_status = 'draft';
     1003                                }
     1004                                break;
     1005                }
     1006
     1007                return $post_status;
     1008        }
     1009
     1010        /**
     1011         * Determine the featured media based on a request param.
     1012         *
     1013         * @param int $featured_media Featured Media ID.
     1014         * @param int $post_id        Post ID.
     1015         * @return bool|WP_Error
     1016         */
     1017        protected function handle_featured_media( $featured_media, $post_id ) {
     1018
     1019                $featured_media = (int) $featured_media;
     1020                if ( $featured_media ) {
     1021                        $result = set_post_thumbnail( $post_id, $featured_media );
     1022                        if ( $result ) {
     1023                                return true;
     1024                        } else {
     1025                                return new WP_Error( 'rest_invalid_featured_media', __( 'Invalid featured media id.' ), array( 'status' => 400 ) );
     1026                        }
     1027                } else {
     1028                        return delete_post_thumbnail( $post_id );
     1029                }
     1030
     1031        }
     1032
     1033        /**
     1034         * Set the template for a page.
     1035         *
     1036         * @param string  $template Page template filename.
     1037         * @param integer $post_id  Post ID.
     1038         */
     1039        public function handle_template( $template, $post_id ) {
     1040                if ( in_array( $template, array_keys( wp_get_theme()->get_page_templates( $this->get_post( $post_id ) ) ), true ) ) {
     1041                        update_post_meta( $post_id, '_wp_page_template', $template );
     1042                } else {
     1043                        update_post_meta( $post_id, '_wp_page_template', '' );
     1044                }
     1045        }
     1046
     1047        /**
     1048         * Update the post's terms from a REST request.
     1049         *
     1050         * @param  int             $post_id The post ID to update the terms form.
     1051         * @param  WP_REST_Request $request The request object with post and terms data.
     1052         * @return null|WP_Error   WP_Error on an error assigning any of the terms.
     1053         */
     1054        protected function handle_terms( $post_id, $request ) {
     1055                $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     1056                foreach ( $taxonomies as $taxonomy ) {
     1057                        $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     1058
     1059                        if ( ! isset( $request[ $base ] ) ) {
     1060                                continue;
     1061                        }
     1062                        $terms = array_map( 'absint', $request[ $base ] );
     1063                        $result = wp_set_object_terms( $post_id, $terms, $taxonomy->name );
     1064                        if ( is_wp_error( $result ) ) {
     1065                                return $result;
     1066                        }
     1067                }
     1068        }
     1069
     1070        /**
     1071         * Check if a given post type should be viewed or managed.
     1072         *
     1073         * @param object|string $post_type Post type name or object.
     1074         * @return boolean Is post type allowed?
     1075         */
     1076        protected function check_is_post_type_allowed( $post_type ) {
     1077                if ( ! is_object( $post_type ) ) {
     1078                        $post_type = get_post_type_object( $post_type );
     1079                }
     1080
     1081                if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
     1082                        return true;
     1083                }
     1084
     1085                return false;
     1086        }
     1087
     1088        /**
     1089         * Check if we can read a post.
     1090         *
     1091         * Correctly handles posts with the inherit status.
     1092         *
     1093         * @param object $post Post object.
     1094         * @return boolean Can we read it?
     1095         */
     1096        public function check_read_permission( $post ) {
     1097                $post_type = get_post_type_object( $post->post_type );
     1098                if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
     1099                        return false;
     1100                }
     1101
     1102                // Can we read the post?
     1103                if ( 'publish' === $post->post_status || current_user_can( $post_type->cap->read_post, $post->ID ) ) {
     1104                        return true;
     1105                }
     1106
     1107                $post_status_obj = get_post_status_object( $post->post_status );
     1108                if ( $post_status_obj && $post_status_obj->public ) {
     1109                        return true;
     1110                }
     1111
     1112                // Can we read the parent if we're inheriting?
     1113                if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
     1114                        $parent = $this->get_post( $post->post_parent );
     1115                        return $this->check_read_permission( $parent );
     1116                }
     1117
     1118                // If we don't have a parent, but the status is set to inherit, assume
     1119                // it's published (as per get_post_status()).
     1120                if ( 'inherit' === $post->post_status ) {
     1121                        return true;
     1122                }
     1123
     1124                return false;
     1125        }
     1126
     1127        /**
     1128         * Check if we can edit a post.
     1129         *
     1130         * @param object $post Post object.
     1131         * @return boolean Can we edit it?
     1132         */
     1133        protected function check_update_permission( $post ) {
     1134                $post_type = get_post_type_object( $post->post_type );
     1135
     1136                if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
     1137                        return false;
     1138                }
     1139
     1140                return current_user_can( $post_type->cap->edit_post, $post->ID );
     1141        }
     1142
     1143        /**
     1144         * Check if we can create a post.
     1145         *
     1146         * @param object $post Post object.
     1147         * @return boolean Can we create it?.
     1148         */
     1149        protected function check_create_permission( $post ) {
     1150                $post_type = get_post_type_object( $post->post_type );
     1151
     1152                if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
     1153                        return false;
     1154                }
     1155
     1156                return current_user_can( $post_type->cap->create_posts );
     1157        }
     1158
     1159        /**
     1160         * Check if we can delete a post.
     1161         *
     1162         * @param object $post Post object.
     1163         * @return boolean Can we delete it?
     1164         */
     1165        protected function check_delete_permission( $post ) {
     1166                $post_type = get_post_type_object( $post->post_type );
     1167
     1168                if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
     1169                        return false;
     1170                }
     1171
     1172                return current_user_can( $post_type->cap->delete_post, $post->ID );
     1173        }
     1174
     1175        /**
     1176         * Prepare a single post output for response.
     1177         *
     1178         * @param WP_Post         $post    Post object.
     1179         * @param WP_REST_Request $request Request object.
     1180         * @return WP_REST_Response $data
     1181         */
     1182        public function prepare_item_for_response( $post, $request ) {
     1183                $GLOBALS['post'] = $post;
     1184                setup_postdata( $post );
     1185
     1186                $schema = $this->get_item_schema();
     1187
     1188                // Base fields for every post.
     1189                $data = array();
     1190
     1191                if ( ! empty( $schema['properties']['id'] ) ) {
     1192                        $data['id'] = $post->ID;
     1193                }
     1194
     1195                if ( ! empty( $schema['properties']['date'] ) ) {
     1196                        $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
     1197                }
     1198
     1199                if ( ! empty( $schema['properties']['date_gmt'] ) ) {
     1200                        $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
     1201                }
     1202
     1203                if ( ! empty( $schema['properties']['guid'] ) ) {
     1204                        $data['guid'] = array(
     1205                                /** This filter is documented in wp-includes/post-template.php */
     1206                                'rendered' => apply_filters( 'get_the_guid', $post->guid ),
     1207                                'raw'      => $post->guid,
     1208                        );
     1209                }
     1210
     1211                if ( ! empty( $schema['properties']['modified'] ) ) {
     1212                        $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
     1213                }
     1214
     1215                if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
     1216                        $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
     1217                }
     1218
     1219                if ( ! empty( $schema['properties']['password'] ) ) {
     1220                        $data['password'] = $post->post_password;
     1221                }
     1222
     1223                if ( ! empty( $schema['properties']['slug'] ) ) {
     1224                        $data['slug'] = $post->post_name;
     1225                }
     1226
     1227                if ( ! empty( $schema['properties']['status'] ) ) {
     1228                        $data['status'] = $post->post_status;
     1229                }
     1230
     1231                if ( ! empty( $schema['properties']['type'] ) ) {
     1232                        $data['type'] = $post->post_type;
     1233                }
     1234
     1235                if ( ! empty( $schema['properties']['link'] ) ) {
     1236                        $data['link'] = get_permalink( $post->ID );
     1237                }
     1238
     1239                if ( ! empty( $schema['properties']['title'] ) ) {
     1240                        add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
     1241                        $data['title'] = array(
     1242                                'raw'      => $post->post_title,
     1243                                'rendered' => get_the_title( $post->ID ),
     1244                        );
     1245                        remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
     1246                }
     1247
     1248                $has_password_filter = false;
     1249                if ( $this->can_access_password_content( $post, $request ) ) {
     1250                        // Allow access to the post, permissions already checked before.
     1251                        add_filter( 'post_password_required', '__return_false' );
     1252                        $has_password_filter = true;
     1253                }
     1254
     1255                if ( ! empty( $schema['properties']['content'] ) ) {
     1256                        $data['content'] = array(
     1257                                'raw'       => $post->post_content,
     1258                                /** This filter is documented in wp-includes/post-template.php */
     1259                                'rendered'  => post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content ),
     1260                                'protected' => (bool) $post->post_password,
     1261                        );
     1262                }
     1263
     1264                if ( ! empty( $schema['properties']['excerpt'] ) ) {
     1265                        /** This filter is documented in wp-includes/post-template.php */
     1266                        $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
     1267                        $data['excerpt'] = array(
     1268                                'raw'       => $post->post_excerpt,
     1269                                'rendered'  => post_password_required( $post ) ? '' : $excerpt,
     1270                                'protected' => (bool) $post->post_password,
     1271                        );
     1272                }
     1273
     1274                if ( $has_password_filter ) {
     1275                        // Reset filter.
     1276                        remove_filter( 'post_password_required', '__return_false' );
     1277                }
     1278
     1279                if ( ! empty( $schema['properties']['author'] ) ) {
     1280                        $data['author'] = (int) $post->post_author;
     1281                }
     1282
     1283                if ( ! empty( $schema['properties']['featured_media'] ) ) {
     1284                        $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
     1285                }
     1286
     1287                if ( ! empty( $schema['properties']['parent'] ) ) {
     1288                        $data['parent'] = (int) $post->post_parent;
     1289                }
     1290
     1291                if ( ! empty( $schema['properties']['menu_order'] ) ) {
     1292                        $data['menu_order'] = (int) $post->menu_order;
     1293                }
     1294
     1295                if ( ! empty( $schema['properties']['comment_status'] ) ) {
     1296                        $data['comment_status'] = $post->comment_status;
     1297                }
     1298
     1299                if ( ! empty( $schema['properties']['ping_status'] ) ) {
     1300                        $data['ping_status'] = $post->ping_status;
     1301                }
     1302
     1303                if ( ! empty( $schema['properties']['sticky'] ) ) {
     1304                        $data['sticky'] = is_sticky( $post->ID );
     1305                }
     1306
     1307                if ( ! empty( $schema['properties']['template'] ) ) {
     1308                        if ( $template = get_page_template_slug( $post->ID ) ) {
     1309                                $data['template'] = $template;
     1310                        } else {
     1311                                $data['template'] = '';
     1312                        }
     1313                }
     1314
     1315                if ( ! empty( $schema['properties']['format'] ) ) {
     1316                        $data['format'] = get_post_format( $post->ID );
     1317                        // Fill in blank post format.
     1318                        if ( empty( $data['format'] ) ) {
     1319                                $data['format'] = 'standard';
     1320                        }
     1321                }
     1322
     1323                if ( ! empty( $schema['properties']['meta'] ) ) {
     1324                        $data['meta'] = $this->meta->get_value( $post->ID, $request );
     1325                }
     1326
     1327                $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     1328                foreach ( $taxonomies as $taxonomy ) {
     1329                        $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     1330                        if ( ! empty( $schema['properties'][ $base ] ) ) {
     1331                                $terms = get_the_terms( $post, $taxonomy->name );
     1332                                $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
     1333                        }
     1334                }
     1335
     1336                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     1337                $data = $this->add_additional_fields_to_object( $data, $request );
     1338                $data = $this->filter_response_by_context( $data, $context );
     1339
     1340                // Wrap the data in a response object.
     1341                $response = rest_ensure_response( $data );
     1342
     1343                $response->add_links( $this->prepare_links( $post ) );
     1344
     1345                /**
     1346                 * Filter the post data for a response.
     1347                 *
     1348                 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
     1349                 * prepared for the response.
     1350                 *
     1351                 * @param WP_REST_Response   $response   The response object.
     1352                 * @param WP_Post            $post       Post object.
     1353                 * @param WP_REST_Request    $request    Request object.
     1354                 */
     1355                return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
     1356        }
     1357
     1358        /**
     1359         * Overwrite the default protected title format.
     1360         *
     1361         * By default WordPress will show password protected posts with a title of
     1362         * "Protected: %s", as the REST API communicates the protected status of a post
     1363         * in a machine readable format, we remove the "Protected: " prefix.
     1364         *
     1365         * @return string
     1366         */
     1367        public function protected_title_format() {
     1368                return '%s';
     1369        }
     1370
     1371        /**
     1372         * Prepare links for the request.
     1373         *
     1374         * @param WP_Post $post Post object.
     1375         * @return array Links for the given post.
     1376         */
     1377        protected function prepare_links( $post ) {
     1378                $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
     1379
     1380                // Entity meta.
     1381                $links = array(
     1382                        'self' => array(
     1383                                'href'   => rest_url( trailingslashit( $base ) . $post->ID ),
     1384                        ),
     1385                        'collection' => array(
     1386                                'href'   => rest_url( $base ),
     1387                        ),
     1388                        'about'      => array(
     1389                                'href'   => rest_url( 'wp/v2/types/' . $this->post_type ),
     1390                        ),
     1391                );
     1392
     1393                if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
     1394                        && ! empty( $post->post_author ) ) {
     1395                        $links['author'] = array(
     1396                                'href'       => rest_url( 'wp/v2/users/' . $post->post_author ),
     1397                                'embeddable' => true,
     1398                        );
     1399                }
     1400
     1401                if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
     1402                        $replies_url = rest_url( 'wp/v2/comments' );
     1403                        $replies_url = add_query_arg( 'post', $post->ID, $replies_url );
     1404                        $links['replies'] = array(
     1405                                'href'         => $replies_url,
     1406                                'embeddable'   => true,
     1407                        );
     1408                }
     1409
     1410                if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
     1411                        $links['version-history'] = array(
     1412                                'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
     1413                        );
     1414                }
     1415                $post_type_obj = get_post_type_object( $post->post_type );
     1416                if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
     1417                        $links['up'] = array(
     1418                                'href'       => rest_url( trailingslashit( $base ) . (int) $post->post_parent ),
     1419                                'embeddable' => true,
     1420                        );
     1421                }
     1422
     1423                // If we have a featured media, add that.
     1424                if ( $featured_media = get_post_thumbnail_id( $post->ID ) ) {
     1425                        $image_url = rest_url( 'wp/v2/media/' . $featured_media );
     1426                        $links['https://api.w.org/featuredmedia'] = array(
     1427                                'href'       => $image_url,
     1428                                'embeddable' => true,
     1429                        );
     1430                }
     1431                if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
     1432                        $attachments_url = rest_url( 'wp/v2/media' );
     1433                        $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
     1434                        $links['https://api.w.org/attachment'] = array(
     1435                                'href'       => $attachments_url,
     1436                        );
     1437                }
     1438
     1439                $taxonomies = get_object_taxonomies( $post->post_type );
     1440                if ( ! empty( $taxonomies ) ) {
     1441                        $links['https://api.w.org/term'] = array();
     1442
     1443                        foreach ( $taxonomies as $tax ) {
     1444                                $taxonomy_obj = get_taxonomy( $tax );
     1445                                // Skip taxonomies that are not public.
     1446                                if ( empty( $taxonomy_obj->show_in_rest ) ) {
     1447                                        continue;
     1448                                }
     1449
     1450                                $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;
     1451                                $terms_url = add_query_arg(
     1452                                        'post',
     1453                                        $post->ID,
     1454                                        rest_url( 'wp/v2/' . $tax_base )
     1455                                );
     1456
     1457                                $links['https://api.w.org/term'][] = array(
     1458                                        'href'       => $terms_url,
     1459                                        'taxonomy'   => $tax,
     1460                                        'embeddable' => true,
     1461                                );
     1462                        }
     1463                }
     1464
     1465                return $links;
     1466        }
     1467
     1468        /**
     1469         * Get the Post's schema, conforming to JSON Schema.
     1470         *
     1471         * @return array
     1472         */
     1473        public function get_item_schema() {
     1474
     1475                $schema = array(
     1476                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     1477                        'title'      => $this->post_type,
     1478                        'type'       => 'object',
     1479                        /*
     1480                         * Base properties for every Post.
     1481                         */
     1482                        'properties' => array(
     1483                                'date'            => array(
     1484                                        'description' => __( "The date the object was published, in the site's timezone." ),
     1485                                        'type'        => 'string',
     1486                                        'format'      => 'date-time',
     1487                                        'context'     => array( 'view', 'edit', 'embed' ),
     1488                                ),
     1489                                'date_gmt'        => array(
     1490                                        'description' => __( 'The date the object was published, as GMT.' ),
     1491                                        'type'        => 'string',
     1492                                        'format'      => 'date-time',
     1493                                        'context'     => array( 'view', 'edit' ),
     1494                                ),
     1495                                'guid'            => array(
     1496                                        'description' => __( 'The globally unique identifier for the object.' ),
     1497                                        'type'        => 'object',
     1498                                        'context'     => array( 'view', 'edit' ),
     1499                                        'readonly'    => true,
     1500                                        'properties'  => array(
     1501                                                'raw'      => array(
     1502                                                        'description' => __( 'GUID for the object, as it exists in the database.' ),
     1503                                                        'type'        => 'string',
     1504                                                        'context'     => array( 'edit' ),
     1505                                                        'readonly'    => true,
     1506                                                ),
     1507                                                'rendered' => array(
     1508                                                        'description' => __( 'GUID for the object, transformed for display.' ),
     1509                                                        'type'        => 'string',
     1510                                                        'context'     => array( 'view', 'edit' ),
     1511                                                        'readonly'    => true,
     1512                                                ),
     1513                                        ),
     1514                                ),
     1515                                'id'              => array(
     1516                                        'description' => __( 'Unique identifier for the object.' ),
     1517                                        'type'        => 'integer',
     1518                                        'context'     => array( 'view', 'edit', 'embed' ),
     1519                                        'readonly'    => true,
     1520                                ),
     1521                                'link'            => array(
     1522                                        'description' => __( 'URL to the object.' ),
     1523                                        'type'        => 'string',
     1524                                        'format'      => 'uri',
     1525                                        'context'     => array( 'view', 'edit', 'embed' ),
     1526                                        'readonly'    => true,
     1527                                ),
     1528                                'modified'        => array(
     1529                                        'description' => __( "The date the object was last modified, in the site's timezone." ),
     1530                                        'type'        => 'string',
     1531                                        'format'      => 'date-time',
     1532                                        'context'     => array( 'view', 'edit' ),
     1533                                        'readonly'    => true,
     1534                                ),
     1535                                'modified_gmt'    => array(
     1536                                        'description' => __( 'The date the object was last modified, as GMT.' ),
     1537                                        'type'        => 'string',
     1538                                        'format'      => 'date-time',
     1539                                        'context'     => array( 'view', 'edit' ),
     1540                                        'readonly'    => true,
     1541                                ),
     1542                                'slug'            => array(
     1543                                        'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
     1544                                        'type'        => 'string',
     1545                                        'context'     => array( 'view', 'edit', 'embed' ),
     1546                                        'arg_options' => array(
     1547                                                'sanitize_callback' => array( $this, 'sanitize_slug' ),
     1548                                        ),
     1549                                ),
     1550                                'status'          => array(
     1551                                        'description' => __( 'A named status for the object.' ),
     1552                                        'type'        => 'string',
     1553                                        'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
     1554                                        'context'     => array( 'edit' ),
     1555                                ),
     1556                                'type'            => array(
     1557                                        'description' => __( 'Type of Post for the object.' ),
     1558                                        'type'        => 'string',
     1559                                        'context'     => array( 'view', 'edit', 'embed' ),
     1560                                        'readonly'    => true,
     1561                                ),
     1562                        ),
     1563                );
     1564
     1565                $post_type_obj = get_post_type_object( $this->post_type );
     1566                if ( $post_type_obj->hierarchical ) {
     1567                        $schema['properties']['parent'] = array(
     1568                                'description' => __( 'The id for the parent of the object.' ),
     1569                                'type'        => 'integer',
     1570                                'context'     => array( 'view', 'edit' ),
     1571                        );
     1572                }
     1573
     1574                $post_type_attributes = array(
     1575                        'title',
     1576                        'editor',
     1577                        'author',
     1578                        'excerpt',
     1579                        'thumbnail',
     1580                        'comments',
     1581                        'revisions',
     1582                        'page-attributes',
     1583                        'post-formats',
     1584                        'custom-fields',
     1585                );
     1586                $fixed_schemas = array(
     1587                        'post' => array(
     1588                                'title',
     1589                                'editor',
     1590                                'author',
     1591                                'excerpt',
     1592                                'thumbnail',
     1593                                'comments',
     1594                                'revisions',
     1595                                'post-formats',
     1596                                'custom-fields',
     1597                        ),
     1598                        'page' => array(
     1599                                'title',
     1600                                'editor',
     1601                                'author',
     1602                                'excerpt',
     1603                                'thumbnail',
     1604                                'comments',
     1605                                'revisions',
     1606                                'page-attributes',
     1607                                'custom-fields',
     1608                        ),
     1609                        'attachment' => array(
     1610                                'title',
     1611                                'author',
     1612                                'comments',
     1613                                'revisions',
     1614                                'custom-fields',
     1615                        ),
     1616                );
     1617                foreach ( $post_type_attributes as $attribute ) {
     1618                        if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
     1619                                continue;
     1620                        } elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
     1621                                continue;
     1622                        }
     1623
     1624                        switch ( $attribute ) {
     1625
     1626                                case 'title':
     1627                                        $schema['properties']['title'] = array(
     1628                                                'description' => __( 'The title for the object.' ),
     1629                                                'type'        => 'object',
     1630                                                'context'     => array( 'view', 'edit', 'embed' ),
     1631                                                'properties'  => array(
     1632                                                        'raw' => array(
     1633                                                                'description' => __( 'Title for the object, as it exists in the database.' ),
     1634                                                                'type'        => 'string',
     1635                                                                'context'     => array( 'edit' ),
     1636                                                        ),
     1637                                                        'rendered' => array(
     1638                                                                'description' => __( 'HTML title for the object, transformed for display.' ),
     1639                                                                'type'        => 'string',
     1640                                                                'context'     => array( 'view', 'edit', 'embed' ),
     1641                                                                'readonly'    => true,
     1642                                                        ),
     1643                                                ),
     1644                                        );
     1645                                        break;
     1646
     1647                                case 'editor':
     1648                                        $schema['properties']['content'] = array(
     1649                                                'description' => __( 'The content for the object.' ),
     1650                                                'type'        => 'object',
     1651                                                'context'     => array( 'view', 'edit' ),
     1652                                                'properties'  => array(
     1653                                                        'raw' => array(
     1654                                                                'description' => __( 'Content for the object, as it exists in the database.' ),
     1655                                                                'type'        => 'string',
     1656                                                                'context'     => array( 'edit' ),
     1657                                                        ),
     1658                                                        'rendered' => array(
     1659                                                                'description' => __( 'HTML content for the object, transformed for display.' ),
     1660                                                                'type'        => 'string',
     1661                                                                'context'     => array( 'view', 'edit' ),
     1662                                                                'readonly'    => true,
     1663                                                        ),
     1664                                                        'protected'       => array(
     1665                                                                'description' => __( 'Whether the content is protected with a password.' ),
     1666                                                                'type'        => 'boolean',
     1667                                                                'context'     => array( 'view', 'edit', 'embed' ),
     1668                                                                'readonly'    => true,
     1669                                                        ),
     1670                                                ),
     1671                                        );
     1672                                        break;
     1673
     1674                                case 'author':
     1675                                        $schema['properties']['author'] = array(
     1676                                                'description' => __( 'The id for the author of the object.' ),
     1677                                                'type'        => 'integer',
     1678                                                'context'     => array( 'view', 'edit', 'embed' ),
     1679                                        );
     1680                                        break;
     1681
     1682                                case 'excerpt':
     1683                                        $schema['properties']['excerpt'] = array(
     1684                                                'description' => __( 'The excerpt for the object.' ),
     1685                                                'type'        => 'object',
     1686                                                'context'     => array( 'view', 'edit', 'embed' ),
     1687                                                'properties'  => array(
     1688                                                        'raw' => array(
     1689                                                                'description' => __( 'Excerpt for the object, as it exists in the database.' ),
     1690                                                                'type'        => 'string',
     1691                                                                'context'     => array( 'edit' ),
     1692                                                        ),
     1693                                                        'rendered' => array(
     1694                                                                'description' => __( 'HTML excerpt for the object, transformed for display.' ),
     1695                                                                'type'        => 'string',
     1696                                                                'context'     => array( 'view', 'edit', 'embed' ),
     1697                                                                'readonly'    => true,
     1698                                                        ),
     1699                                                        'protected'       => array(
     1700                                                                'description' => __( 'Whether the excerpt is protected with a password.' ),
     1701                                                                'type'        => 'boolean',
     1702                                                                'context'     => array( 'view', 'edit', 'embed' ),
     1703                                                                'readonly'    => true,
     1704                                                        ),
     1705                                                ),
     1706                                        );
     1707                                        break;
     1708
     1709                                case 'thumbnail':
     1710                                        $schema['properties']['featured_media'] = array(
     1711                                                'description' => __( 'The id of the featured media for the object.' ),
     1712                                                'type'        => 'integer',
     1713                                                'context'     => array( 'view', 'edit' ),
     1714                                        );
     1715                                        break;
     1716
     1717                                case 'comments':
     1718                                        $schema['properties']['comment_status'] = array(
     1719                                                'description' => __( 'Whether or not comments are open on the object.' ),
     1720                                                'type'        => 'string',
     1721                                                'enum'        => array( 'open', 'closed' ),
     1722                                                'context'     => array( 'view', 'edit' ),
     1723                                        );
     1724                                        $schema['properties']['ping_status'] = array(
     1725                                                'description' => __( 'Whether or not the object can be pinged.' ),
     1726                                                'type'        => 'string',
     1727                                                'enum'        => array( 'open', 'closed' ),
     1728                                                'context'     => array( 'view', 'edit' ),
     1729                                        );
     1730                                        break;
     1731
     1732                                case 'page-attributes':
     1733                                        $schema['properties']['menu_order'] = array(
     1734                                                'description' => __( 'The order of the object in relation to other object of its type.' ),
     1735                                                'type'        => 'integer',
     1736                                                'context'     => array( 'view', 'edit' ),
     1737                                        );
     1738                                        break;
     1739
     1740                                case 'post-formats':
     1741                                        $schema['properties']['format'] = array(
     1742                                                'description' => __( 'The format for the object.' ),
     1743                                                'type'        => 'string',
     1744                                                'enum'        => array_values( get_post_format_slugs() ),
     1745                                                'context'     => array( 'view', 'edit' ),
     1746                                        );
     1747                                        break;
     1748
     1749                                case 'custom-fields':
     1750                                        $schema['properties']['meta'] = $this->meta->get_field_schema();
     1751                                        break;
     1752
     1753                        }
     1754                }
     1755
     1756                if ( 'post' === $this->post_type ) {
     1757                        $schema['properties']['sticky'] = array(
     1758                                'description' => __( 'Whether or not the object should be treated as sticky.' ),
     1759                                'type'        => 'boolean',
     1760                                'context'     => array( 'view', 'edit' ),
     1761                        );
     1762
     1763                        $schema['properties']['password'] = array(
     1764                                'description' => __( 'A password to protect access to the content and excerpt.' ),
     1765                                'type'        => 'string',
     1766                                'context'     => array( 'edit' ),
     1767                        );
     1768                }
     1769
     1770                if ( 'page' === $this->post_type ) {
     1771                        $schema['properties']['template'] = array(
     1772                                'description' => __( 'The theme file to use to display the object.' ),
     1773                                'type'        => 'string',
     1774                                'enum'        => array_keys( wp_get_theme()->get_page_templates() ),
     1775                                'context'     => array( 'view', 'edit' ),
     1776                        );
     1777                }
     1778
     1779                $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     1780                foreach ( $taxonomies as $taxonomy ) {
     1781                        $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     1782                        $schema['properties'][ $base ] = array(
     1783                                'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
     1784                                'type'        => 'array',
     1785                                'context'     => array( 'view', 'edit' ),
     1786                        );
     1787                        $schema['properties'][ $base . '_exclude' ] = array(
     1788                                'description' => sprintf( __( 'The terms in the %s taxonomy that should not be assigned to the object.' ), $taxonomy->name ),
     1789                                'type'        => 'array',
     1790                                'context'     => array( 'view', 'edit' ),
     1791                        );
     1792                }
     1793
     1794                return $this->add_additional_fields_schema( $schema );
     1795        }
     1796
     1797        /**
     1798         * Get the query params for collections of attachments.
     1799         *
     1800         * @return array
     1801         */
     1802        public function get_collection_params() {
     1803                $params = parent::get_collection_params();
     1804
     1805                $params['context']['default'] = 'view';
     1806
     1807                $params['after'] = array(
     1808                        'description'        => __( 'Limit response to resources published after a given ISO8601 compliant date.' ),
     1809                        'type'               => 'string',
     1810                        'format'             => 'date-time',
     1811                        'validate_callback'  => 'rest_validate_request_arg',
     1812                );
     1813                if ( post_type_supports( $this->post_type, 'author' ) ) {
     1814                        $params['author'] = array(
     1815                                'description'         => __( 'Limit result set to posts assigned to specific authors.' ),
     1816                                'type'                => 'array',
     1817                                'default'             => array(),
     1818                                'sanitize_callback'   => 'wp_parse_id_list',
     1819                        );
     1820                        $params['author_exclude'] = array(
     1821                                'description'         => __( 'Ensure result set excludes posts assigned to specific authors.' ),
     1822                                'type'                => 'array',
     1823                                'default'             => array(),
     1824                                'sanitize_callback'   => 'wp_parse_id_list',
     1825                        );
     1826                }
     1827                $params['before'] = array(
     1828                        'description'        => __( 'Limit response to resources published before a given ISO8601 compliant date.' ),
     1829                        'type'               => 'string',
     1830                        'format'             => 'date-time',
     1831                        'validate_callback'  => 'rest_validate_request_arg',
     1832                );
     1833                $params['exclude'] = array(
     1834                        'description'        => __( 'Ensure result set excludes specific ids.' ),
     1835                        'type'               => 'array',
     1836                        'default'            => array(),
     1837                        'sanitize_callback'  => 'wp_parse_id_list',
     1838                );
     1839                $params['include'] = array(
     1840                        'description'        => __( 'Limit result set to specific ids.' ),
     1841                        'type'               => 'array',
     1842                        'default'            => array(),
     1843                        'sanitize_callback'  => 'wp_parse_id_list',
     1844                );
     1845                if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
     1846                        $params['menu_order'] = array(
     1847                                'description'        => __( 'Limit result set to resources with a specific menu_order value.' ),
     1848                                'type'               => 'integer',
     1849                                'sanitize_callback'  => 'absint',
     1850                                'validate_callback'  => 'rest_validate_request_arg',
     1851                        );
     1852                }
     1853                $params['offset'] = array(
     1854                        'description'        => __( 'Offset the result set by a specific number of items.' ),
     1855                        'type'               => 'integer',
     1856                        'sanitize_callback'  => 'absint',
     1857                        'validate_callback'  => 'rest_validate_request_arg',
     1858                );
     1859                $params['order'] = array(
     1860                        'description'        => __( 'Order sort attribute ascending or descending.' ),
     1861                        'type'               => 'string',
     1862                        'default'            => 'desc',
     1863                        'enum'               => array( 'asc', 'desc' ),
     1864                        'validate_callback'  => 'rest_validate_request_arg',
     1865                );
     1866                $params['orderby'] = array(
     1867                        'description'        => __( 'Sort collection by object attribute.' ),
     1868                        'type'               => 'string',
     1869                        'default'            => 'date',
     1870                        'enum'               => array(
     1871                                'date',
     1872                                'relevance',
     1873                                'id',
     1874                                'include',
     1875                                'title',
     1876                                'slug',
     1877                        ),
     1878                        'validate_callback'  => 'rest_validate_request_arg',
     1879                );
     1880                if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
     1881                        $params['orderby']['enum'][] = 'menu_order';
     1882                }
     1883
     1884                $post_type_obj = get_post_type_object( $this->post_type );
     1885                if ( $post_type_obj->hierarchical || 'attachment' === $this->post_type ) {
     1886                        $params['parent'] = array(
     1887                                'description'       => __( 'Limit result set to those of particular parent ids.' ),
     1888                                'type'              => 'array',
     1889                                'sanitize_callback' => 'wp_parse_id_list',
     1890                                'default'           => array(),
     1891                        );
     1892                        $params['parent_exclude'] = array(
     1893                                'description'       => __( 'Limit result set to all items except those of a particular parent id.' ),
     1894                                'type'              => 'array',
     1895                                'sanitize_callback' => 'wp_parse_id_list',
     1896                                'default'           => array(),
     1897                        );
     1898                }
     1899
     1900                $params['slug'] = array(
     1901                        'description'       => __( 'Limit result set to posts with a specific slug.' ),
     1902                        'type'              => 'string',
     1903                        'validate_callback' => 'rest_validate_request_arg',
     1904                );
     1905                $params['status'] = array(
     1906                        'default'           => 'publish',
     1907                        'description'       => __( 'Limit result set to posts assigned a specific status; can be comma-delimited list of status types.' ),
     1908                        'enum'              => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
     1909                        'sanitize_callback' => 'sanitize_key',
     1910                        'type'              => 'string',
     1911                        'validate_callback' => array( $this, 'validate_user_can_query_private_statuses' ),
     1912                );
     1913                $params['filter'] = array(
     1914                        'description'       => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.' ),
     1915                );
     1916
     1917                $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     1918                foreach ( $taxonomies as $taxonomy ) {
     1919                        $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     1920
     1921                        $params[ $base ] = array(
     1922                                'description'       => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
     1923                                'type'              => 'array',
     1924                                'sanitize_callback' => 'wp_parse_id_list',
     1925                                'default'           => array(),
     1926                        );
     1927                }
     1928
     1929                if ( 'post' === $this->post_type ) {
     1930                        $params['sticky'] = array(
     1931                                'description'       => __( 'Limit result set to items that are sticky.' ),
     1932                                'type'              => 'boolean',
     1933                                'sanitize_callback' => 'rest_parse_request_arg',
     1934                        );
     1935                }
     1936
     1937                return $params;
     1938        }
     1939
     1940        /**
     1941         * Validate whether the user can query private statuses.
     1942         *
     1943         * @param  mixed           $value     Post status.
     1944         * @param  WP_REST_Request $request   Full details about the request.
     1945         * @param  string          $parameter
     1946         * @return WP_Error|boolean
     1947         */
     1948        public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
     1949                if ( 'publish' === $value ) {
     1950                        return true;
     1951                }
     1952                $post_type_obj = get_post_type_object( $this->post_type );
     1953                if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
     1954                        return true;
     1955                }
     1956                return new WP_Error( 'rest_forbidden_status', __( 'Status is forbidden.' ), array( 'status' => rest_authorization_required_code() ) );
     1957        }
     1958}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3class WP_REST_Revisions_Controller extends WP_REST_Controller {
     4
     5        private $parent_post_type;
     6        private $parent_controller;
     7        private $parent_base;
     8
     9        public function __construct( $parent_post_type ) {
     10                $this->parent_post_type = $parent_post_type;
     11                $this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
     12                $this->namespace = 'wp/v2';
     13                $this->rest_base = 'revisions';
     14                $post_type_object = get_post_type_object( $parent_post_type );
     15                $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
     16        }
     17
     18        /**
     19         * Register routes for revisions based on post types supporting revisions
     20         *
     21         * @access public
     22         */
     23        public function register_routes() {
     24
     25                register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base, array(
     26                        array(
     27                                'methods'         => WP_REST_Server::READABLE,
     28                                'callback'        => array( $this, 'get_items' ),
     29                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     30                                'args'            => $this->get_collection_params(),
     31                        ),
     32                        'schema' => array( $this, 'get_public_item_schema' ),
     33                ) );
     34
     35                register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
     36                        array(
     37                                'methods'         => WP_REST_Server::READABLE,
     38                                'callback'        => array( $this, 'get_item' ),
     39                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     40                                'args'            => array(
     41                                        'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
     42                                ),
     43                        ),
     44                        array(
     45                                'methods'         => WP_REST_Server::DELETABLE,
     46                                'callback'        => array( $this, 'delete_item' ),
     47                                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
     48                        ),
     49                        'schema' => array( $this, 'get_public_item_schema' ),
     50                ));
     51
     52        }
     53
     54        /**
     55         * Check if a given request has access to get revisions
     56         *
     57         * @access public
     58         *
     59         * @param WP_REST_Request $request Full data about the request.
     60         * @return WP_Error|boolean
     61         */
     62        public function get_items_permissions_check( $request ) {
     63
     64                $parent = $this->get_post( $request['parent'] );
     65                if ( ! $parent ) {
     66                        return true;
     67                }
     68                $parent_post_type_obj = get_post_type_object( $parent->post_type );
     69                if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
     70                        return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
     71                }
     72
     73                return true;
     74        }
     75
     76        /**
     77         * Get a collection of revisions
     78         *
     79         * @access public
     80         *
     81         * @param WP_REST_Request $request Full data about the request.
     82         * @return WP_Error|WP_REST_Response
     83         */
     84        public function get_items( $request ) {
     85
     86                $parent = $this->get_post( $request['parent'] );
     87                if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
     88                        return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
     89                }
     90
     91                $revisions = wp_get_post_revisions( $request['parent'] );
     92
     93                $response = array();
     94                foreach ( $revisions as $revision ) {
     95                        $data = $this->prepare_item_for_response( $revision, $request );
     96                        $response[] = $this->prepare_response_for_collection( $data );
     97                }
     98                return rest_ensure_response( $response );
     99        }
     100
     101        /**
     102         * Check if a given request has access to get a specific revision
     103         *
     104         * @access public
     105         *
     106         * @param WP_REST_Request $request Full data about the request.
     107         * @return WP_Error|boolean
     108         */
     109        public function get_item_permissions_check( $request ) {
     110                return $this->get_items_permissions_check( $request );
     111        }
     112
     113        /**
     114         * Get one revision from the collection
     115         *
     116         * @access public
     117         *
     118         * @param WP_REST_Request $request Full data about the request.
     119         * @return WP_Error|array
     120         */
     121        public function get_item( $request ) {
     122
     123                $parent = $this->get_post( $request['parent'] );
     124                if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
     125                        return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
     126                }
     127
     128                $revision = $this->get_post( $request['id'] );
     129                if ( ! $revision || 'revision' !== $revision->post_type ) {
     130                        return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
     131                }
     132
     133                $response = $this->prepare_item_for_response( $revision, $request );
     134                return rest_ensure_response( $response );
     135        }
     136
     137        /**
     138         * Check if a given request has access to delete a revision
     139         *
     140         * @access public
     141         *
     142         * @param  WP_REST_Request $request Full details about the request.
     143         * @return WP_Error|boolean
     144         */
     145        public function delete_item_permissions_check( $request ) {
     146
     147                $response = $this->get_items_permissions_check( $request );
     148                if ( ! $response || is_wp_error( $response ) ) {
     149                        return $response;
     150                }
     151
     152                $post = $this->get_post( $request['id'] );
     153                if ( ! $post ) {
     154                        return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
     155                }
     156                $post_type = get_post_type_object( 'revision' );
     157                return current_user_can( $post_type->cap->delete_post, $post->ID );
     158        }
     159
     160        /**
     161         * Delete a single revision
     162         *
     163         * @access public
     164         *
     165         * @param WP_REST_Request $request Full details about the request.
     166         * @return WP_Error|boolean
     167         */
     168        public function delete_item( $request ) {
     169                $result = wp_delete_post( $request['id'], true );
     170
     171                /**
     172                 * Fires after a revision is deleted via the REST API.
     173                 *
     174                 * @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
     175                 *                        or false (failure). If the revision was moved to to the trash, $result represents
     176                 *                        its new state; if it was deleted, $result represents its state before deletion.
     177                 * @param WP_REST_Request $request The request sent to the API.
     178                 */
     179                do_action( 'rest_delete_revision', $result, $request );
     180
     181                if ( $result ) {
     182                        return true;
     183                } else {
     184                        return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
     185                }
     186        }
     187
     188        /**
     189         * Prepare the revision for the REST response
     190         *
     191         * @access public
     192         *
     193         * @param WP_Post         $post    Post revision object.
     194         * @param WP_REST_Request $request Request object.
     195         * @return WP_REST_Response $response
     196         */
     197        public function prepare_item_for_response( $post, $request ) {
     198
     199                $schema = $this->get_item_schema();
     200
     201                $data = array();
     202
     203                if ( ! empty( $schema['properties']['author'] ) ) {
     204                        $data['author'] = $post->post_author;
     205                }
     206
     207                if ( ! empty( $schema['properties']['date'] ) ) {
     208                        $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
     209                }
     210
     211                if ( ! empty( $schema['properties']['date_gmt'] ) ) {
     212                        $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
     213                }
     214
     215                if ( ! empty( $schema['properties']['id'] ) ) {
     216                        $data['id'] = $post->ID;
     217                }
     218
     219                if ( ! empty( $schema['properties']['modified'] ) ) {
     220                        $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
     221                }
     222
     223                if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
     224                        $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
     225                }
     226
     227                if ( ! empty( $schema['properties']['parent'] ) ) {
     228                        $data['parent'] = (int) $post->post_parent;
     229                }
     230
     231                if ( ! empty( $schema['properties']['slug'] ) ) {
     232                        $data['slug'] = $post->post_name;
     233                }
     234
     235                if ( ! empty( $schema['properties']['guid'] ) ) {
     236                        $data['guid'] = array(
     237                                /** This filter is documented in wp-includes/post-template.php */
     238                                'rendered' => apply_filters( 'get_the_guid', $post->guid ),
     239                                'raw'      => $post->guid,
     240                        );
     241                }
     242
     243                if ( ! empty( $schema['properties']['title'] ) ) {
     244                        $data['title'] = array(
     245                                'raw'      => $post->post_title,
     246                                'rendered' => get_the_title( $post->ID ),
     247                        );
     248                }
     249
     250                if ( ! empty( $schema['properties']['content'] ) ) {
     251
     252                        $data['content'] = array(
     253                                'raw'      => $post->post_content,
     254                                /** This filter is documented in wp-includes/post-template.php */
     255                                'rendered' => apply_filters( 'the_content', $post->post_content ),
     256                        );
     257                }
     258
     259                if ( ! empty( $schema['properties']['excerpt'] ) ) {
     260                        $data['excerpt'] = array(
     261                                'raw'      => $post->post_excerpt,
     262                                'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
     263                        );
     264                }
     265
     266                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     267                $data = $this->add_additional_fields_to_object( $data, $request );
     268                $data = $this->filter_response_by_context( $data, $context );
     269                $response = rest_ensure_response( $data );
     270
     271                if ( ! empty( $data['parent'] ) ) {
     272                        $response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
     273                }
     274
     275                /**
     276                 * Filter a revision returned from the API.
     277                 *
     278                 * Allows modification of the revision right before it is returned.
     279                 *
     280                 * @param WP_REST_Response  $response   The response object.
     281                 * @param WP_Post           $post       The original revision object.
     282                 * @param WP_REST_Request   $request    Request used to generate the response.
     283                 */
     284                return apply_filters( 'rest_prepare_revision', $response, $post, $request );
     285        }
     286
     287        /**
     288         * Check the post_date_gmt or modified_gmt and prepare any post or
     289         * modified date for single post output.
     290         *
     291         * @access protected
     292         *
     293         * @param string      $date_gmt GMT publication time.
     294         * @param string|null $date     Optional, default is null. Local publication time.
     295         * @return string|null ISO8601/RFC3339 formatted datetime.
     296         */
     297        protected function prepare_date_response( $date_gmt, $date = null ) {
     298                if ( '0000-00-00 00:00:00' === $date_gmt ) {
     299                        return null;
     300                }
     301
     302                if ( isset( $date ) ) {
     303                        return mysql_to_rfc3339( $date );
     304                }
     305
     306                return mysql_to_rfc3339( $date_gmt );
     307        }
     308
     309        /**
     310         * Get the revision's schema, conforming to JSON Schema
     311         *
     312         * @access public
     313         *
     314         * @return array
     315         */
     316        public function get_item_schema() {
     317                $schema = array(
     318                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     319                        'title'      => "{$this->parent_post_type}-revision",
     320                        'type'       => 'object',
     321                        /*
     322                         * Base properties for every Revision
     323                         */
     324                        'properties' => array(
     325                                'author'          => array(
     326                                        'description' => __( 'The id for the author of the object.' ),
     327                                        'type'        => 'integer',
     328                                        'context'     => array( 'view', 'edit', 'embed' ),
     329                                ),
     330                                'date'            => array(
     331                                        'description' => __( 'The date the object was published.' ),
     332                                        'type'        => 'string',
     333                                        'format'      => 'date-time',
     334                                        'context'     => array( 'view', 'edit', 'embed' ),
     335                                ),
     336                                'date_gmt'        => array(
     337                                        'description' => __( 'The date the object was published, as GMT.' ),
     338                                        'type'        => 'string',
     339                                        'format'      => 'date-time',
     340                                        'context'     => array( 'view', 'edit' ),
     341                                ),
     342                                'guid'            => array(
     343                                        'description' => __( 'GUID for the object, as it exists in the database.' ),
     344                                        'type'        => 'string',
     345                                        'context'     => array( 'view', 'edit' ),
     346                                ),
     347                                'id'              => array(
     348                                        'description' => __( 'Unique identifier for the object.' ),
     349                                        'type'        => 'integer',
     350                                        'context'     => array( 'view', 'edit', 'embed' ),
     351                                ),
     352                                'modified'        => array(
     353                                        'description' => __( 'The date the object was last modified.' ),
     354                                        'type'        => 'string',
     355                                        'format'      => 'date-time',
     356                                        'context'     => array( 'view', 'edit' ),
     357                                ),
     358                                'modified_gmt'    => array(
     359                                        'description' => __( 'The date the object was last modified, as GMT.' ),
     360                                        'type'        => 'string',
     361                                        'format'      => 'date-time',
     362                                        'context'     => array( 'view', 'edit' ),
     363                                ),
     364                                'parent'          => array(
     365                                        'description' => __( 'The id for the parent of the object.' ),
     366                                        'type'        => 'integer',
     367                                        'context'     => array( 'view', 'edit', 'embed' ),
     368                                        ),
     369                                'slug'            => array(
     370                                        'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
     371                                        'type'        => 'string',
     372                                        'context'     => array( 'view', 'edit', 'embed' ),
     373                                ),
     374                        ),
     375                );
     376
     377                $parent_schema = $this->parent_controller->get_item_schema();
     378
     379                if ( ! empty( $parent_schema['properties']['title'] ) ) {
     380                        $schema['properties']['title'] = $parent_schema['properties']['title'];
     381                }
     382                if ( ! empty( $parent_schema['properties']['content'] ) ) {
     383                        $schema['properties']['content'] = $parent_schema['properties']['content'];
     384                }
     385                if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
     386                        $schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
     387                }
     388                if ( ! empty( $parent_schema['properties']['guid'] ) ) {
     389                        $schema['properties']['guid'] = $parent_schema['properties']['guid'];
     390                }
     391
     392                return $this->add_additional_fields_schema( $schema );
     393        }
     394
     395        /**
     396         * Get the query params for collections
     397         *
     398         * @access public
     399         *
     400         * @return array
     401         */
     402        public function get_collection_params() {
     403                return array(
     404                        'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     405                );
     406        }
     407
     408        /**
     409         * Check the post excerpt and prepare it for single post output.
     410         *
     411         * @access protected
     412         *
     413         * @param string  $excerpt The post excerpt.
     414         * @param WP_Post $post    Post revision object.
     415         * @return string|null $excerpt
     416         */
     417        protected function prepare_excerpt_response( $excerpt, $post ) {
     418
     419                /** This filter is documented in wp-includes/post-template.php */
     420                $excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
     421
     422                if ( empty( $excerpt ) ) {
     423                        return '';
     424                }
     425
     426                return $excerpt;
     427        }
     428}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3/**
     4 * Manage a WordPress site's settings.
     5 */
     6
     7class WP_REST_Settings_Controller extends WP_REST_Controller {
     8
     9        protected $rest_base = 'settings';
     10        protected $namespace = 'wp/v2';
     11
     12        /**
     13         * Register the routes for the objects of the controller.
     14         */
     15        public function register_routes() {
     16                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     17                        array(
     18                                'methods'             => WP_REST_Server::READABLE,
     19                                'callback'            => array( $this, 'get_item' ),
     20                                'args'                => array(),
     21                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     22                        ),
     23                        array(
     24                                'methods'             => WP_REST_Server::EDITABLE,
     25                                'callback'            => array( $this, 'update_item' ),
     26                                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
     27                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     28                        ),
     29                        'schema' => array( $this, 'get_public_item_schema' ),
     30                ) );
     31        }
     32
     33        /**
     34         * Check if a given request has access to read and manage settings.
     35         *
     36         * @param  WP_REST_Request $request Full details about the request.
     37         * @return boolean
     38         */
     39        public function get_item_permissions_check( $request ) {
     40                return current_user_can( 'manage_options' );
     41        }
     42
     43        /**
     44         * Get the settings.
     45         *
     46         * @param WP_REST_Request $request Full details about the request.
     47         * @return WP_Error|array
     48         */
     49        public function get_item( $request ) {
     50                $options  = $this->get_registered_options();
     51                $response = array();
     52
     53                foreach ( $options as $name => $args ) {
     54                        /**
     55                         * Filters the value of a setting recognized by the REST API.
     56                         *
     57                         * Allow hijacking the setting value and overriding the built-in behavior by returning a
     58                         * non-null value.  The returned value will be presented as the setting value instead.
     59                         *
     60                         * @since 4.7.0
     61                         *
     62                         * @param mixed  $result  Value to use for the requested setting. Can be a scalar
     63                         *                        matching the registered schema for the setting, or null to
     64                         *                        follow the default `get_option` behavior.
     65                         * @param string $name    Setting name (as shown in REST API responses).
     66                         * @param array  $args    Arguments passed to `register_setting()` for this setting.
     67                         */
     68                        $response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
     69
     70                        if ( is_null( $response[ $name ] ) ) {
     71                                // Default to a null value as "null" in the response means "not set".
     72                                $response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
     73                        }
     74
     75                        // Because get_option() is lossy, we have to
     76                        // cast values to the type they are registered with.
     77                        $response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
     78                }
     79
     80                return $response;
     81        }
     82
     83        /**
     84         * Prepare a value for output based off a schema array.
     85         *
     86         * @param  mixed $value
     87         * @param  array $schema
     88         * @return mixed
     89         */
     90        protected function prepare_value( $value, $schema ) {
     91                switch ( $schema['type'] ) {
     92                        case 'string':
     93                                return (string) $value;
     94                        case 'number':
     95                                return (float) $value;
     96                        case 'boolean':
     97                                return (bool) $value;
     98                        default:
     99                                return null;
     100                }
     101        }
     102
     103        /**
     104         * Update settings for the settings object.
     105         *
     106         * @param  WP_REST_Request $request Full detail about the request.
     107         * @return WP_Error|array
     108         */
     109        public function update_item( $request ) {
     110                $options = $this->get_registered_options();
     111                $params = $request->get_params();
     112
     113                foreach ( $options as $name => $args ) {
     114                        if ( ! array_key_exists( $name, $params ) ) {
     115                                continue;
     116                        }
     117
     118                        /**
     119                         * Filters whether to preempt a setting value update.
     120                         *
     121                         * Allow hijacking the setting update logic and overriding the built-in behavior by
     122                         * returning true.
     123                         *
     124                         * @since 4.7.0
     125                         *
     126                         * @param boolean $result Whether to override the default behavior for updating the
     127                         *                        value of a setting.
     128                         * @param string  $name   Setting name (as shown in REST API responses).
     129                         * @param mixed   $value  Updated setting value.
     130                         * @param array   $args   Arguments passed to `register_setting()` for this setting.
     131                         */
     132                        $updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
     133                        if ( $updated ) {
     134                                continue;
     135                        }
     136
     137                        // A null value means reset the option, which is essentially deleting it
     138                        // from the database and then relying on the default value.
     139                        if ( is_null( $request[ $name ] ) ) {
     140                                delete_option( $args['option_name'] );
     141                        } else {
     142                                update_option( $args['option_name'], $request[ $name ] );
     143                        }
     144                }
     145
     146                return $this->get_item( $request );
     147        }
     148
     149        /**
     150         * Get all the registered options for the Settings API
     151         *
     152         * @return array
     153         */
     154        protected function get_registered_options() {
     155                $rest_options = array();
     156
     157                foreach ( get_registered_settings() as $name => $args ) {
     158                        if ( empty( $args['show_in_rest'] ) ) {
     159                                continue;
     160                        }
     161
     162                        $rest_args = array();
     163                        if ( is_array( $args['show_in_rest'] ) ) {
     164                                $rest_args = $args['show_in_rest'];
     165                        }
     166
     167                        $defaults = array(
     168                                'name'   => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
     169                                'schema' => array(),
     170                        );
     171                        $rest_args = array_merge( $defaults, $rest_args );
     172
     173                        $default_schema = array(
     174                                'type'        => empty( $args['type'] ) ? null : $args['type'],
     175                                'description' => empty( $args['description'] ) ? '' : $args['description'],
     176                                'default'     => isset( $args['default'] ) ? $args['default'] : null,
     177                        );
     178
     179                        $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
     180                        $rest_args['option_name'] = $name;
     181
     182                        // Skip over settings that don't have a defined type in the schema.
     183                        if ( empty( $rest_args['schema']['type'] ) ) {
     184                                continue;
     185                        }
     186
     187                        // Whitelist the supported types for settings, as we don't want invalid types
     188                        // to be updated with arbitrary values that we can't do decent sanitizing for.
     189                        if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'string', 'boolean' ), true ) ) {
     190                                continue;
     191                        }
     192
     193                        $rest_options[ $rest_args['name'] ] = $rest_args;
     194                }
     195
     196                return $rest_options;
     197        }
     198
     199        /**
     200         * Get the site setting schema, conforming to JSON Schema.
     201         *
     202         * @return array
     203         */
     204        public function get_item_schema() {
     205                $options = $this->get_registered_options();
     206
     207                $schema = array(
     208                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     209                        'title'      => 'settings',
     210                        'type'       => 'object',
     211                        'properties' => array(),
     212                );
     213
     214                foreach ( $options as $option_name => $option ) {
     215                        $schema['properties'][ $option_name ] = $option['schema'];
     216                }
     217
     218                return $this->add_additional_fields_schema( $schema );
     219        }
     220}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php

     
     1<?php
     2
     3class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
     4
     5        public function __construct() {
     6                $this->namespace = 'wp/v2';
     7                $this->rest_base = 'taxonomies';
     8        }
     9
     10        /**
     11         * Register the routes for the objects of the controller.
     12         */
     13        public function register_routes() {
     14
     15                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     16                        array(
     17                                'methods'         => WP_REST_Server::READABLE,
     18                                'callback'        => array( $this, 'get_items' ),
     19                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     20                                'args'            => $this->get_collection_params(),
     21                        ),
     22                        'schema' => array( $this, 'get_public_item_schema' ),
     23                ) );
     24
     25                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<taxonomy>[\w-]+)', array(
     26                        array(
     27                                'methods'         => WP_REST_Server::READABLE,
     28                                'callback'        => array( $this, 'get_item' ),
     29                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     30                                'args'            => array(
     31                                        'context'     => $this->get_context_param( array( 'default' => 'view' ) ),
     32                                ),
     33                        ),
     34                        'schema' => array( $this, 'get_public_item_schema' ),
     35                ) );
     36        }
     37
     38        /**
     39         * Check whether a given request has permission to read taxonomies.
     40         *
     41         * @param  WP_REST_Request $request Full details about the request.
     42         * @return WP_Error|boolean
     43         */
     44        public function get_items_permissions_check( $request ) {
     45                if ( 'edit' === $request['context'] ) {
     46                        if ( ! empty( $request['type'] ) ) {
     47                                $taxonomies = get_object_taxonomies( $request['type'], 'objects' );
     48                        } else {
     49                                $taxonomies = get_taxonomies( '', 'objects' );
     50                        }
     51                        foreach ( $taxonomies as $taxonomy ) {
     52                                if ( ! empty( $taxonomy->show_in_rest ) && current_user_can( $taxonomy->cap->manage_terms ) ) {
     53                                        return true;
     54                                }
     55                        }
     56                        return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     57                }
     58                return true;
     59        }
     60
     61        /**
     62         * Get all public taxonomies
     63         *
     64         * @param WP_REST_Request $request
     65         * @return array
     66         */
     67        public function get_items( $request ) {
     68
     69                // Retrieve the list of registered collection query parameters.
     70                $registered = $this->get_collection_params();
     71
     72                if ( isset( $registered['type'] ) && ! empty( $request['type'] ) ) {
     73                        $taxonomies = get_object_taxonomies( $request['type'], 'objects' );
     74                } else {
     75                        $taxonomies = get_taxonomies( '', 'objects' );
     76                }
     77                $data = array();
     78                foreach ( $taxonomies as $tax_type => $value ) {
     79                        if ( empty( $value->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $value->cap->manage_terms ) ) ) {
     80                                continue;
     81                        }
     82                        $tax = $this->prepare_item_for_response( $value, $request );
     83                        $tax = $this->prepare_response_for_collection( $tax );
     84                        $data[ $tax_type ] = $tax;
     85                }
     86
     87                if ( empty( $data ) ) {
     88                        // Response should still be returned as a JSON object when it is empty.
     89                        $data = (object) $data;
     90                }
     91
     92                return rest_ensure_response( $data );
     93        }
     94
     95        /**
     96         * Check if a given request has access a taxonomy
     97         *
     98         * @param  WP_REST_Request $request Full details about the request.
     99         * @return WP_Error|boolean
     100         */
     101        public function get_item_permissions_check( $request ) {
     102
     103                $tax_obj = get_taxonomy( $request['taxonomy'] );
     104
     105                if ( $tax_obj ) {
     106                        if ( empty( $tax_obj->show_in_rest ) ) {
     107                                return false;
     108                        }
     109                        if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->manage_terms ) ) {
     110                                return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this resource.' ), array( 'status' => rest_authorization_required_code() ) );
     111                        }
     112                }
     113
     114                return true;
     115        }
     116
     117        /**
     118         * Get a specific taxonomy
     119         *
     120         * @param WP_REST_Request $request
     121         * @return array|WP_Error
     122         */
     123        public function get_item( $request ) {
     124                $tax_obj = get_taxonomy( $request['taxonomy'] );
     125                if ( empty( $tax_obj ) ) {
     126                        return new WP_Error( 'rest_taxonomy_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
     127                }
     128                $data = $this->prepare_item_for_response( $tax_obj, $request );
     129                return rest_ensure_response( $data );
     130        }
     131
     132        /**
     133         * Prepare a taxonomy object for serialization
     134         *
     135         * @param stdClass $taxonomy Taxonomy data
     136         * @param WP_REST_Request $request
     137         * @return WP_REST_Response $response
     138         */
     139        public function prepare_item_for_response( $taxonomy, $request ) {
     140
     141                $data = array(
     142                        'name'         => $taxonomy->label,
     143                        'slug'         => $taxonomy->name,
     144                        'capabilities' => $taxonomy->cap,
     145                        'description'  => $taxonomy->description,
     146                        'labels'       => $taxonomy->labels,
     147                        'types'        => $taxonomy->object_type,
     148                        'show_cloud'   => $taxonomy->show_tagcloud,
     149                        'hierarchical' => $taxonomy->hierarchical,
     150                );
     151
     152                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     153                $data = $this->add_additional_fields_to_object( $data, $request );
     154                $data = $this->filter_response_by_context( $data, $context );
     155
     156                // Wrap the data in a response object.
     157                $response = rest_ensure_response( $data );
     158
     159                $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     160                $response->add_links( array(
     161                        'collection'                => array(
     162                                'href'                  => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
     163                        ),
     164                        'https://api.w.org/items'   => array(
     165                                'href'                  => rest_url( sprintf( 'wp/v2/%s', $base ) ),
     166                        ),
     167                ) );
     168
     169                /**
     170                 * Filter a taxonomy returned from the API.
     171                 *
     172                 * Allows modification of the taxonomy data right before it is returned.
     173                 *
     174                 * @param WP_REST_Response  $response   The response object.
     175                 * @param object            $item       The original taxonomy object.
     176                 * @param WP_REST_Request   $request    Request used to generate the response.
     177                 */
     178                return apply_filters( 'rest_prepare_taxonomy', $response, $taxonomy, $request );
     179        }
     180
     181        /**
     182         * Get the taxonomy's schema, conforming to JSON Schema
     183         *
     184         * @return array
     185         */
     186        public function get_item_schema() {
     187                $schema = array(
     188                        '$schema'              => 'http://json-schema.org/draft-04/schema#',
     189                        'title'                => 'taxonomy',
     190                        'type'                 => 'object',
     191                        'properties'           => array(
     192                                'capabilities'     => array(
     193                                        'description'  => __( 'All capabilities used by the resource.' ),
     194                                        'type'         => 'array',
     195                                        'context'      => array( 'edit' ),
     196                                        'readonly'     => true,
     197                                ),
     198                                'description'      => array(
     199                                        'description'  => __( 'A human-readable description of the resource.' ),
     200                                        'type'         => 'string',
     201                                        'context'      => array( 'view', 'edit' ),
     202                                        'readonly'     => true,
     203                                ),
     204                                'hierarchical'     => array(
     205                                        'description'  => __( 'Whether or not the resource should have children.' ),
     206                                        'type'         => 'boolean',
     207                                        'context'      => array( 'view', 'edit' ),
     208                                        'readonly'     => true,
     209                                ),
     210                                'labels'           => array(
     211                                        'description'  => __( 'Human-readable labels for the resource for various contexts.' ),
     212                                        'type'         => 'object',
     213                                        'context'      => array( 'edit' ),
     214                                        'readonly'     => true,
     215                                ),
     216                                'name'             => array(
     217                                        'description'  => __( 'The title for the resource.' ),
     218                                        'type'         => 'string',
     219                                        'context'      => array( 'view', 'edit', 'embed' ),
     220                                        'readonly'     => true,
     221                                ),
     222                                'slug'             => array(
     223                                        'description'  => __( 'An alphanumeric identifier for the resource.' ),
     224                                        'type'         => 'string',
     225                                        'context'      => array( 'view', 'edit', 'embed' ),
     226                                        'readonly'     => true,
     227                                ),
     228                                'show_cloud'       => array(
     229                                        'description'  => __( 'Whether or not the term cloud should be displayed.' ),
     230                                        'type'         => 'boolean',
     231                                        'context'      => array( 'edit' ),
     232                                        'readonly'     => true,
     233                                ),
     234                                'types'            => array(
     235                                        'description'  => __( 'Types associated with resource.' ),
     236                                        'type'         => 'array',
     237                                        'context'      => array( 'view', 'edit' ),
     238                                        'readonly'     => true,
     239                                ),
     240                        ),
     241                );
     242                return $this->add_additional_fields_schema( $schema );
     243        }
     244
     245        /**
     246         * Get the query params for collections
     247         *
     248         * @return array
     249         */
     250        public function get_collection_params() {
     251                $new_params = array();
     252                $new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
     253                $new_params['type'] = array(
     254                        'description'  => __( 'Limit results to resources associated with a specific post type.' ),
     255                        'type'         => 'string',
     256                        'validate_callback' => 'rest_validate_request_arg',
     257                );
     258                return $new_params;
     259        }
     260
     261}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3/**
     4 * Access terms associated with a taxonomy.
     5 */
     6class WP_REST_Terms_Controller extends WP_REST_Controller {
     7
     8        /**
     9         * Taxonomy key.
     10         *
     11         * @access protected
     12         * @var string
     13         */
     14        protected $taxonomy;
     15
     16        /**
     17         * Instance of a term meta fields object.
     18         *
     19         * @access protected
     20         * @var WP_REST_Term_Meta_Fields
     21         */
     22        protected $meta;
     23
     24        /**
     25         * Column to have the terms be sorted by.
     26         *
     27         * @access protected
     28         * @var string
     29         */
     30        protected $sort_column;
     31
     32        /**
     33         * Number of terms that were found.
     34         *
     35         * @access protected
     36         * @var int
     37         */
     38        protected $total_terms;
     39
     40        /**
     41         * Constructor.
     42         *
     43         * @param string $taxonomy Taxonomy key.
     44         */
     45        public function __construct( $taxonomy ) {
     46                $this->taxonomy = $taxonomy;
     47                $this->namespace = 'wp/v2';
     48                $tax_obj = get_taxonomy( $taxonomy );
     49                $this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name;
     50
     51                $this->meta = new WP_REST_Term_Meta_Fields( $taxonomy );
     52        }
     53
     54        /**
     55         * Registers the routes for the objects of the controller.
     56         */
     57        public function register_routes() {
     58
     59                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     60                        array(
     61                                'methods'             => WP_REST_Server::READABLE,
     62                                'callback'            => array( $this, 'get_items' ),
     63                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     64                                'args'                => $this->get_collection_params(),
     65                        ),
     66                        array(
     67                                'methods'             => WP_REST_Server::CREATABLE,
     68                                'callback'            => array( $this, 'create_item' ),
     69                                'permission_callback' => array( $this, 'create_item_permissions_check' ),
     70                                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
     71                        ),
     72                        'schema' => array( $this, 'get_public_item_schema' ),
     73                ) );
     74                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
     75                        array(
     76                                'methods'             => WP_REST_Server::READABLE,
     77                                'callback'            => array( $this, 'get_item' ),
     78                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     79                                'args'                => array(
     80                                        'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     81                                ),
     82                        ),
     83                        array(
     84                                'methods'             => WP_REST_Server::EDITABLE,
     85                                'callback'            => array( $this, 'update_item' ),
     86                                'permission_callback' => array( $this, 'update_item_permissions_check' ),
     87                                'args'                 => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
     88                        ),
     89                        array(
     90                                'methods'             => WP_REST_Server::DELETABLE,
     91                                'callback'            => array( $this, 'delete_item' ),
     92                                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
     93                                'args'                => array(
     94                                        'force' => array(
     95                                                'default'     => false,
     96                                                'description' => __( 'Required to be true, as resource does not support trashing.' ),
     97                                        ),
     98                                ),
     99                        ),
     100                        'schema' => array( $this, 'get_public_item_schema' ),
     101                ) );
     102        }
     103
     104        /**
     105         * Checks if a request has access to read terms in the specified taxonomy.
     106         *
     107         * @param  WP_REST_Request $request Full details about the request.
     108         * @return WP_Error|boolean
     109         */
     110        public function get_items_permissions_check( $request ) {
     111                $tax_obj = get_taxonomy( $this->taxonomy );
     112                if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
     113                        return false;
     114                }
     115                if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
     116                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     117                }
     118                return true;
     119        }
     120
     121        /**
     122         * Gets terms associated with a taxonomy.
     123         *
     124         * @param WP_REST_Request $request Full details about the request.
     125         * @return WP_REST_Response|WP_Error
     126         */
     127        public function get_items( $request ) {
     128
     129                // Retrieve the list of registered collection query parameters.
     130                $registered = $this->get_collection_params();
     131
     132                // This array defines mappings between public API query parameters whose
     133                // values are accepted as-passed, and their internal WP_Query parameter
     134                // name equivalents (some are the same). Only values which are also
     135                // present in $registered will be set.
     136                $parameter_mappings = array(
     137                        'exclude'    => 'exclude',
     138                        'include'    => 'include',
     139                        'order'      => 'order',
     140                        'orderby'    => 'orderby',
     141                        'post'       => 'post',
     142                        'hide_empty' => 'hide_empty',
     143                        'per_page'   => 'number',
     144                        'search'     => 'search',
     145                        'slug'       => 'slug',
     146                );
     147
     148                $prepared_args = array();
     149
     150                // For each known parameter which is both registered and present in the request,
     151                // set the parameter's value on the query $prepared_args.
     152                foreach ( $parameter_mappings as $api_param => $wp_param ) {
     153                        if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
     154                                $prepared_args[ $wp_param ] = $request[ $api_param ];
     155                        }
     156                }
     157
     158                if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
     159                        $prepared_args['offset'] = $request['offset'];
     160                } else {
     161                        $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
     162                }
     163
     164                $taxonomy_obj = get_taxonomy( $this->taxonomy );
     165
     166                if ( $taxonomy_obj->hierarchical && isset( $registered['parent'], $request['parent'] ) ) {
     167                        if ( 0 === $request['parent'] ) {
     168                                // Only query top-level terms.
     169                                $prepared_args['parent'] = 0;
     170                        } else {
     171                                if ( $request['parent'] ) {
     172                                        $prepared_args['parent'] = $request['parent'];
     173                                }
     174                        }
     175                }
     176
     177                /**
     178                 * Filters the query arguments before passing them to get_terms().
     179                 *
     180                 * Enables adding extra arguments or setting defaults for a terms
     181                 * collection request.
     182                 *
     183                 * @see https://developer.wordpress.org/reference/functions/get_terms/
     184                 *
     185                 * @param array           $prepared_args Array of arguments to be
     186                 *                                       passed to get_terms().
     187                 * @param WP_REST_Request $request       The current request.
     188                 */
     189                $prepared_args = apply_filters( "rest_{$this->taxonomy}_query", $prepared_args, $request );
     190
     191                if ( ! empty( $prepared_args['post'] )  ) {
     192                        $query_result = $this->get_terms_for_post( $prepared_args );
     193                        $total_terms = $this->total_terms;
     194                } else {
     195                        $query_result = get_terms( $this->taxonomy, $prepared_args );
     196
     197                        $count_args = $prepared_args;
     198                        unset( $count_args['number'], $count_args['offset'] );
     199                        $total_terms = wp_count_terms( $this->taxonomy, $count_args );
     200
     201                        // wp_count_terms can return a falsy value when the term has no children
     202                        if ( ! $total_terms ) {
     203                                $total_terms = 0;
     204                        }
     205                }
     206                $response = array();
     207                foreach ( $query_result as $term ) {
     208                        $data = $this->prepare_item_for_response( $term, $request );
     209                        $response[] = $this->prepare_response_for_collection( $data );
     210                }
     211
     212                $response = rest_ensure_response( $response );
     213
     214                // Store pagination values for headers.
     215                $per_page = (int) $prepared_args['number'];
     216                $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
     217
     218                $response->header( 'X-WP-Total', (int) $total_terms );
     219                $max_pages = ceil( $total_terms / $per_page );
     220                $response->header( 'X-WP-TotalPages', (int) $max_pages );
     221
     222                $base = add_query_arg( $request->get_query_params(), rest_url( $this->namespace . '/' . $this->rest_base ) );
     223                if ( $page > 1 ) {
     224                        $prev_page = $page - 1;
     225                        if ( $prev_page > $max_pages ) {
     226                                $prev_page = $max_pages;
     227                        }
     228                        $prev_link = add_query_arg( 'page', $prev_page, $base );
     229                        $response->link_header( 'prev', $prev_link );
     230                }
     231                if ( $max_pages > $page ) {
     232                        $next_page = $page + 1;
     233                        $next_link = add_query_arg( 'page', $next_page, $base );
     234                        $response->link_header( 'next', $next_link );
     235                }
     236
     237                return $response;
     238        }
     239
     240        /**
     241         * Gets the terms attached to a post.
     242         *
     243         * This is an alternative to get_terms() that uses get_the_terms()
     244         * instead, which hits the object cache. There are a few things not
     245         * supported, notably `include`, `exclude`. In `self::get_items()` these
     246         * are instead treated as a full query.
     247         *
     248         * @param array $prepared_args Arguments for get_terms().
     249         * @return array List of term objects. (Total count in `$this->total_terms`)
     250         */
     251        protected function get_terms_for_post( $prepared_args ) {
     252                $query_result = get_the_terms( $prepared_args['post'], $this->taxonomy );
     253                if ( empty( $query_result ) ) {
     254                        $this->total_terms = 0;
     255                        return array();
     256                }
     257
     258                /*
     259                 * get_items() verifies that we don't have `include` set, and default
     260                 * ordering is by `name`.
     261                 */
     262                if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) {
     263                        switch ( $prepared_args['orderby'] ) {
     264                                case 'id':
     265                                        $this->sort_column = 'term_id';
     266                                        break;
     267
     268                                case 'slug':
     269                                case 'term_group':
     270                                case 'description':
     271                                case 'count':
     272                                        $this->sort_column = $prepared_args['orderby'];
     273                                        break;
     274                        }
     275                        usort( $query_result, array( $this, 'compare_terms' ) );
     276                }
     277                if ( strtolower( $prepared_args['order'] ) !== 'asc' ) {
     278                        $query_result = array_reverse( $query_result );
     279                }
     280
     281                // Pagination.
     282                $this->total_terms = count( $query_result );
     283                $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] );
     284
     285                return $query_result;
     286        }
     287
     288        /**
     289         * Comparison function for sorting terms by a column.
     290         *
     291         * Uses `$this->sort_column` to determine field to sort by.
     292         *
     293         * @access protected
     294         *
     295         * @param stdClass $left Term object.
     296         * @param stdClass $right Term object.
     297         * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left.
     298         */
     299        protected function compare_terms( $left, $right ) {
     300                $col = $this->sort_column;
     301                $left_val = $left->$col;
     302                $right_val = $right->$col;
     303
     304                if ( is_int( $left_val ) && is_int( $right_val ) ) {
     305                        return $left_val - $right_val;
     306                }
     307
     308                return strcmp( $left_val, $right_val );
     309        }
     310
     311        /**
     312         * Checks if a request has access to read the specified term.
     313         *
     314         * @param  WP_REST_Request $request Full details about the request.
     315         * @return WP_Error|boolean
     316         */
     317        public function get_item_permissions_check( $request ) {
     318                $tax_obj = get_taxonomy( $this->taxonomy );
     319                if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
     320                        return false;
     321                }
     322                if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
     323                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     324                }
     325                return true;
     326        }
     327
     328        /**
     329         * Gets a single term from a taxonomy.
     330         *
     331         * @param WP_REST_Request $request Full details about the request
     332         * @return WP_REST_Request|WP_Error
     333         */
     334        public function get_item( $request ) {
     335
     336                $term = get_term( (int) $request['id'], $this->taxonomy );
     337                if ( ! $term || $term->taxonomy !== $this->taxonomy ) {
     338                        return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
     339                }
     340                if ( is_wp_error( $term ) ) {
     341                        return $term;
     342                }
     343
     344                $response = $this->prepare_item_for_response( $term, $request );
     345
     346                return rest_ensure_response( $response );
     347        }
     348
     349        /**
     350         * Checks if a request has access to create a term.
     351         *
     352         * @param  WP_REST_Request $request Full details about the request.
     353         * @return WP_Error|boolean
     354         */
     355        public function create_item_permissions_check( $request ) {
     356
     357                if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
     358                        return false;
     359                }
     360
     361                $taxonomy_obj = get_taxonomy( $this->taxonomy );
     362                if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) {
     363                        return new WP_Error( 'rest_cannot_create', __( 'Sorry, you cannot create new resource.' ), array( 'status' => rest_authorization_required_code() ) );
     364                }
     365
     366                return true;
     367        }
     368
     369        /**
     370         * Creates a single term in a taxonomy.
     371         *
     372         * @param WP_REST_Request $request Full details about the request
     373         * @return WP_REST_Request|WP_Error
     374         */
     375        public function create_item( $request ) {
     376                if ( isset( $request['parent'] ) ) {
     377                        if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
     378                                return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
     379                        }
     380
     381                        $parent = get_term( (int) $request['parent'], $this->taxonomy );
     382
     383                        if ( ! $parent ) {
     384                                return new WP_Error( 'rest_term_invalid', __( "Parent resource doesn't exist." ), array( 'status' => 400 ) );
     385                        }
     386                }
     387
     388                $prepared_term = $this->prepare_item_for_database( $request );
     389
     390                $term = wp_insert_term( $prepared_term->name, $this->taxonomy, $prepared_term );
     391                if ( is_wp_error( $term ) ) {
     392
     393                        /*
     394                         * If we're going to inform the client that the term already exists,
     395                         * give them the identifier for future use.
     396                         */
     397                        if ( $term_id = $term->get_error_data( 'term_exists' ) ) {
     398                                $existing_term = get_term( $term_id, $this->taxonomy );
     399                                $term->add_data( $existing_term->term_id, 'term_exists' );
     400                        }
     401
     402                        return $term;
     403                }
     404
     405                $term = get_term( $term['term_id'], $this->taxonomy );
     406
     407                /**
     408                 * Fires after a single term is created or updated via the REST API.
     409                 *
     410                 * @param WP_Term         $term     Inserted Term object.
     411                 * @param WP_REST_Request $request  Request object.
     412                 * @param boolean         $creating True when creating term, false when updating.
     413                 */
     414                do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
     415
     416                $schema = $this->get_item_schema();
     417                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     418                        $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
     419                        if ( is_wp_error( $meta_update ) ) {
     420                                return $meta_update;
     421                        }
     422                }
     423
     424                $fields_update = $this->update_additional_fields_for_object( $term, $request );
     425                if ( is_wp_error( $fields_update ) ) {
     426                        return $fields_update;
     427                }
     428
     429                $request->set_param( 'context', 'view' );
     430                $response = $this->prepare_item_for_response( $term, $request );
     431                $response = rest_ensure_response( $response );
     432                $response->set_status( 201 );
     433                $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
     434                return $response;
     435        }
     436
     437        /**
     438         * Checks if a request has access to update the specified term.
     439         *
     440         * @param  WP_REST_Request $request Full details about the request.
     441         * @return WP_Error|boolean
     442         */
     443        public function update_item_permissions_check( $request ) {
     444
     445                if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
     446                        return false;
     447                }
     448
     449                $term = get_term( (int) $request['id'], $this->taxonomy );
     450                if ( ! $term ) {
     451                        return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
     452                }
     453
     454                $taxonomy_obj = get_taxonomy( $this->taxonomy );
     455                if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) {
     456                        return new WP_Error( 'rest_cannot_update', __( 'Sorry, you cannot update resource.' ), array( 'status' => rest_authorization_required_code() ) );
     457                }
     458
     459                return true;
     460        }
     461
     462        /**
     463         * Updates a single term from a taxonomy.
     464         *
     465         * @param WP_REST_Request $request Full details about the request
     466         * @return WP_REST_Request|WP_Error
     467         */
     468        public function update_item( $request ) {
     469                if ( isset( $request['parent'] ) ) {
     470                        if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
     471                                return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
     472                        }
     473
     474                        $parent = get_term( (int) $request['parent'], $this->taxonomy );
     475
     476                        if ( ! $parent ) {
     477                                return new WP_Error( 'rest_term_invalid', __( "Parent resource doesn't exist." ), array( 'status' => 400 ) );
     478                        }
     479                }
     480
     481                $prepared_term = $this->prepare_item_for_database( $request );
     482
     483                $term = get_term( (int) $request['id'], $this->taxonomy );
     484
     485                // Only update the term if we haz something to update.
     486                if ( ! empty( $prepared_term ) ) {
     487                        $update = wp_update_term( $term->term_id, $term->taxonomy, (array) $prepared_term );
     488                        if ( is_wp_error( $update ) ) {
     489                                return $update;
     490                        }
     491                }
     492
     493                $term = get_term( (int) $request['id'], $this->taxonomy );
     494
     495                /* This action is documented in lib/endpoints/class-wp-rest-terms-controller.php */
     496                do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
     497
     498                $schema = $this->get_item_schema();
     499                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     500                        $meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
     501                        if ( is_wp_error( $meta_update ) ) {
     502                                return $meta_update;
     503                        }
     504                }
     505
     506                $fields_update = $this->update_additional_fields_for_object( $term, $request );
     507                if ( is_wp_error( $fields_update ) ) {
     508                        return $fields_update;
     509                }
     510
     511                $request->set_param( 'context', 'view' );
     512                $response = $this->prepare_item_for_response( $term, $request );
     513                return rest_ensure_response( $response );
     514        }
     515
     516        /**
     517         * Checks if a request has access to delete the specified term.
     518         *
     519         * @param  WP_REST_Request $request Full details about the request.
     520         * @return WP_Error|boolean
     521         */
     522        public function delete_item_permissions_check( $request ) {
     523                if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
     524                        return false;
     525                }
     526                $term = get_term( (int) $request['id'], $this->taxonomy );
     527                if ( ! $term ) {
     528                        return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
     529                }
     530                $taxonomy_obj = get_taxonomy( $this->taxonomy );
     531                if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) {
     532                        return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you cannot delete resource.' ), array( 'status' => rest_authorization_required_code() ) );
     533                }
     534                return true;
     535        }
     536
     537        /**
     538         * Deletes a single term from a taxonomy.
     539         *
     540         * @param WP_REST_Request $request Full details about the request
     541         * @return WP_REST_Response|WP_Error
     542         */
     543        public function delete_item( $request ) {
     544
     545                $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
     546
     547                // We don't support trashing for this resource type.
     548                if ( ! $force ) {
     549                        return new WP_Error( 'rest_trash_not_supported', __( 'Resource does not support trashing.' ), array( 'status' => 501 ) );
     550                }
     551
     552                $term = get_term( (int) $request['id'], $this->taxonomy );
     553                $request->set_param( 'context', 'view' );
     554                $response = $this->prepare_item_for_response( $term, $request );
     555
     556                $retval = wp_delete_term( $term->term_id, $term->taxonomy );
     557                if ( ! $retval ) {
     558                        return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
     559                }
     560
     561                /**
     562                 * Fires after a single term is deleted via the REST API.
     563                 *
     564                 * @param WP_Term          $term     The deleted term.
     565                 * @param WP_REST_Response $response The response data.
     566                 * @param WP_REST_Request  $request  The request sent to the API.
     567                 */
     568                do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
     569
     570                return $response;
     571        }
     572
     573        /**
     574         * Prepares a single term for create or update.
     575         *
     576         * @param WP_REST_Request $request Request object.
     577         * @return object $prepared_term Term object.
     578         */
     579        public function prepare_item_for_database( $request ) {
     580                $prepared_term = new stdClass;
     581
     582                $schema = $this->get_item_schema();
     583                if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
     584                        $prepared_term->name = $request['name'];
     585                }
     586
     587                if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
     588                        $prepared_term->slug = $request['slug'];
     589                }
     590
     591                if ( isset( $request['taxonomy'] ) && ! empty( $schema['properties']['taxonomy'] ) ) {
     592                        $prepared_term->taxonomy = $request['taxonomy'];
     593                }
     594
     595                if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
     596                        $prepared_term->description = $request['description'];
     597                }
     598
     599                if ( isset( $request['parent'] ) && ! empty( $schema['properties']['parent'] ) ) {
     600                        $parent_term_id = 0;
     601                        $parent_term = get_term( (int) $request['parent'], $this->taxonomy );
     602
     603                        if ( $parent_term ) {
     604                                $parent_term_id = $parent_term->term_id;
     605                        }
     606
     607                        $prepared_term->parent = $parent_term_id;
     608                }
     609
     610                /**
     611                 * Filters term data before inserting term via the REST API.
     612                 *
     613                 * @param object          $prepared_term Term object.
     614                 * @param WP_REST_Request $request       Request object.
     615                 */
     616                return apply_filters( "rest_pre_insert_{$this->taxonomy}", $prepared_term, $request );
     617        }
     618
     619        /**
     620         * Prepares a single term output for response.
     621         *
     622         * @param obj             $item    Term object.
     623         * @param WP_REST_Request $request Request object.
     624         * @return WP_REST_Response $response
     625         */
     626        public function prepare_item_for_response( $item, $request ) {
     627
     628                $schema = $this->get_item_schema();
     629                $data = array();
     630                if ( ! empty( $schema['properties']['id'] ) ) {
     631                        $data['id'] = (int) $item->term_id;
     632                }
     633                if ( ! empty( $schema['properties']['count'] ) ) {
     634                        $data['count'] = (int) $item->count;
     635                }
     636                if ( ! empty( $schema['properties']['description'] ) ) {
     637                        $data['description'] = $item->description;
     638                }
     639                if ( ! empty( $schema['properties']['link'] ) ) {
     640                        $data['link'] = get_term_link( $item );
     641                }
     642                if ( ! empty( $schema['properties']['name'] ) ) {
     643                        $data['name'] = $item->name;
     644                }
     645                if ( ! empty( $schema['properties']['slug'] ) ) {
     646                        $data['slug'] = $item->slug;
     647                }
     648                if ( ! empty( $schema['properties']['taxonomy'] ) ) {
     649                        $data['taxonomy'] = $item->taxonomy;
     650                }
     651                if ( ! empty( $schema['properties']['parent'] ) ) {
     652                        $data['parent'] = (int) $item->parent;
     653                }
     654                if ( ! empty( $schema['properties']['meta'] ) ) {
     655                        $data['meta'] = $this->meta->get_value( $item->term_id, $request );
     656                }
     657
     658                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     659                $data = $this->add_additional_fields_to_object( $data, $request );
     660                $data = $this->filter_response_by_context( $data, $context );
     661
     662                $response = rest_ensure_response( $data );
     663
     664                $response->add_links( $this->prepare_links( $item ) );
     665
     666                /**
     667                 * Filters a term item returned from the API.
     668                 *
     669                 * Allows modification of the term data right before it is returned.
     670                 *
     671                 * @param WP_REST_Response  $response  The response object.
     672                 * @param object            $item      The original term object.
     673                 * @param WP_REST_Request   $request   Request used to generate the response.
     674                 */
     675                return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $item, $request );
     676        }
     677
     678        /**
     679         * Prepares links for the request.
     680         *
     681         * @param object $term Term object.
     682         * @return array Links for the given term.
     683         */
     684        protected function prepare_links( $term ) {
     685                $base = $this->namespace . '/' . $this->rest_base;
     686                $links = array(
     687                        'self'       => array(
     688                                'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
     689                        ),
     690                        'collection' => array(
     691                                'href' => rest_url( $base ),
     692                        ),
     693                        'about'      => array(
     694                                'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
     695                        ),
     696                );
     697
     698                if ( $term->parent ) {
     699                        $parent_term = get_term( (int) $term->parent, $term->taxonomy );
     700                        if ( $parent_term ) {
     701                                $links['up'] = array(
     702                                        'href'       => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
     703                                        'embeddable' => true,
     704                                );
     705                        }
     706                }
     707
     708                $taxonomy_obj = get_taxonomy( $term->taxonomy );
     709                if ( empty( $taxonomy_obj->object_type ) ) {
     710                        return $links;
     711                }
     712
     713                $post_type_links = array();
     714                foreach ( $taxonomy_obj->object_type as $type ) {
     715                        $post_type_object = get_post_type_object( $type );
     716                        if ( empty( $post_type_object->show_in_rest ) ) {
     717                                continue;
     718                        }
     719                        $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
     720                        $post_type_links[] = array(
     721                                'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( sprintf( 'wp/v2/%s', $rest_base ) ) ),
     722                        );
     723                }
     724                if ( ! empty( $post_type_links ) ) {
     725                        $links['https://api.w.org/post_type'] = $post_type_links;
     726                }
     727
     728                return $links;
     729        }
     730
     731        /**
     732         * Gets the term's schema, conforming to JSON Schema.
     733         *
     734         * @return array
     735         */
     736        public function get_item_schema() {
     737                $schema = array(
     738                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     739                        'title'      => 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy,
     740                        'type'       => 'object',
     741                        'properties' => array(
     742                                'id'          => array(
     743                                        'description'  => __( 'Unique identifier for the resource.' ),
     744                                        'type'         => 'integer',
     745                                        'context'      => array( 'view', 'embed', 'edit' ),
     746                                        'readonly'     => true,
     747                                ),
     748                                'count'       => array(
     749                                        'description'  => __( 'Number of published posts for the resource.' ),
     750                                        'type'         => 'integer',
     751                                        'context'      => array( 'view', 'edit' ),
     752                                        'readonly'     => true,
     753                                ),
     754                                'description' => array(
     755                                        'description'  => __( 'HTML description of the resource.' ),
     756                                        'type'         => 'string',
     757                                        'context'      => array( 'view', 'edit' ),
     758                                        'arg_options'  => array(
     759                                                'sanitize_callback' => 'wp_filter_post_kses',
     760                                        ),
     761                                ),
     762                                'link'        => array(
     763                                        'description'  => __( 'URL to the resource.' ),
     764                                        'type'         => 'string',
     765                                        'format'       => 'uri',
     766                                        'context'      => array( 'view', 'embed', 'edit' ),
     767                                        'readonly'     => true,
     768                                ),
     769                                'name'        => array(
     770                                        'description'  => __( 'HTML title for the resource.' ),
     771                                        'type'         => 'string',
     772                                        'context'      => array( 'view', 'embed', 'edit' ),
     773                                        'arg_options'  => array(
     774                                                'sanitize_callback' => 'sanitize_text_field',
     775                                        ),
     776                                        'required'     => true,
     777                                ),
     778                                'slug'        => array(
     779                                        'description'  => __( 'An alphanumeric identifier for the resource unique to its type.' ),
     780                                        'type'         => 'string',
     781                                        'context'      => array( 'view', 'embed', 'edit' ),
     782                                        'arg_options'  => array(
     783                                                'sanitize_callback' => array( $this, 'sanitize_slug' ),
     784                                        ),
     785                                ),
     786                                'taxonomy'    => array(
     787                                        'description'  => __( 'Type attribution for the resource.' ),
     788                                        'type'         => 'string',
     789                                        'enum'         => array_keys( get_taxonomies() ),
     790                                        'context'      => array( 'view', 'embed', 'edit' ),
     791                                        'readonly'     => true,
     792                                ),
     793                        ),
     794                );
     795                $taxonomy = get_taxonomy( $this->taxonomy );
     796                if ( $taxonomy->hierarchical ) {
     797                        $schema['properties']['parent'] = array(
     798                                'description'  => __( 'The id for the parent of the resource.' ),
     799                                'type'         => 'integer',
     800                                'context'      => array( 'view', 'edit' ),
     801                        );
     802                }
     803
     804                $schema['properties']['meta'] = $this->meta->get_field_schema();
     805                return $this->add_additional_fields_schema( $schema );
     806        }
     807
     808        /**
     809         * Gets the query params for collections.
     810         *
     811         * @return array
     812         */
     813        public function get_collection_params() {
     814                $query_params = parent::get_collection_params();
     815                $taxonomy = get_taxonomy( $this->taxonomy );
     816
     817                $query_params['context']['default'] = 'view';
     818
     819                $query_params['exclude'] = array(
     820                        'description'       => __( 'Ensure result set excludes specific ids.' ),
     821                        'type'              => 'array',
     822                        'default'           => array(),
     823                        'sanitize_callback' => 'wp_parse_id_list',
     824                );
     825                $query_params['include'] = array(
     826                        'description'       => __( 'Limit result set to specific ids.' ),
     827                        'type'              => 'array',
     828                        'default'           => array(),
     829                        'sanitize_callback' => 'wp_parse_id_list',
     830                );
     831                if ( ! $taxonomy->hierarchical ) {
     832                        $query_params['offset'] = array(
     833                                'description'       => __( 'Offset the result set by a specific number of items.' ),
     834                                'type'              => 'integer',
     835                                'sanitize_callback' => 'absint',
     836                                'validate_callback' => 'rest_validate_request_arg',
     837                        );
     838                }
     839                $query_params['order'] = array(
     840                        'description'       => __( 'Order sort attribute ascending or descending.' ),
     841                        'type'              => 'string',
     842                        'sanitize_callback' => 'sanitize_key',
     843                        'default'           => 'asc',
     844                        'enum'              => array(
     845                                'asc',
     846                                'desc',
     847                        ),
     848                        'validate_callback' => 'rest_validate_request_arg',
     849                );
     850                $query_params['orderby'] = array(
     851                        'description'       => __( 'Sort collection by resource attribute.' ),
     852                        'type'              => 'string',
     853                        'sanitize_callback' => 'sanitize_key',
     854                        'default'           => 'name',
     855                        'enum'              => array(
     856                                'id',
     857                                'include',
     858                                'name',
     859                                'slug',
     860                                'term_group',
     861                                'description',
     862                                'count',
     863                        ),
     864                        'validate_callback' => 'rest_validate_request_arg',
     865                );
     866                $query_params['hide_empty'] = array(
     867                        'description'       => __( 'Whether to hide resources not assigned to any posts.' ),
     868                        'type'              => 'boolean',
     869                        'default'           => false,
     870                        'validate_callback' => 'rest_validate_request_arg',
     871                );
     872                if ( $taxonomy->hierarchical ) {
     873                        $query_params['parent'] = array(
     874                                'description'       => __( 'Limit result set to resources assigned to a specific parent.' ),
     875                                'type'              => 'integer',
     876                                'sanitize_callback' => 'absint',
     877                                'validate_callback' => 'rest_validate_request_arg',
     878                        );
     879                }
     880                $query_params['post'] = array(
     881                        'description'       => __( 'Limit result set to resources assigned to a specific post.' ),
     882                        'type'              => 'integer',
     883                        'default'           => null,
     884                        'validate_callback' => 'rest_validate_request_arg',
     885                );
     886                $query_params['slug'] = array(
     887                        'description'       => __( 'Limit result set to resources with a specific slug.' ),
     888                        'type'              => 'string',
     889                        'validate_callback' => 'rest_validate_request_arg',
     890                );
     891                return $query_params;
     892        }
     893
     894        /**
     895         * Checks that the taxonomy is valid.
     896         *
     897         * @param string $taxonomy Taxonomy to check.
     898         * @return WP_Error|boolean
     899         */
     900        protected function check_is_taxonomy_allowed( $taxonomy ) {
     901                $taxonomy_obj = get_taxonomy( $taxonomy );
     902                if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
     903                        return true;
     904                }
     905                return false;
     906        }
     907}
  • src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3/**
     4 * Access users
     5 */
     6class WP_REST_Users_Controller extends WP_REST_Controller {
     7
     8        /**
     9         * Instance of a user meta fields object.
     10         *
     11         * @access protected
     12         * @var WP_REST_User_Meta_Fields
     13         */
     14        protected $meta;
     15
     16        public function __construct() {
     17                $this->namespace = 'wp/v2';
     18                $this->rest_base = 'users';
     19
     20                $this->meta = new WP_REST_User_Meta_Fields();
     21        }
     22
     23        /**
     24         * Register the routes for the objects of the controller.
     25         */
     26        public function register_routes() {
     27
     28                register_rest_route( $this->namespace, '/' . $this->rest_base, array(
     29                        array(
     30                                'methods'         => WP_REST_Server::READABLE,
     31                                'callback'        => array( $this, 'get_items' ),
     32                                'permission_callback' => array( $this, 'get_items_permissions_check' ),
     33                                'args'            => $this->get_collection_params(),
     34                        ),
     35                        array(
     36                                'methods'         => WP_REST_Server::CREATABLE,
     37                                'callback'        => array( $this, 'create_item' ),
     38                                'permission_callback' => array( $this, 'create_item_permissions_check' ),
     39                                'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
     40                        ),
     41                        'schema' => array( $this, 'get_public_item_schema' ),
     42                ) );
     43                register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
     44                        array(
     45                                'methods'         => WP_REST_Server::READABLE,
     46                                'callback'        => array( $this, 'get_item' ),
     47                                'permission_callback' => array( $this, 'get_item_permissions_check' ),
     48                                'args'            => array(
     49                                        'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
     50                                ),
     51                        ),
     52                        array(
     53                                'methods'         => WP_REST_Server::EDITABLE,
     54                                'callback'        => array( $this, 'update_item' ),
     55                                'permission_callback' => array( $this, 'update_item_permissions_check' ),
     56                                'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
     57                        ),
     58                        array(
     59                                'methods' => WP_REST_Server::DELETABLE,
     60                                'callback' => array( $this, 'delete_item' ),
     61                                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
     62                                'args' => array(
     63                                        'force'    => array(
     64                                                'default'     => false,
     65                                                'description' => __( 'Required to be true, as resource does not support trashing.' ),
     66                                        ),
     67                                        'reassign' => array(),
     68                                ),
     69                        ),
     70                        'schema' => array( $this, 'get_public_item_schema' ),
     71                ) );
     72
     73                register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array(
     74                        'methods'         => WP_REST_Server::READABLE,
     75                        'callback'        => array( $this, 'get_current_item' ),
     76                        'args'            => array(
     77                                'context'          => array(),
     78                        ),
     79                        'schema' => array( $this, 'get_public_item_schema' ),
     80                ));
     81        }
     82
     83        /**
     84         * Permissions check for getting all users.
     85         *
     86         * @param WP_REST_Request $request Full details about the request.
     87         * @return WP_Error|boolean
     88         */
     89        public function get_items_permissions_check( $request ) {
     90                // Check if roles is specified in GET request and if user can list users.
     91                if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
     92                        return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot filter by role.' ), array( 'status' => rest_authorization_required_code() ) );
     93                }
     94
     95                if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
     96                        return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     97                }
     98
     99                if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
     100                        return new WP_Error( 'rest_forbidden_orderby', __( 'Sorry, you cannot order by this parameter.' ), array( 'status' => rest_authorization_required_code() ) );
     101                }
     102
     103                return true;
     104        }
     105
     106        /**
     107         * Get all users
     108         *
     109         * @param WP_REST_Request $request Full details about the request.
     110         * @return WP_Error|WP_REST_Response
     111         */
     112        public function get_items( $request ) {
     113
     114                // Retrieve the list of registered collection query parameters.
     115                $registered = $this->get_collection_params();
     116
     117                // This array defines mappings between public API query parameters whose
     118                // values are accepted as-passed, and their internal WP_Query parameter
     119                // name equivalents (some are the same). Only values which are also
     120                // present in $registered will be set.
     121                $parameter_mappings = array(
     122                        'exclude'  => 'exclude',
     123                        'include'  => 'include',
     124                        'order'    => 'order',
     125                        'per_page' => 'number',
     126                        'search'   => 'search',
     127                        'roles'    => 'role__in',
     128                );
     129
     130                $prepared_args = array();
     131
     132                // For each known parameter which is both registered and present in the request,
     133                // set the parameter's value on the query $prepared_args.
     134                foreach ( $parameter_mappings as $api_param => $wp_param ) {
     135                        if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
     136                                $prepared_args[ $wp_param ] = $request[ $api_param ];
     137                        }
     138                }
     139
     140                if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
     141                        $prepared_args['offset'] = $request['offset'];
     142                } else {
     143                        $prepared_args['offset']  = ( $request['page'] - 1 ) * $prepared_args['number'];
     144                }
     145
     146                if ( isset( $registered['orderby'] ) ) {
     147                        $orderby_possibles = array(
     148                                'id'              => 'ID',
     149                                'include'         => 'include',
     150                                'name'            => 'display_name',
     151                                'registered_date' => 'registered',
     152                                'slug'            => 'user_nicename',
     153                                'email'           => 'user_email',
     154                                'url'             => 'user_url',
     155                        );
     156                        $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
     157                }
     158
     159                if ( ! current_user_can( 'list_users' ) ) {
     160                        $prepared_args['has_published_posts'] = true;
     161                }
     162
     163                if ( ! empty( $prepared_args['search'] ) ) {
     164                        $prepared_args['search'] = '*' . $prepared_args['search'] . '*';
     165                }
     166
     167                if ( isset( $registered['slug'] ) && ! empty( $request['slug'] ) ) {
     168                        $prepared_args['search'] = $request['slug'];
     169                        $prepared_args['search_columns'] = array( 'user_nicename' );
     170                }
     171
     172                /**
     173                 * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
     174                 *
     175                 * @see https://developer.wordpress.org/reference/classes/wp_user_query/
     176                 *
     177                 * @param array           $prepared_args Array of arguments for WP_User_Query.
     178                 * @param WP_REST_Request $request       The current request.
     179                 */
     180                $prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
     181
     182                $query = new WP_User_Query( $prepared_args );
     183
     184                $users = array();
     185                foreach ( $query->results as $user ) {
     186                        $data = $this->prepare_item_for_response( $user, $request );
     187                        $users[] = $this->prepare_response_for_collection( $data );
     188                }
     189
     190                $response = rest_ensure_response( $users );
     191
     192                // Store pagination values for headers then unset for count query.
     193                $per_page = (int) $prepared_args['number'];
     194                $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
     195
     196                $prepared_args['fields'] = 'ID';
     197
     198                $total_users = $query->get_total();
     199                if ( $total_users < 1 ) {
     200                        // Out-of-bounds, run the query again without LIMIT for total count
     201                        unset( $prepared_args['number'], $prepared_args['offset'] );
     202                        $count_query = new WP_User_Query( $prepared_args );
     203                        $total_users = $count_query->get_total();
     204                }
     205                $response->header( 'X-WP-Total', (int) $total_users );
     206                $max_pages = ceil( $total_users / $per_page );
     207                $response->header( 'X-WP-TotalPages', (int) $max_pages );
     208
     209                $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
     210                if ( $page > 1 ) {
     211                        $prev_page = $page - 1;
     212                        if ( $prev_page > $max_pages ) {
     213                                $prev_page = $max_pages;
     214                        }
     215                        $prev_link = add_query_arg( 'page', $prev_page, $base );
     216                        $response->link_header( 'prev', $prev_link );
     217                }
     218                if ( $max_pages > $page ) {
     219                        $next_page = $page + 1;
     220                        $next_link = add_query_arg( 'page', $next_page, $base );
     221                        $response->link_header( 'next', $next_link );
     222                }
     223
     224                return $response;
     225        }
     226
     227        /**
     228         * Check if a given request has access to read a user
     229         *
     230         * @param  WP_REST_Request $request Full details about the request.
     231         * @return WP_Error|boolean
     232         */
     233        public function get_item_permissions_check( $request ) {
     234
     235                $id = (int) $request['id'];
     236                $user = get_userdata( $id );
     237                $types = get_post_types( array( 'show_in_rest' => true ), 'names' );
     238
     239                if ( empty( $id ) || empty( $user->ID ) ) {
     240                        return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
     241                }
     242
     243                if ( get_current_user_id() === $id ) {
     244                        return true;
     245                }
     246
     247                if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
     248                        return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
     249                } elseif ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) {
     250                        return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource.' ), array( 'status' => rest_authorization_required_code() ) );
     251                }
     252
     253                return true;
     254        }
     255
     256        /**
     257         * Get a single user
     258         *
     259         * @param WP_REST_Request $request Full details about the request.
     260         * @return WP_Error|WP_REST_Response
     261         */
     262        public function get_item( $request ) {
     263                $id = (int) $request['id'];
     264                $user = get_userdata( $id );
     265
     266                if ( empty( $id ) || empty( $user->ID ) ) {
     267                        return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
     268                }
     269
     270                $user = $this->prepare_item_for_response( $user, $request );
     271                $response = rest_ensure_response( $user );
     272
     273                return $response;
     274        }
     275
     276        /**
     277         * Get the current user
     278         *
     279         * @param WP_REST_Request $request Full details about the request.
     280         * @return WP_Error|WP_REST_Response
     281         */
     282        public function get_current_item( $request ) {
     283                $current_user_id = get_current_user_id();
     284                if ( empty( $current_user_id ) ) {
     285                        return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
     286                }
     287
     288                $user = wp_get_current_user();
     289                $response = $this->prepare_item_for_response( $user, $request );
     290                $response = rest_ensure_response( $response );
     291                $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $current_user_id ) ) );
     292                $response->set_status( 302 );
     293
     294                return $response;
     295        }
     296
     297        /**
     298         * Check if a given request has access create users
     299         *
     300         * @param  WP_REST_Request $request Full details about the request.
     301         * @return WP_Error|boolean
     302         */
     303        public function create_item_permissions_check( $request ) {
     304
     305                if ( ! current_user_can( 'create_users' ) ) {
     306                        return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create resource.' ), array( 'status' => rest_authorization_required_code() ) );
     307                }
     308
     309                return true;
     310        }
     311
     312        /**
     313         * Create a single user
     314         *
     315         * @param WP_REST_Request $request Full details about the request.
     316         * @return WP_Error|WP_REST_Response
     317         */
     318        public function create_item( $request ) {
     319                if ( ! empty( $request['id'] ) ) {
     320                        return new WP_Error( 'rest_user_exists', __( 'Cannot create existing resource.' ), array( 'status' => 400 ) );
     321                }
     322
     323                $schema = $this->get_item_schema();
     324
     325                if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
     326                        $check_permission = $this->check_role_update( $request['id'], $request['roles'] );
     327                        if ( is_wp_error( $check_permission ) ) {
     328                                return $check_permission;
     329                        }
     330                }
     331
     332                $user = $this->prepare_item_for_database( $request );
     333
     334                if ( is_multisite() ) {
     335                        $ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );
     336                        if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) {
     337                                return $ret['errors'];
     338                        }
     339                }
     340
     341                if ( is_multisite() ) {
     342                        $user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
     343                        if ( ! $user_id ) {
     344                                return new WP_Error( 'rest_user_create', __( 'Error creating new resource.' ), array( 'status' => 500 ) );
     345                        }
     346                        $user->ID = $user_id;
     347                        $user_id = wp_update_user( $user );
     348                        if ( is_wp_error( $user_id ) ) {
     349                                return $user_id;
     350                        }
     351                } else {
     352                        $user_id = wp_insert_user( $user );
     353                        if ( is_wp_error( $user_id ) ) {
     354                                return $user_id;
     355                        }
     356                }
     357
     358                $user = get_user_by( 'id', $user_id );
     359                if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
     360                        array_map( array( $user, 'add_role' ), $request['roles'] );
     361                }
     362
     363                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     364                        $meta_update = $this->meta->update_value( $request['meta'], $user_id );
     365                        if ( is_wp_error( $meta_update ) ) {
     366                                return $meta_update;
     367                        }
     368                }
     369
     370                $fields_update = $this->update_additional_fields_for_object( $user, $request );
     371                if ( is_wp_error( $fields_update ) ) {
     372                        return $fields_update;
     373                }
     374
     375                /**
     376                 * Fires after a user is created or updated via the REST API.
     377                 *
     378                 * @param WP_User         $user      Data used to create the user.
     379                 * @param WP_REST_Request $request   Request object.
     380                 * @param boolean         $creating  True when creating user, false when updating user.
     381                 */
     382                do_action( 'rest_insert_user', $user, $request, true );
     383
     384                $request->set_param( 'context', 'edit' );
     385                $response = $this->prepare_item_for_response( $user, $request );
     386                $response = rest_ensure_response( $response );
     387                $response->set_status( 201 );
     388                $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) );
     389
     390                return $response;
     391        }
     392
     393        /**
     394         * Check if a given request has access update a user
     395         *
     396         * @param  WP_REST_Request $request Full details about the request.
     397         * @return WP_Error|boolean
     398         */
     399        public function update_item_permissions_check( $request ) {
     400
     401                $id = (int) $request['id'];
     402
     403                if ( ! current_user_can( 'edit_user', $id ) ) {
     404                        return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.' ), array( 'status' => rest_authorization_required_code() ) );
     405                }
     406
     407                if ( ! empty( $request['roles'] ) && ! current_user_can( 'edit_users' ) ) {
     408                        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() ) );
     409                }
     410
     411                return true;
     412        }
     413
     414        /**
     415         * Update a single user
     416         *
     417         * @param WP_REST_Request $request Full details about the request.
     418         * @return WP_Error|WP_REST_Response
     419         */
     420        public function update_item( $request ) {
     421                $id = (int) $request['id'];
     422
     423                $user = get_userdata( $id );
     424                if ( ! $user ) {
     425                        return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
     426                }
     427
     428                if ( email_exists( $request['email'] ) && $request['email'] !== $user->user_email ) {
     429                        return new WP_Error( 'rest_user_invalid_email', __( 'Email address is invalid.' ), array( 'status' => 400 ) );
     430                }
     431
     432                if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
     433                        return new WP_Error( 'rest_user_invalid_argument', __( "Username isn't editable." ), array( 'status' => 400 ) );
     434                }
     435
     436                if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
     437                        return new WP_Error( 'rest_user_invalid_slug', __( 'Slug is invalid.' ), array( 'status' => 400 ) );
     438                }
     439
     440                if ( ! empty( $request['roles'] ) ) {
     441                        $check_permission = $this->check_role_update( $id, $request['roles'] );
     442                        if ( is_wp_error( $check_permission ) ) {
     443                                return $check_permission;
     444                        }
     445                }
     446
     447                $user = $this->prepare_item_for_database( $request );
     448
     449                // Ensure we're operating on the same user we already checked
     450                $user->ID = $id;
     451
     452                $user_id = wp_update_user( $user );
     453                if ( is_wp_error( $user_id ) ) {
     454                        return $user_id;
     455                }
     456
     457                $user = get_user_by( 'id', $id );
     458                if ( ! empty( $request['roles'] ) ) {
     459                        array_map( array( $user, 'add_role' ), $request['roles'] );
     460                }
     461
     462                $schema = $this->get_item_schema();
     463                if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
     464                        $meta_update = $this->meta->update_value( $request['meta'], $id );
     465                        if ( is_wp_error( $meta_update ) ) {
     466                                return $meta_update;
     467                        }
     468                }
     469
     470                $fields_update = $this->update_additional_fields_for_object( $user, $request );
     471                if ( is_wp_error( $fields_update ) ) {
     472                        return $fields_update;
     473                }
     474
     475                /* This action is documented in lib/endpoints/class-wp-rest-users-controller.php */
     476                do_action( 'rest_insert_user', $user, $request, false );
     477
     478                $request->set_param( 'context', 'edit' );
     479                $response = $this->prepare_item_for_response( $user, $request );
     480                $response = rest_ensure_response( $response );
     481                return $response;
     482        }
     483
     484        /**
     485         * Check if a given request has access delete a user
     486         *
     487         * @param  WP_REST_Request $request Full details about the request.
     488         * @return WP_Error|boolean
     489         */
     490        public function delete_item_permissions_check( $request ) {
     491
     492                $id = (int) $request['id'];
     493
     494                if ( ! current_user_can( 'delete_user', $id ) ) {
     495                        return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.' ), array( 'status' => rest_authorization_required_code() ) );
     496                }
     497
     498                return true;
     499        }
     500
     501        /**
     502         * Delete a single user
     503         *
     504         * @param WP_REST_Request $request Full details about the request.
     505         * @return WP_Error|WP_REST_Response
     506         */
     507        public function delete_item( $request ) {
     508                $id = (int) $request['id'];
     509                $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
     510                $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
     511
     512                // We don't support trashing for this type, error out
     513                if ( ! $force ) {
     514                        return new WP_Error( 'rest_trash_not_supported', __( 'Users do not support trashing.' ), array( 'status' => 501 ) );
     515                }
     516
     517                $user = get_userdata( $id );
     518                if ( ! $user ) {
     519                        return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
     520                }
     521
     522                if ( ! empty( $reassign ) ) {
     523                        if ( $reassign === $id || ! get_userdata( $reassign ) ) {
     524                                return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid resource id for reassignment.' ), array( 'status' => 400 ) );
     525                        }
     526                }
     527
     528                $request->set_param( 'context', 'edit' );
     529                $response = $this->prepare_item_for_response( $user, $request );
     530
     531                /** Include admin user functions to get access to wp_delete_user() */
     532                require_once ABSPATH . 'wp-admin/includes/user.php';
     533
     534                $result = wp_delete_user( $id, $reassign );
     535
     536                if ( ! $result ) {
     537                        return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
     538                }
     539
     540                /**
     541                 * Fires after a user is deleted via the REST API.
     542                 *
     543                 * @param WP_User          $user     The user data.
     544                 * @param WP_REST_Response $response The response returned from the API.
     545                 * @param WP_REST_Request  $request  The request sent to the API.
     546                 */
     547                do_action( 'rest_delete_user', $user, $response, $request );
     548
     549                return $response;
     550        }
     551
     552        /**
     553         * Prepare a single user output for response
     554         *
     555         * @param object $user User object.
     556         * @param WP_REST_Request $request Request object.
     557         * @return WP_REST_Response $response Response data.
     558         */
     559        public function prepare_item_for_response( $user, $request ) {
     560
     561                $data = array();
     562                $schema = $this->get_item_schema();
     563                if ( ! empty( $schema['properties']['id'] ) ) {
     564                        $data['id'] = $user->ID;
     565                }
     566
     567                if ( ! empty( $schema['properties']['username'] ) ) {
     568                        $data['username'] = $user->user_login;
     569                }
     570
     571                if ( ! empty( $schema['properties']['name'] ) ) {
     572                        $data['name'] = $user->display_name;
     573                }
     574
     575                if ( ! empty( $schema['properties']['first_name'] ) ) {
     576                        $data['first_name'] = $user->first_name;
     577                }
     578
     579                if ( ! empty( $schema['properties']['last_name'] ) ) {
     580                        $data['last_name'] = $user->last_name;
     581                }
     582
     583                if ( ! empty( $schema['properties']['email'] ) ) {
     584                        $data['email'] = $user->user_email;
     585                }
     586
     587                if ( ! empty( $schema['properties']['url'] ) ) {
     588                        $data['url'] = $user->user_url;
     589                }
     590
     591                if ( ! empty( $schema['properties']['description'] ) ) {
     592                        $data['description'] = $user->description;
     593                }
     594
     595                if ( ! empty( $schema['properties']['link'] ) ) {
     596                        $data['link'] = get_author_posts_url( $user->ID, $user->user_nicename );
     597                }
     598
     599                if ( ! empty( $schema['properties']['nickname'] ) ) {
     600                        $data['nickname'] = $user->nickname;
     601                }
     602
     603                if ( ! empty( $schema['properties']['slug'] ) ) {
     604                        $data['slug'] = $user->user_nicename;
     605                }
     606
     607                if ( ! empty( $schema['properties']['roles'] ) ) {
     608                        // Defensively call array_values() to ensure an array is returned.
     609                        $data['roles'] = array_values( $user->roles );
     610                }
     611
     612                if ( ! empty( $schema['properties']['registered_date'] ) ) {
     613                        $data['registered_date'] = date( 'c', strtotime( $user->user_registered ) );
     614                }
     615
     616                if ( ! empty( $schema['properties']['capabilities'] ) ) {
     617                        $data['capabilities'] = (object) $user->allcaps;
     618                }
     619
     620                if ( ! empty( $schema['properties']['extra_capabilities'] ) ) {
     621                        $data['extra_capabilities'] = (object) $user->caps;
     622                }
     623
     624                if ( ! empty( $schema['properties']['avatar_urls'] ) ) {
     625                        $data['avatar_urls'] = rest_get_avatar_urls( $user->user_email );
     626                }
     627
     628                if ( ! empty( $schema['properties']['meta'] ) ) {
     629                        $data['meta'] = $this->meta->get_value( $user->ID, $request );
     630                }
     631
     632                $context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
     633
     634                $data = $this->add_additional_fields_to_object( $data, $request );
     635                $data = $this->filter_response_by_context( $data, $context );
     636
     637                // Wrap the data in a response object
     638                $response = rest_ensure_response( $data );
     639
     640                $response->add_links( $this->prepare_links( $user ) );
     641
     642                /**
     643                 * Filter user data returned from the REST API.
     644                 *
     645                 * @param WP_REST_Response $response  The response object.
     646                 * @param object           $user      User object used to create response.
     647                 * @param WP_REST_Request  $request   Request object.
     648                 */
     649                return apply_filters( 'rest_prepare_user', $response, $user, $request );
     650        }
     651
     652        /**
     653         * Prepare links for the request.
     654         *
     655         * @param WP_Post $user User object.
     656         * @return array Links for the given user.
     657         */
     658        protected function prepare_links( $user ) {
     659                $links = array(
     660                        'self' => array(
     661                                'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ),
     662                        ),
     663                        'collection' => array(
     664                                'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
     665                        ),
     666                );
     667
     668                return $links;
     669        }
     670
     671        /**
     672         * Prepare a single user for create or update
     673         *
     674         * @param WP_REST_Request $request Request object.
     675         * @return object $prepared_user User object.
     676         */
     677        protected function prepare_item_for_database( $request ) {
     678                $prepared_user = new stdClass;
     679
     680                $schema = $this->get_item_schema();
     681
     682                // required arguments.
     683                if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) {
     684                        $prepared_user->user_email = $request['email'];
     685                }
     686                if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) {
     687                        $prepared_user->user_login = $request['username'];
     688                }
     689                if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) {
     690                        $prepared_user->user_pass = $request['password'];
     691                }
     692
     693                // optional arguments.
     694                if ( isset( $request['id'] ) ) {
     695                        $prepared_user->ID = absint( $request['id'] );
     696                }
     697                if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
     698                        $prepared_user->display_name = $request['name'];
     699                }
     700                if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) {
     701                        $prepared_user->first_name = $request['first_name'];
     702                }
     703                if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) {
     704                        $prepared_user->last_name = $request['last_name'];
     705                }
     706                if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) {
     707                        $prepared_user->nickname = $request['nickname'];
     708                }
     709                if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
     710                        $prepared_user->user_nicename = $request['slug'];
     711                }
     712                if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
     713                        $prepared_user->description = $request['description'];
     714                }
     715
     716                if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) {
     717                        $prepared_user->user_url = $request['url'];
     718                }
     719
     720                // setting roles will be handled outside of this function.
     721                if ( isset( $request['roles'] ) ) {
     722                        $prepared_user->role = false;
     723                }
     724
     725                /**
     726                 * Filter user data before inserting user via the REST API.
     727                 *
     728                 * @param object          $prepared_user User object.
     729                 * @param WP_REST_Request $request       Request object.
     730                 */
     731                return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
     732        }
     733
     734        /**
     735         * Determine if the current user is allowed to make the desired roles change.
     736         *
     737         * @param integer $user_id User ID.
     738         * @param array   $roles   New user roles.
     739         * @return WP_Error|boolean
     740         */
     741        protected function check_role_update( $user_id, $roles ) {
     742                global $wp_roles;
     743
     744                foreach ( $roles as $role ) {
     745
     746                        if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
     747                                return new WP_Error( 'rest_user_invalid_role', sprintf( __( 'The role %s does not exist.' ), $role ), array( 'status' => 400 ) );
     748                        }
     749
     750                        $potential_role = $wp_roles->role_objects[ $role ];
     751                        // Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
     752                        // Multisite super admins can freely edit their blog roles -- they possess all caps.
     753                        if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $user_id && ! $potential_role->has_cap( 'edit_users' ) ) {
     754                                return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => rest_authorization_required_code() ) );
     755                        }
     756
     757                        // The new role must be editable by the logged-in user.
     758
     759                        /** Include admin functions to get access to get_editable_roles() */
     760                        require_once ABSPATH . 'wp-admin/includes/admin.php';
     761
     762                        $editable_roles = get_editable_roles();
     763                        if ( empty( $editable_roles[ $role ] ) ) {
     764                                return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => 403 ) );
     765                        }
     766                }
     767
     768                return true;
     769
     770        }
     771
     772        /**
     773         * Get the User's schema, conforming to JSON Schema
     774         *
     775         * @return array
     776         */
     777        public function get_item_schema() {
     778                $schema = array(
     779                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     780                        'title'      => 'user',
     781                        'type'       => 'object',
     782                        'properties' => array(
     783                                'id'          => array(
     784                                        'description' => __( 'Unique identifier for the resource.' ),
     785                                        'type'        => 'integer',
     786                                        'context'     => array( 'embed', 'view', 'edit' ),
     787                                        'readonly'    => true,
     788                                ),
     789                                'username'    => array(
     790                                        'description' => __( 'Login name for the resource.' ),
     791                                        'type'        => 'string',
     792                                        'context'     => array( 'edit' ),
     793                                        'required'    => true,
     794                                        'arg_options' => array(
     795                                                'sanitize_callback' => 'sanitize_user',
     796                                        ),
     797                                ),
     798                                'name'        => array(
     799                                        'description' => __( 'Display name for the resource.' ),
     800                                        'type'        => 'string',
     801                                        'context'     => array( 'embed', 'view', 'edit' ),
     802                                        'arg_options' => array(
     803                                                'sanitize_callback' => 'sanitize_text_field',
     804                                        ),
     805                                ),
     806                                'first_name'  => array(
     807                                        'description' => __( 'First name for the resource.' ),
     808                                        'type'        => 'string',
     809                                        'context'     => array( 'edit' ),
     810                                        'arg_options' => array(
     811                                                'sanitize_callback' => 'sanitize_text_field',
     812                                        ),
     813                                ),
     814                                'last_name'   => array(
     815                                        'description' => __( 'Last name for the resource.' ),
     816                                        'type'        => 'string',
     817                                        'context'     => array( 'edit' ),
     818                                        'arg_options' => array(
     819                                                'sanitize_callback' => 'sanitize_text_field',
     820                                        ),
     821                                ),
     822                                'email'       => array(
     823                                        'description' => __( 'The email address for the resource.' ),
     824                                        'type'        => 'string',
     825                                        'format'      => 'email',
     826                                        'context'     => array( 'edit' ),
     827                                        'required'    => true,
     828                                ),
     829                                'url'         => array(
     830                                        'description' => __( 'URL of the resource.' ),
     831                                        'type'        => 'string',
     832                                        'format'      => 'uri',
     833                                        'context'     => array( 'embed', 'view', 'edit' ),
     834                                ),
     835                                'description' => array(
     836                                        'description' => __( 'Description of the resource.' ),
     837                                        'type'        => 'string',
     838                                        'context'     => array( 'embed', 'view', 'edit' ),
     839                                        'arg_options' => array(
     840                                                'sanitize_callback' => 'wp_filter_post_kses',
     841                                        ),
     842                                ),
     843                                'link'        => array(
     844                                        'description' => __( 'Author URL to the resource.' ),
     845                                        'type'        => 'string',
     846                                        'format'      => 'uri',
     847                                        'context'     => array( 'embed', 'view', 'edit' ),
     848                                        'readonly'    => true,
     849                                ),
     850                                'nickname'    => array(
     851                                        'description' => __( 'The nickname for the resource.' ),
     852                                        'type'        => 'string',
     853                                        'context'     => array( 'edit' ),
     854                                        'arg_options' => array(
     855                                                'sanitize_callback' => 'sanitize_text_field',
     856                                        ),
     857                                ),
     858                                'slug'        => array(
     859                                        'description' => __( 'An alphanumeric identifier for the resource.' ),
     860                                        'type'        => 'string',
     861                                        'context'     => array( 'embed', 'view', 'edit' ),
     862                                        'arg_options' => array(
     863                                                'sanitize_callback' => array( $this, 'sanitize_slug' ),
     864                                        ),
     865                                ),
     866                                'registered_date' => array(
     867                                        'description' => __( 'Registration date for the resource.' ),
     868                                        'type'        => 'string',
     869                                        'format'      => 'date-time',
     870                                        'context'     => array( 'edit' ),
     871                                        'readonly'    => true,
     872                                ),
     873                                'roles'           => array(
     874                                        'description' => __( 'Roles assigned to the resource.' ),
     875                                        'type'        => 'array',
     876                                        'context'     => array( 'edit' ),
     877                                ),
     878                                'password'        => array(
     879                                        'description' => __( 'Password for the resource (never included).' ),
     880                                        'type'        => 'string',
     881                                        'context'     => array(), // Password is never displayed
     882                                        'required'    => true,
     883                                ),
     884                                'capabilities'    => array(
     885                                        'description' => __( 'All capabilities assigned to the resource.' ),
     886                                        'type'        => 'object',
     887                                        'context'     => array( 'edit' ),
     888                                        'readonly'    => true,
     889                                ),
     890                                'extra_capabilities' => array(
     891                                        'description' => __( 'Any extra capabilities assigned to the resource.' ),
     892                                        'type'        => 'object',
     893                                        'context'     => array( 'edit' ),
     894                                        'readonly'    => true,
     895                                ),
     896                        ),
     897                );
     898
     899                if ( get_option( 'show_avatars' ) ) {
     900                        $avatar_properties = array();
     901
     902                        $avatar_sizes = rest_get_avatar_sizes();
     903                        foreach ( $avatar_sizes as $size ) {
     904                                $avatar_properties[ $size ] = array(
     905                                        'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
     906                                        'type'        => 'string',
     907                                        'format'      => 'uri',
     908                                        'context'     => array( 'embed', 'view', 'edit' ),
     909                                );
     910                        }
     911
     912                        $schema['properties']['avatar_urls']  = array(
     913                                'description' => __( 'Avatar URLs for the resource.' ),
     914                                'type'        => 'object',
     915                                'context'     => array( 'embed', 'view', 'edit' ),
     916                                'readonly'    => true,
     917                                'properties'  => $avatar_properties,
     918                        );
     919                }
     920
     921                $schema['properties']['meta'] = $this->meta->get_field_schema();
     922
     923                return $this->add_additional_fields_schema( $schema );
     924        }
     925
     926        /**
     927         * Get the query params for collections
     928         *
     929         * @return array
     930         */
     931        public function get_collection_params() {
     932                $query_params = parent::get_collection_params();
     933
     934                $query_params['context']['default'] = 'view';
     935
     936                $query_params['exclude'] = array(
     937                        'description'        => __( 'Ensure result set excludes specific ids.' ),
     938                        'type'               => 'array',
     939                        'default'            => array(),
     940                        'sanitize_callback'  => 'wp_parse_id_list',
     941                );
     942                $query_params['include'] = array(
     943                        'description'        => __( 'Limit result set to specific ids.' ),
     944                        'type'               => 'array',
     945                        'default'            => array(),
     946                        'sanitize_callback'  => 'wp_parse_id_list',
     947                );
     948                $query_params['offset'] = array(
     949                        'description'        => __( 'Offset the result set by a specific number of items.' ),
     950                        'type'               => 'integer',
     951                        'sanitize_callback'  => 'absint',
     952                        'validate_callback'  => 'rest_validate_request_arg',
     953                );
     954                $query_params['order'] = array(
     955                        'default'            => 'asc',
     956                        'description'        => __( 'Order sort attribute ascending or descending.' ),
     957                        'enum'               => array( 'asc', 'desc' ),
     958                        'sanitize_callback'  => 'sanitize_key',
     959                        'type'               => 'string',
     960                        'validate_callback'  => 'rest_validate_request_arg',
     961                );
     962                $query_params['orderby'] = array(
     963                        'default'            => 'name',
     964                        'description'        => __( 'Sort collection by object attribute.' ),
     965                        'enum'               => array(
     966                                'id',
     967                                'include',
     968                                'name',
     969                                'registered_date',
     970                                'slug',
     971                                'email',
     972                                'url',
     973                        ),
     974                        'sanitize_callback'  => 'sanitize_key',
     975                        'type'               => 'string',
     976                        'validate_callback'  => 'rest_validate_request_arg',
     977                );
     978                $query_params['slug']    = array(
     979                        'description'        => __( 'Limit result set to resources with a specific slug.' ),
     980                        'type'               => 'string',
     981                        'validate_callback'  => 'rest_validate_request_arg',
     982                );
     983                $query_params['roles']   = array(
     984                        'description'        => __( 'Limit result set to resources matching at least one specific role provided. Accepts csv list or single role.' ),
     985                        'type'               => 'array',
     986                        'sanitize_callback'  => 'wp_parse_slug_list',
     987                );
     988                return $query_params;
     989        }
     990}
  • src/wp-includes/rest-api/fields/class-wp-rest-comment-meta-fields.php

    Property changes on: src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1<?php
     2
     3class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields {
     4        /**
     5         * Get the object type for meta.
     6         *
     7         * @return string
     8         */
     9        protected function get_meta_type() {
     10                return 'comment';
     11        }
     12
     13        /**
     14         * Get the type for `register_rest_field`.
     15         *
     16         * @return string
     17         */
     18        public function get_rest_field_type() {
     19                return 'comment';
     20        }
     21}
  • src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php

     
     1<?php
     2
     3/**
     4 * Manage meta values for an object.
     5 */
     6abstract class WP_REST_Meta_Fields {
     7
     8        /**
     9         * Get the object type for meta.
     10         *
     11         * @return string One of 'post', 'comment', 'term', 'user', or anything
     12         *                else supported by `_get_meta_table()`.
     13         */
     14        abstract protected function get_meta_type();
     15
     16        /**
     17         * Get the object type for `register_rest_field`.
     18         *
     19         * @return string Custom post type, 'taxonomy', 'comment', or `user`.
     20         */
     21        abstract protected function get_rest_field_type();
     22
     23        /**
     24         * Register the meta field.
     25         */
     26        public function register_field() {
     27                register_rest_field( $this->get_rest_field_type(), 'meta', array(
     28                        'get_callback' => array( $this, 'get_value' ),
     29                        'update_callback' => array( $this, 'update_value' ),
     30                        'schema' => $this->get_field_schema(),
     31                ));
     32        }
     33
     34        /**
     35         * Get the `meta` field value.
     36         *
     37         * @param int             $object_id Object ID to fetch meta for.
     38         * @param WP_REST_Request $request   Full details about the request.
     39         * @return WP_Error|object
     40         */
     41        public function get_value( $object_id, $request ) {
     42                $fields   = $this->get_registered_fields();
     43                $response = array();
     44
     45                foreach ( $fields as $name => $args ) {
     46                        $all_values = get_metadata( $this->get_meta_type(), $object_id, $name, false );
     47                        if ( $args['single'] ) {
     48                                if ( empty( $all_values ) ) {
     49                                        $value = $args['schema']['default'];
     50                                } else {
     51                                        $value = $all_values[0];
     52                                }
     53                                $value = $this->prepare_value_for_response( $value, $request, $args );
     54                        } else {
     55                                $value = array();
     56                                foreach ( $all_values as $row ) {
     57                                        $value[] = $this->prepare_value_for_response( $row, $request, $args );
     58                                }
     59                        }
     60
     61                        $response[ $name ] = $value;
     62                }
     63
     64                return (object) $response;
     65        }
     66
     67        /**
     68         * Prepare value for response.
     69         *
     70         * This is required because some native types cannot be stored correctly in
     71         * the database, such as booleans. We need to cast back to the relevant
     72         * type before passing back to JSON.
     73         *
     74         * @param mixed           $value   Value to prepare.
     75         * @param WP_REST_Request $request Current request object.
     76         * @param array           $args    Options for the field.
     77         * @return mixed Prepared value.
     78         */
     79        protected function prepare_value_for_response( $value, $request, $args ) {
     80                if ( ! empty( $args['prepare_callback'] ) ) {
     81                        $value = call_user_func( $args['prepare_callback'], $value, $request, $args );
     82                }
     83
     84                return $value;
     85        }
     86
     87        /**
     88         * Update meta values.
     89         *
     90         * @param WP_REST_Request $request    Full details about the request.
     91         * @param int             $object_id  Object ID to fetch meta for.
     92         * @return WP_Error|null Error if one occurs, null on success.
     93         */
     94        public function update_value( $request, $object_id ) {
     95                $fields = $this->get_registered_fields();
     96
     97                foreach ( $fields as $name => $args ) {
     98                        if ( ! array_key_exists( $name, $request ) ) {
     99                                continue;
     100                        }
     101
     102                        // A null value means reset the field, which is essentially deleting it
     103                        // from the database and then relying on the default value.
     104                        if ( is_null( $request[ $name ] ) ) {
     105                                $result = $this->delete_meta_value( $object_id, $name );
     106                        } elseif ( $args['single'] ) {
     107                                $result = $this->update_meta_value( $object_id, $name, $request[ $name ] );
     108                        } else {
     109                                $result = $this->update_multi_meta_value( $object_id, $name, $request[ $name ] );
     110                        }
     111
     112                        if ( is_wp_error( $result ) ) {
     113                                return $result;
     114                        }
     115                }
     116
     117                return null;
     118        }
     119
     120        /**
     121         * Delete meta value for an object.
     122         *
     123         * @param int    $object_id Object ID the field belongs to.
     124         * @param string $name      Key for the field.
     125         * @return bool|WP_Error True if meta field is deleted, error otherwise.
     126         */
     127        protected function delete_meta_value( $object_id, $name ) {
     128                if ( ! current_user_can( 'delete_post_meta', $object_id, $name ) ) {
     129                        return new WP_Error(
     130                                'rest_cannot_delete',
     131                                sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
     132                                array( 'key' => $name, 'status' => rest_authorization_required_code() )
     133                        );
     134                }
     135
     136                if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ) ) ) {
     137                        return new WP_Error(
     138                                'rest_meta_database_error',
     139                                __( 'Could not delete meta value from database.' ),
     140                                array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
     141                        );
     142                }
     143
     144                return true;
     145        }
     146
     147        /**
     148         * Update multiple meta values for an object.
     149         *
     150         * Alters the list of values in the database to match the list of provided values.
     151         *
     152         * @param int    $object_id Object ID to update.
     153         * @param string $name      Key for the custom field.
     154         * @param array  $values    List of values to update to.
     155         * @return bool|WP_Error True if meta fields are updated, error otherwise.
     156         */
     157        protected function update_multi_meta_value( $object_id, $name, $values ) {
     158                if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
     159                        return new WP_Error(
     160                                'rest_cannot_update',
     161                                sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
     162                                array( 'key' => $name, 'status' => rest_authorization_required_code() )
     163                        );
     164                }
     165
     166                $current = get_metadata( $this->get_meta_type(), $object_id, $name, false );
     167
     168                $to_remove = $current;
     169                $to_add = $values;
     170                foreach ( $to_add as $add_key => $value ) {
     171                        $remove_keys = array_keys( $to_remove, $value, true );
     172                        if ( empty( $remove_keys ) ) {
     173                                continue;
     174                        }
     175
     176                        if ( count( $remove_keys ) > 1 ) {
     177                                // To remove, we need to remove first, then add, so don't touch.
     178                                continue;
     179                        }
     180
     181                        $remove_key = $remove_keys[0];
     182                        unset( $to_remove[ $remove_key ] );
     183                        unset( $to_add[ $add_key ] );
     184                }
     185
     186                // `delete_metadata` removes _all_ instances of the value, so only call
     187                // once.
     188                $to_remove = array_unique( $to_remove );
     189                foreach ( $to_remove as $value ) {
     190                        if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
     191                                return new WP_Error(
     192                                        'rest_meta_database_error',
     193                                        __( 'Could not update meta value in database.' ),
     194                                        array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
     195                                );
     196                        }
     197                }
     198                foreach ( $to_add as $value ) {
     199                        if ( ! add_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
     200                                return new WP_Error(
     201                                        'rest_meta_database_error',
     202                                        __( 'Could not update meta value in database.' ),
     203                                        array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
     204                                );
     205                        }
     206                }
     207
     208                return true;
     209        }
     210
     211        /**
     212         * Update meta value for an object.
     213         *
     214         * @param int    $object_id Object ID to update.
     215         * @param string $name      Key for the custom field.
     216         * @param mixed  $value     Updated value.
     217         * @return bool|WP_Error True if meta field is updated, error otherwise.
     218         */
     219        protected function update_meta_value( $object_id, $name, $value ) {
     220                if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
     221                        return new WP_Error(
     222                                'rest_cannot_update',
     223                                sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
     224                                array( 'key' => $name, 'status' => rest_authorization_required_code() )
     225                        );
     226                }
     227
     228                $meta_type  = $this->get_meta_type();
     229                $meta_key   = wp_slash( $name );
     230                $meta_value = wp_slash( $value );
     231
     232                // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
     233                $old_value = get_metadata( $meta_type, $object_id, $meta_key );
     234                if ( 1 === count( $old_value ) ) {
     235                        if ( $old_value[0] === $meta_value ) {
     236                                return true;
     237                        }
     238                }
     239
     240                if ( ! update_metadata( $meta_type, $object_id, $meta_key, $meta_value ) ) {
     241                        return new WP_Error(
     242                                'rest_meta_database_error',
     243                                __( 'Could not update meta value in database.' ),
     244                                array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
     245                        );
     246                }
     247
     248                return true;
     249        }
     250
     251        /**
     252         * Get all the registered meta fields.
     253         *
     254         * @return array
     255         */
     256        protected function get_registered_fields() {
     257                $registered = array();
     258
     259                foreach ( get_registered_meta_keys( $this->get_meta_type() ) as $name => $args ) {
     260                        if ( empty( $args['show_in_rest'] ) ) {
     261                                continue;
     262                        }
     263
     264                        $rest_args = array();
     265                        if ( is_array( $args['show_in_rest'] ) ) {
     266                                $rest_args = $args['show_in_rest'];
     267                        }
     268
     269                        $default_args = array(
     270                                'name'             => $name,
     271                                'single'           => $args['single'],
     272                                'schema'           => array(),
     273                                'prepare_callback' => array( $this, 'prepare_value' ),
     274                        );
     275                        $default_schema = array(
     276                                'type'        => null,
     277                                'description' => empty( $args['description'] ) ? '' : $args['description'],
     278                                'default'     => isset( $args['default'] ) ? $args['default'] : null,
     279                        );
     280                        $rest_args = array_merge( $default_args, $rest_args );
     281                        $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
     282
     283                        if ( empty( $rest_args['schema']['type'] ) ) {
     284                                // Skip over meta fields that don't have a defined type.
     285                                if ( empty( $args['type'] ) ) {
     286                                        continue;
     287                                }
     288
     289                                if ( $rest_args['single'] ) {
     290                                        $rest_args['schema']['type'] = $args['type'];
     291                                } else {
     292                                        $rest_args['schema']['type'] = 'array';
     293                                        $rest_args['schema']['items'] = array(
     294                                                'type' => $args['type'],
     295                                        );
     296                                }
     297                        }
     298
     299                        $registered[ $rest_args['name'] ] = $rest_args;
     300                } // End foreach().
     301
     302                return $registered;
     303        }
     304
     305        /**
     306         * Get the object's `meta` schema, conforming to JSON Schema.
     307         *
     308         * @return array
     309         */
     310        public function get_field_schema() {
     311                $fields = $this->get_registered_fields();
     312
     313                $schema = array(
     314                        'description' => __( 'Meta fields.' ),
     315                        'type'        => 'object',
     316                        'context'     => array( 'view', 'edit' ),
     317                        'properties'  => array(),
     318                );
     319
     320                foreach ( $fields as $key => $args ) {
     321                        $schema['properties'][ $key ] = $args['schema'];
     322                }
     323
     324                return $schema;
     325        }
     326
     327        /**
     328         * Prepare a meta value for output.
     329         *
     330         * Default preparation for meta fields. Override by passing the
     331         * `prepare_callback` in your `show_in_rest` options.
     332         *
     333         * @param mixed           $value   Meta value from the database.
     334         * @param WP_REST_Request $request Request object.
     335         * @param array           $args    REST-specific options for the meta key.
     336         * @return mixed Value prepared for output.
     337         */
     338        public static function prepare_value( $value, $request, $args ) {
     339                $type = $args['schema']['type'];
     340
     341                // For multi-value fields, check the item type instead.
     342                if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) {
     343                        $type = $args['schema']['items']['type'];
     344                }
     345
     346                switch ( $type ) {
     347                        case 'string':
     348                                $value = (string) $value;
     349                                break;
     350                        case 'number':
     351                                $value = (float) $value;
     352                                break;
     353                        case 'boolean':
     354                                $value = (bool) $value;
     355                                break;
     356                }
     357
     358                // Don't allow objects to be output.
     359                if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) {
     360                        return null;
     361                }
     362
     363                return $value;
     364        }
     365}
  • src/wp-includes/rest-api/fields/class-wp-rest-post-meta-fields.php

     
     1<?php
     2
     3class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields {
     4        /**
     5         * Post type to register fields for.
     6         *
     7         * @var string
     8         */
     9        protected $post_type;
     10
     11        /**
     12         * Constructor.
     13         *
     14         * @param string $post_type Post type to register fields for.
     15         */
     16        public function __construct( $post_type ) {
     17                $this->post_type = $post_type;
     18        }
     19
     20        /**
     21         * Get the object type for meta.
     22         *
     23         * @return string
     24         */
     25        protected function get_meta_type() {
     26                return 'post';
     27        }
     28
     29        /**
     30         * Get the type for `register_rest_field`.
     31         *
     32         * @return string Custom post type slug.
     33         */
     34        public function get_rest_field_type() {
     35                return $this->post_type;
     36        }
     37}
  • src/wp-includes/rest-api/fields/class-wp-rest-term-meta-fields.php

     
     1<?php
     2
     3/**
     4 * Manage meta values for terms.
     5 */
     6class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields {
     7        /**
     8         * Taxonomy to register fields for.
     9         *
     10         * @var string
     11         */
     12        protected $taxonomy;
     13        /**
     14         * Constructor.
     15         *
     16         * @param string $taxonomy Taxonomy to register fields for.
     17         */
     18        public function __construct( $taxonomy ) {
     19                $this->taxonomy = $taxonomy;
     20        }
     21
     22        /**
     23         * Get the object type for meta.
     24         *
     25         * @return string
     26         */
     27        protected function get_meta_type() {
     28                return 'term';
     29        }
     30
     31        /**
     32         * Get the type for `register_rest_field`.
     33         *
     34         * @return string
     35         */
     36        public function get_rest_field_type() {
     37                return 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy;
     38        }
     39}
  • src/wp-includes/rest-api/fields/class-wp-rest-user-meta-fields.php

     
     1<?php
     2
     3class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields {
     4        /**
     5         * Get the object type for meta.
     6         *
     7         * @return string
     8         */
     9        protected function get_meta_type() {
     10                return 'user';
     11        }
     12
     13        /**
     14         * Get the type for `register_rest_field`.
     15         *
     16         * @return string
     17         */
     18        public function get_rest_field_type() {
     19                return 'user';
     20        }
     21}
  • src/wp-includes/rest-api.php

     
    7171}
    7272
    7373/**
     74 * Registers a new field on an existing WordPress object type.
     75 *
     76 * @since 4.7.0
     77 *
     78 * @global array $wp_rest_additional_fields Holds registered fields, organized
     79 *                                          by object type.
     80 *
     81 * @param string|array $object_type Object(s) the field is being registered
     82 *                                  to, "post"|"term"|"comment" etc.
     83 * @param string $attribute         The attribute name.
     84 * @param array  $args {
     85 *     Optional. An array of arguments used to handle the registered field.
     86 *
     87 *     @type string|array|null $get_callback    Optional. The callback function used to retrieve the field
     88 *                                              value. Default is 'null', the field will not be returned in
     89 *                                              the response.
     90 *     @type string|array|null $update_callback Optional. The callback function used to set and update the
     91 *                                              field value. Default is 'null', the value cannot be set or
     92 *                                              updated.
     93 *     @type string|array|null $schema          Optional. The callback function used to create the schema for
     94 *                                              this field. Default is 'null', no schema entry will be returned.
     95 * }
     96 */
     97function register_rest_field( $object_type, $attribute, $args = array() ) {
     98        $defaults = array(
     99                'get_callback'    => null,
     100                'update_callback' => null,
     101                'schema'          => null,
     102        );
     103
     104        $args = wp_parse_args( $args, $defaults );
     105
     106        global $wp_rest_additional_fields;
     107
     108        $object_types = (array) $object_type;
     109
     110        foreach ( $object_types as $object_type ) {
     111                $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
     112        }
     113}
     114
     115/**
    74116 * Registers rewrite rules for the API.
    75117 *
    76118 * @since 4.4.0
     
    125167}
    126168
    127169/**
     170 * Registers default REST API routes.
     171 *
     172 * @since 4.7.0
     173 */
     174function create_initial_rest_routes() {
     175        foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
     176                $class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller';
     177
     178                if ( ! class_exists( $class ) ) {
     179                        continue;
     180                }
     181                $controller = new $class( $post_type->name );
     182                if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
     183                        continue;
     184                }
     185
     186                $controller->register_routes();
     187
     188                if ( post_type_supports( $post_type->name, 'revisions' ) ) {
     189                        $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
     190                        $revisions_controller->register_routes();
     191                }
     192        }
     193
     194        // Post types.
     195        $controller = new WP_REST_Post_Types_Controller;
     196        $controller->register_routes();
     197
     198        // Post statuses.
     199        $controller = new WP_REST_Post_Statuses_Controller;
     200        $controller->register_routes();
     201
     202        // Taxonomies.
     203        $controller = new WP_REST_Taxonomies_Controller;
     204        $controller->register_routes();
     205
     206        // Terms.
     207        foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
     208                $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
     209
     210                if ( ! class_exists( $class ) ) {
     211                        continue;
     212                }
     213                $controller = new $class( $taxonomy->name );
     214                if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
     215                        continue;
     216                }
     217
     218                $controller->register_routes();
     219        }
     220
     221        // Users.
     222        $controller = new WP_REST_Users_Controller;
     223        $controller->register_routes();
     224
     225        // Comments.
     226        $controller = new WP_REST_Comments_Controller;
     227        $controller->register_routes();
     228
     229        // Settings.
     230        $controller = new WP_REST_Settings_Controller;
     231        $controller->register_routes();
     232}
     233
     234/**
    128235 * Loads the REST API.
    129236 *
    130237 * @since 4.4.0
     
    683790
    684791        return array( $local, $utc );
    685792}
     793
     794/**
     795 * Returns a contextual HTTP error code for authorization failure.
     796 *
     797 * @since 4.7.0
     798 *
     799 * @return integer 401 if the user is not logged in, 403 if the user is logged in.
     800 */
     801function rest_authorization_required_code() {
     802        return is_user_logged_in() ? 403 : 401;
     803}
     804
     805/**
     806 * Validate a request argument based on details registered to the route.
     807 *
     808 * @since 4.7.0
     809 *
     810 * @param  mixed            $value
     811 * @param  WP_REST_Request  $request
     812 * @param  string           $param
     813 * @return WP_Error|boolean
     814 */
     815function rest_validate_request_arg( $value, $request, $param ) {
     816        $attributes = $request->get_attributes();
     817        if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
     818                return true;
     819        }
     820        $args = $attributes['args'][ $param ];
     821
     822        if ( ! empty( $args['enum'] ) ) {
     823                if ( ! in_array( $value, $args['enum'] ) ) {
     824                        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'] ) ) );
     825                }
     826        }
     827
     828        if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) {
     829                return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
     830        }
     831
     832        if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
     833                return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) );
     834        }
     835
     836        if ( 'string' === $args['type'] && ! is_string( $value ) ) {
     837                return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
     838        }
     839
     840        if ( isset( $args['format'] ) ) {
     841                switch ( $args['format'] ) {
     842                        case 'date-time' :
     843                                if ( ! rest_parse_date( $value ) ) {
     844                                        return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) );
     845                                }
     846                                break;
     847
     848                        case 'email' :
     849                                if ( ! is_email( $value ) ) {
     850                                        return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) );
     851                                }
     852                                break;
     853                        case 'ipv4' :
     854                                if ( ! rest_is_ip_address( $value ) ) {
     855                                        return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) );
     856                                }
     857                                break;
     858                }
     859        }
     860
     861        if ( in_array( $args['type'], array( 'numeric', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
     862                if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
     863                        if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
     864                                return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) );
     865                        } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
     866                                return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) );
     867                        }
     868                } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
     869                        if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
     870                                return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) );
     871                        } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
     872                                return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) );
     873                        }
     874                } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
     875                        if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
     876                                if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
     877                                        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'] ) );
     878                                }
     879                        } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
     880                                if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
     881                                        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'] ) );
     882                                }
     883                        } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
     884                                if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
     885                                        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'] ) );
     886                                }
     887                        } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
     888                                if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
     889                                        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'] ) );
     890                                }
     891                        }
     892                }
     893        }
     894
     895        return true;
     896}
     897
     898/**
     899 * Sanitize a request argument based on details registered to the route.
     900 *
     901 * @since 4.7.0
     902 *
     903 * @param  mixed            $value
     904 * @param  WP_REST_Request  $request
     905 * @param  string           $param
     906 * @return mixed
     907 */
     908function rest_sanitize_request_arg( $value, $request, $param ) {
     909        $attributes = $request->get_attributes();
     910        if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
     911                return $value;
     912        }
     913        $args = $attributes['args'][ $param ];
     914
     915        if ( 'integer' === $args['type'] ) {
     916                return (int) $value;
     917        }
     918
     919        if ( 'boolean' === $args['type'] ) {
     920                return rest_sanitize_boolean( $value );
     921        }
     922
     923        if ( isset( $args['format'] ) ) {
     924                switch ( $args['format'] ) {
     925                        case 'date-time' :
     926                                return sanitize_text_field( $value );
     927
     928                        case 'email' :
     929                                /*
     930                                 * sanitize_email() validates, which would be unexpected
     931                                 */
     932                                return sanitize_text_field( $value );
     933
     934                        case 'uri' :
     935                                return esc_url_raw( $value );
     936
     937                        case 'ipv4' :
     938                                return sanitize_text_field( $value );
     939                }
     940        }
     941
     942        return $value;
     943}
     944
     945/**
     946 * Parse a request argument based on details registered to the route.
     947 *
     948 * Runs a validation check and sanitizes the value, primarily to be used via
     949 * the `sanitize_callback` arguments in the endpoint args registration.
     950 *
     951 * @since 4.7.0
     952 *
     953 * @param  mixed            $value
     954 * @param  WP_REST_Request  $request
     955 * @param  string           $param
     956 * @return mixed
     957 */
     958function rest_parse_request_arg( $value, $request, $param ) {
     959        $is_valid = rest_validate_request_arg( $value, $request, $param );
     960
     961        if ( is_wp_error( $is_valid ) ) {
     962                return $is_valid;
     963        }
     964
     965        $value = rest_sanitize_request_arg( $value, $request, $param );
     966
     967        return $value;
     968}
     969
     970/**
     971 * Determines if a IPv4 address is valid.
     972 *
     973 * Does not handle IPv6 addresses.
     974 *
     975 * @since 4.7.0
     976 *
     977 * @param  string $ipv4 IP 32-bit address.
     978 * @return string|false The valid IPv4 address, otherwise false.
     979 */
     980function rest_is_ip_address( $ipv4 ) {
     981        $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]?)$/';
     982
     983        if ( ! preg_match( $pattern, $ipv4 ) ) {
     984                return false;
     985        }
     986
     987        return $ipv4;
     988}
     989
     990/**
     991 * Changes a boolean-like value into the proper boolean value.
     992 *
     993 * @since 4.7.0
     994 *
     995 * @param bool|string|int $value The value being evaluated.
     996 * @return boolean Returns the proper associated boolean value.
     997 */
     998function rest_sanitize_boolean( $value ) {
     999        // String values are translated to `true`; make sure 'false' is false.
     1000        if ( is_string( $value )  ) {
     1001                $value = strtolower( $value );
     1002                if ( in_array( $value, array( 'false', '0' ), true ) ) {
     1003                        $value = false;
     1004                }
     1005        }
     1006
     1007        // Everything else will map nicely to boolean.
     1008        return (boolean) $value;
     1009}
     1010
     1011/**
     1012 * Determines if a given value is boolean-like.
     1013 *
     1014 * @since 4.7.0
     1015 *
     1016 * @param bool|string $maybe_bool The value being evaluated.
     1017 * @return boolean True if a boolean, otherwise false.
     1018 */
     1019function rest_is_boolean( $maybe_bool ) {
     1020        if ( is_bool( $maybe_bool ) ) {
     1021                return true;
     1022        }
     1023
     1024        if ( is_string( $maybe_bool ) ) {
     1025                $maybe_bool = strtolower( $maybe_bool );
     1026
     1027                $valid_boolean_values = array(
     1028                        'false',
     1029                        'true',
     1030                        '0',
     1031                        '1',
     1032                );
     1033
     1034                return in_array( $maybe_bool, $valid_boolean_values, true );
     1035        }
     1036
     1037        if ( is_int( $maybe_bool ) ) {
     1038                return in_array( $maybe_bool, array( 0, 1 ), true );
     1039        }
     1040
     1041        return false;
     1042}
     1043
     1044/**
     1045 * Retrieves the avatar urls in various sizes based on a given email address.
     1046 *
     1047 * @since 4.7.0
     1048 *
     1049 * @see get_avatar_url()
     1050 *
     1051 * @param string $email Email address.
     1052 * @return array $urls Gravatar url for each size.
     1053 */
     1054function rest_get_avatar_urls( $email ) {
     1055        $avatar_sizes = rest_get_avatar_sizes();
     1056
     1057        $urls = array();
     1058        foreach ( $avatar_sizes as $size ) {
     1059                $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
     1060        }
     1061
     1062        return $urls;
     1063}
     1064
     1065/**
     1066 * Retrieves the pixel sizes for avatars.
     1067 *
     1068 * @since 4.7.0
     1069 *
     1070 * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
     1071 */
     1072function rest_get_avatar_sizes() {
     1073        /**
     1074         * Filter the REST avatar sizes.
     1075         *
     1076         * Use this filter to adjust the array of sizes returned by the
     1077         * `rest_get_avatar_sizes` function.
     1078         *
     1079         * @since 4.4.0
     1080         *
     1081         * @param array $sizes An array of int values that are the pixel sizes for avatars.
     1082         *                     Default `[ 24, 48, 96 ]`.
     1083         */
     1084        return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
     1085}
  • src/wp-includes/script-loader.php

     
    499499        $scripts->add( 'media-audiovideo', "/wp-includes/js/media-audiovideo$suffix.js", array( 'media-editor' ), false, 1 );
    500500        $scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'jquery', 'media-views', 'media-audiovideo' ), false, 1 );
    501501
     502        $scripts->add( 'wp-api', "/wp-includes/js/wp-api$suffix.js", array( 'jquery', 'backbone', 'underscore' ), false, 1 );
     503        did_action( 'init' ) && $scripts->localize( 'wp-api', 'wpApiSettings', array(
     504                'root'          => esc_url_raw( get_rest_url() ),
     505                'nonce'         => wp_create_nonce( 'wp_rest' ),
     506                'versionString' => 'wp/v2/',
     507        ) );
     508
    502509        if ( is_admin() ) {
    503510                $scripts->add( 'admin-tags', "/wp-admin/js/tags$suffix.js", array( 'jquery', 'wp-ajax-response' ), false, 1 );
    504511                did_action( 'init' ) && $scripts->localize( 'admin-tags', 'tagsl10n', array(
  • src/wp-includes/taxonomy.php

     
    6767                        'delete_terms' => 'delete_categories',
    6868                        'assign_terms' => 'assign_categories',
    6969                ),
     70                'show_in_rest' => true,
     71                'rest_base' => 'categories',
     72                'rest_controller_class' => 'WP_REST_Terms_Controller',
    7073        ) );
    7174
    7275        register_taxonomy( 'post_tag', 'post', array(
     
    8386                        'delete_terms' => 'delete_post_tags',
    8487                        'assign_terms' => 'assign_post_tags',
    8588                ),
     89                'show_in_rest' => true,
     90                'rest_base' => 'tags',
     91                'rest_controller_class' => 'WP_REST_Terms_Controller',
    8692        ) );
    8793
    8894        register_taxonomy( 'nav_menu', 'nav_menu_item', array(
  • src/wp-settings.php

     
    218218require( ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php' );
    219219require( ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php' );
    220220require( ABSPATH . WPINC . '/rest-api/class-wp-rest-request.php' );
     221require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-controller.php' );
     222require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-posts-controller.php' );
     223require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-attachments-controller.php' );
     224require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-types-controller.php' );
     225require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-statuses-controller.php' );
     226require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-revisions-controller.php' );
     227require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-taxonomies-controller.php' );
     228require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.php' );
     229require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' );
     230require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php' );
     231require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' );
     232require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' );
     233require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php' );
     234require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' );
     235require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-term-meta-fields.php' );
     236require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php' );
    221237
    222238$GLOBALS['wp_embed'] = new WP_Embed();
    223239
  • tests/phpunit/includes/bootstrap.php

     
    4040define( 'WP_MEMORY_LIMIT', -1 );
    4141define( 'WP_MAX_MEMORY_LIMIT', -1 );
    4242
     43define( 'REST_TESTS_IMPOSSIBLY_HIGH_NUMBER', 99999999 );
     44
    4345$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php';
    4446
    4547// Should we run in multisite mode?
     
    8890
    8991require dirname( __FILE__ ) . '/testcase.php';
    9092require dirname( __FILE__ ) . '/testcase-rest-api.php';
     93require dirname( __FILE__ ) . '/testcase-rest-controller.php';
     94require dirname( __FILE__ ) . '/testcase-rest-post-type-controller.php';
    9195require dirname( __FILE__ ) . '/testcase-xmlrpc.php';
    9296require dirname( __FILE__ ) . '/testcase-ajax.php';
    9397require dirname( __FILE__ ) . '/testcase-canonical.php';
  • tests/phpunit/includes/testcase-rest-controller.php

     
     1<?php
     2
     3abstract class WP_Test_REST_Controller_Testcase extends WP_Test_REST_TestCase {
     4
     5        protected $server;
     6
     7        public function setUp() {
     8                parent::setUp();
     9                add_filter( 'rest_url', array( $this, 'filter_rest_url_for_leading_slash' ), 10, 2 );
     10                /** @var WP_REST_Server $wp_rest_server */
     11                global $wp_rest_server;
     12                $this->server = $wp_rest_server = new WP_Test_Spy_REST_Server;
     13                do_action( 'rest_api_init' );
     14        }
     15
     16        public function tearDown() {
     17                parent::tearDown();
     18                remove_filter( 'rest_url', array( $this, 'test_rest_url_for_leading_slash' ), 10, 2 );
     19                /** @var WP_REST_Server $wp_rest_server */
     20                global $wp_rest_server;
     21                $wp_rest_server = null;
     22        }
     23
     24        abstract public function test_register_routes();
     25
     26        abstract public function test_context_param();
     27
     28        abstract public function test_get_items();
     29
     30        abstract public function test_get_item();
     31
     32        abstract public function test_create_item();
     33
     34        abstract public function test_update_item();
     35
     36        abstract public function test_delete_item();
     37
     38        abstract public function test_prepare_item();
     39
     40        abstract public function test_get_item_schema();
     41
     42        public function filter_rest_url_for_leading_slash( $url, $path ) {
     43                // Make sure path for rest_url has a leading slash for proper resolution.
     44                $this->assertTrue( 0 === strpos( $path, '/' ) );
     45
     46                return $url;
     47        }
     48}
  • tests/phpunit/includes/testcase-rest-post-type-controller.php

     
     1<?php
     2
     3abstract class WP_Test_REST_Post_Type_Controller_Testcase extends WP_Test_REST_Controller_Testcase {
     4
     5        protected function check_post_data( $post, $data, $context, $links ) {
     6                $post_type_obj = get_post_type_object( $post->post_type );
     7
     8                // Standard fields
     9                $this->assertEquals( $post->ID, $data['id'] );
     10                $this->assertEquals( $post->post_name, $data['slug'] );
     11                $this->assertEquals( get_permalink( $post->ID ), $data['link'] );
     12                if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
     13                        $this->assertNull( $data['date_gmt'] );
     14                }
     15                $this->assertEquals( mysql_to_rfc3339( $post->post_date ), $data['date'] );
     16
     17                if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
     18                        $this->assertNull( $data['modified_gmt'] );
     19                }
     20                $this->assertEquals( mysql_to_rfc3339( $post->post_modified ), $data['modified'] );
     21
     22                // author
     23                if ( post_type_supports( $post->post_type, 'author' ) ) {
     24                        $this->assertEquals( $post->post_author, $data['author'] );
     25                } else {
     26                        $this->assertEmpty( $data['author'] );
     27                }
     28
     29                // post_parent
     30                if ( $post_type_obj->hierarchical ) {
     31                        $this->assertArrayHasKey( 'parent', $data );
     32                        if ( $post->post_parent ) {
     33                                if ( is_int( $data['parent'] ) ) {
     34                                        $this->assertEquals( $post->post_parent, $data['parent'] );
     35                                } else {
     36                                        $this->assertEquals( $post->post_parent, $data['parent']['id'] );
     37                                        $this->check_get_post_response( $data['parent'], get_post( $data['parent']['id'] ), 'view-parent' );
     38                                }
     39                        } else {
     40                                $this->assertEmpty( $data['parent'] );
     41                        }
     42                } else {
     43                        $this->assertFalse( isset( $data['parent'] ) );
     44                }
     45
     46                // page attributes
     47                if ( $post_type_obj->hierarchical && post_type_supports( $post->post_type, 'page-attributes' ) ) {
     48                        $this->assertEquals( $post->menu_order, $data['menu_order'] );
     49                } else {
     50                        $this->assertFalse( isset( $data['menu_order'] ) );
     51                }
     52
     53                // Comments
     54                if ( post_type_supports( $post->post_type, 'comments' ) ) {
     55                        $this->assertEquals( $post->comment_status, $data['comment_status'] );
     56                        $this->assertEquals( $post->ping_status, $data['ping_status'] );
     57                } else {
     58                        $this->assertFalse( isset( $data['comment_status'] ) );
     59                        $this->assertFalse( isset( $data['ping_status'] ) );
     60                }
     61
     62                if ( 'post' === $post->post_type ) {
     63                        $this->assertEquals( is_sticky( $post->ID ), $data['sticky'] );
     64                }
     65
     66                if ( 'post' === $post->post_type && 'edit' === $context ) {
     67                        $this->assertEquals( $post->post_password, $data['password'] );
     68                }
     69
     70                if ( 'page' === $post->post_type ) {
     71                        $this->assertEquals( get_page_template_slug( $post->ID ), $data['template'] );
     72                }
     73
     74                if ( post_type_supports( $post->post_type, 'thumbnail' ) ) {
     75                        $this->assertEquals( (int) get_post_thumbnail_id( $post->ID ), $data['featured_media'] );
     76                } else {
     77                        $this->assertFalse( isset( $data['featured_media'] ) );
     78                }
     79
     80                // Check post format.
     81                if ( post_type_supports( $post->post_type, 'post-formats' ) ) {
     82                        $post_format = get_post_format( $post->ID );
     83                        if ( empty( $post_format ) ) {
     84                                $this->assertEquals( 'standard', $data['format'] );
     85                        } else {
     86                                $this->assertEquals( get_post_format( $post->ID ), $data['format'] );
     87                        }
     88                } else {
     89                        $this->assertFalse( isset( $data['format'] ) );
     90                }
     91
     92                // Check filtered values.
     93                if ( post_type_supports( $post->post_type, 'title' ) ) {
     94                        add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
     95                        $this->assertEquals( get_the_title( $post->ID ), $data['title']['rendered'] );
     96                        remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
     97                        if ( 'edit' === $context ) {
     98                                $this->assertEquals( $post->post_title, $data['title']['raw'] );
     99                        } else {
     100                                $this->assertFalse( isset( $data['title']['raw'] ) );
     101                        }
     102                } else {
     103                        $this->assertFalse( isset( $data['title'] ) );
     104                }
     105
     106                if ( post_type_supports( $post->post_type, 'editor' ) ) {
     107                        // TODO: apply content filter for more accurate testing.
     108                        if ( ! $post->post_password ) {
     109                                $this->assertEquals( wpautop( $post->post_content ), $data['content']['rendered'] );
     110                        }
     111
     112                        if ( 'edit' === $context ) {
     113                                $this->assertEquals( $post->post_content, $data['content']['raw'] );
     114                        } else {
     115                                $this->assertFalse( isset( $data['content']['raw'] ) );
     116                        }
     117                } else {
     118                        $this->assertFalse( isset( $data['content'] ) );
     119                }
     120
     121                if ( post_type_supports( $post->post_type, 'excerpt' ) ) {
     122                        if ( empty( $post->post_password ) ) {
     123                                // TODO: apply excerpt filter for more accurate testing.
     124                                $this->assertEquals( wpautop( $post->post_excerpt ), $data['excerpt']['rendered'] );
     125                        } else {
     126                                // TODO: better testing for excerpts for password protected posts.
     127                        }
     128                        if ( 'edit' === $context ) {
     129                                $this->assertEquals( $post->post_excerpt, $data['excerpt']['raw'] );
     130                        } else {
     131                                $this->assertFalse( isset( $data['excerpt']['raw'] ) );
     132                        }
     133                } else {
     134                        $this->assertFalse( isset( $data['excerpt'] ) );
     135                }
     136
     137                $this->assertEquals( $post->guid, $data['guid']['rendered'] );
     138
     139                if ( 'edit' === $context ) {
     140                        $this->assertEquals( $post->guid, $data['guid']['raw'] );
     141                        $this->assertEquals( $post->post_status, $data['status'] );
     142
     143                        if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
     144                                $this->assertNull( $data['date_gmt'] );
     145                        } else {
     146                                $this->assertEquals( mysql_to_rfc3339( $post->post_date_gmt ), $data['date_gmt'] );
     147                        }
     148
     149                        if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
     150                                $this->assertNull( $data['modified_gmt'] );
     151                        } else {
     152                                $this->assertEquals( mysql_to_rfc3339( $post->post_modified_gmt ), $data['modified_gmt'] );
     153                        }
     154                }
     155
     156                $taxonomies = wp_list_filter( get_object_taxonomies( $post->post_type, 'objects' ), array( 'show_in_rest' => true ) );
     157                foreach ( $taxonomies as $taxonomy ) {
     158                        $this->assertTrue( isset( $data[ $taxonomy->rest_base ] ) );
     159                        $terms = wp_get_object_terms( $post->ID, $taxonomy->name, array( 'fields' => 'ids' ) );
     160                        sort( $terms );
     161                        sort( $data[ $taxonomy->rest_base ] );
     162                        $this->assertEquals( $terms, $data[ $taxonomy->rest_base ] );
     163                }
     164
     165                // test links
     166                if ( $links ) {
     167
     168                        $links = test_rest_expand_compact_links( $links );
     169                        $post_type = get_post_type_object( $data['type'] );
     170                        $this->assertEquals( $links['self'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base . '/' . $data['id'] ) );
     171                        $this->assertEquals( $links['collection'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base ) );
     172                        $this->assertEquals( $links['about'][0]['href'], rest_url( 'wp/v2/types/' . $data['type'] ) );
     173
     174                        if ( post_type_supports( $post->post_type, 'author' ) && $data['author'] ) {
     175                                $this->assertEquals( $links['author'][0]['href'], rest_url( 'wp/v2/users/' . $data['author'] ) );
     176                        }
     177
     178                        if ( post_type_supports( $post->post_type, 'comments' ) ) {
     179                                $this->assertEquals( $links['replies'][0]['href'], add_query_arg( 'post', $data['id'], rest_url( 'wp/v2/comments' ) ) );
     180                        }
     181
     182                        if ( post_type_supports( $post->post_type, 'revisions' ) ) {
     183                                $this->assertEquals( $links['version-history'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base . '/' . $data['id'] . '/revisions' ) );
     184                        }
     185
     186                        if ( $post_type->hierarchical && ! empty( $data['parent'] ) ) {
     187                                $this->assertEquals( $links['up'][0]['href'], rest_url( 'wp/v2/' . $post_type->rest_base . '/' . $data['parent'] ) );
     188                        }
     189
     190                        if ( ! in_array( $data['type'], array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
     191                                $this->assertEquals( $links['https://api.w.org/attachment'][0]['href'], add_query_arg( 'parent', $data['id'], rest_url( 'wp/v2/media' ) ) );
     192                        }
     193
     194                        if ( ! empty( $data['featured_media'] ) ) {
     195                                $this->assertEquals( $links['https://api.w.org/featuredmedia'][0]['href'], rest_url( 'wp/v2/media/' . $data['featured_media'] ) );
     196                        }
     197
     198                        $num = 0;
     199                        foreach ( $taxonomies as $key => $taxonomy ) {
     200                                $this->assertEquals( $taxonomy->name, $links['https://api.w.org/term'][ $num ]['attributes']['taxonomy'] );
     201                                $this->assertEquals( add_query_arg( 'post', $data['id'], rest_url( 'wp/v2/' . $taxonomy->rest_base ) ), $links['https://api.w.org/term'][ $num ]['href'] );
     202                                $num++;
     203                        }
     204                }
     205
     206        }
     207
     208        protected function check_get_posts_response( $response, $context = 'view' ) {
     209                $this->assertNotInstanceOf( 'WP_Error', $response );
     210                $response = rest_ensure_response( $response );
     211                $this->assertEquals( 200, $response->get_status() );
     212
     213                $headers = $response->get_headers();
     214                $this->assertArrayHasKey( 'X-WP-Total', $headers );
     215                $this->assertArrayHasKey( 'X-WP-TotalPages', $headers );
     216
     217                $all_data = $response->get_data();
     218                foreach ( $all_data as $data ) {
     219                        $post = get_post( $data['id'] );
     220                        // as the links for the post are "response_links" format in the data array we have to pull them
     221                        // out and parse them.
     222                        $links = $data['_links'];
     223                        foreach ( $links as &$links_array ) {
     224                                foreach ( $links_array as &$link ) {
     225                                        $attributes = array_diff_key( $link, array( 'href' => 1, 'name' => 1 ) );
     226                                        $link = array_diff_key( $link, $attributes );
     227                                        $link['attributes'] = $attributes;
     228                                }
     229                        }
     230
     231                        $this->check_post_data( $post, $data, $context, $links );
     232                }
     233        }
     234
     235        protected function check_get_post_response( $response, $context = 'view' ) {
     236                $this->assertNotInstanceOf( 'WP_Error', $response );
     237                $response = rest_ensure_response( $response );
     238                $this->assertEquals( 200, $response->get_status() );
     239
     240                $data = $response->get_data();
     241                $post = get_post( $data['id'] );
     242                $this->check_post_data( $post, $data, $context, $response->get_links() );
     243
     244        }
     245
     246        protected function check_create_post_response( $response ) {
     247                $this->assertNotInstanceOf( 'WP_Error', $response );
     248                $response = rest_ensure_response( $response );
     249
     250                $this->assertEquals( 201, $response->get_status() );
     251                $headers = $response->get_headers();
     252                $this->assertArrayHasKey( 'Location', $headers );
     253
     254                $data = $response->get_data();
     255                $post = get_post( $data['id'] );
     256                $this->check_post_data( $post, $data, 'edit', $response->get_links() );
     257        }
     258
     259        protected function check_update_post_response( $response ) {
     260                $this->assertNotInstanceOf( 'WP_Error', $response );
     261                $response = rest_ensure_response( $response );
     262
     263                $this->assertEquals( 200, $response->get_status() );
     264                $headers = $response->get_headers();
     265                $this->assertArrayNotHasKey( 'Location', $headers );
     266
     267                $data = $response->get_data();
     268                $post = get_post( $data['id'] );
     269                $this->check_post_data( $post, $data, 'edit', $response->get_links() );
     270        }
     271
     272        protected function set_post_data( $args = array() ) {
     273                $defaults = array(
     274                        'title'   => rand_str(),
     275                        'content' => rand_str(),
     276                        'excerpt' => rand_str(),
     277                        'name'    => 'test',
     278                        'status'  => 'publish',
     279                        'author'  => get_current_user_id(),
     280                        'type'    => 'post',
     281                );
     282
     283                return wp_parse_args( $args, $defaults );
     284        }
     285
     286        protected function set_raw_post_data( $args = array() ) {
     287                return wp_parse_args( $args, $this->set_post_data( array(
     288                        'title'   => array(
     289                                'raw' => rand_str(),
     290                        ),
     291                        'content' => array(
     292                                'raw' => rand_str(),
     293                        ),
     294                        'excerpt' => array(
     295                                'raw' => rand_str(),
     296                        ),
     297                ) ) );
     298        }
     299
     300        /**
     301         * Overwrite the default protected title format.
     302         *
     303         * By default WordPress will show password protected posts with a title of
     304         * "Protected: %s", as the REST API communicates the protected status of a post
     305         * in a machine readable format, we remove the "Protected: " prefix.
     306         *
     307         * @return string
     308         */
     309        public function protected_title_format() {
     310                return '%s';
     311        }
     312}
  • tests/phpunit/tests/rest-api/data/canola.jpg

    Cannot display: file marked as a binary type.
    svn:mime-type = application/octet-stream
  • tests/phpunit/tests/rest-api/data/codeispoetry.png

    Property changes on: tests/phpunit/tests/rest-api/data/canola.jpg
    ___________________________________________________________________
    Added: svn:mime-type
    ## -0,0 +1 ##
    +application/octet-stream
    \ No newline at end of property
     
     1
     2
     3
     4
     5<!DOCTYPE html>
     6<html lang="en" class="">
     7  <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#">
     8    <meta charset='utf-8'>
     9
     10    <link crossorigin="anonymous" href="https://assets-cdn.github.com/assets/github-c1d91683d54d54239f36043630b76fe2b39dbc4fc91cfbd38ef9f3743e56c851.css" media="all" rel="stylesheet" />
     11    <link crossorigin="anonymous" href="https://assets-cdn.github.com/assets/github2-6b792c972e1d61f45ce186e5e65d3410c90cf78d94776abff9512f5417eb2bac.css" media="all" rel="stylesheet" />
     12   
     13   
     14   
     15
     16    <link as="script" href="https://assets-cdn.github.com/assets/frameworks-ee521b8e9facac68ff27e93fc3ae0f8ed811d7bf9e434e84f4b9ea227780b084.js" rel="preload" />
     17    <link as="script" href="https://assets-cdn.github.com/assets/github-863d0e4c2905010278cfd87e9c7c738e812530db73c36c274a185354977a2e41.js" rel="preload" />
     18
     19    <meta http-equiv="X-UA-Compatible" content="IE=edge">
     20    <meta http-equiv="Content-Language" content="en">
     21    <meta name="viewport" content="width=1020">
     22   
     23   
     24    <title>wp-cli.github.com/codeispoetry.png at master · wp-cli/wp-cli.github.com · GitHub</title>
     25    <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub">
     26    <link rel="fluid-icon" href="https://github.com/fluidicon.png" title="GitHub">
     27    <link rel="apple-touch-icon" href="/apple-touch-icon.png">
     28    <link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png">
     29    <link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png">
     30    <link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png">
     31    <link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png">
     32    <link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png">
     33    <link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
     34    <link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png">
     35    <link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png">
     36    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
     37    <meta property="fb:app_id" content="1401488693436528">
     38
     39      <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" />
     40      <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" />
     41      <meta name="browser-stats-url" content="https://api.github.com/_private/browser/stats">
     42    <meta name="browser-errors-url" content="https://api.github.com/_private/browser/errors">
     43    <link rel="assets" href="https://assets-cdn.github.com/">
     44   
     45    <meta name="pjax-timeout" content="1000">
     46   
     47
     48    <meta name="msapplication-TileImage" content="/windows-tile.png">
     49    <meta name="msapplication-TileColor" content="#ffffff">
     50    <meta name="selected-link" value="repo_source" data-pjax-transient>
     51
     52    <meta name="google-site-verification" content="KT5gs8h0wvaagLKAVWq8bbeNwnZZK1r1XQysX3xurLU">
     53<meta name="google-site-verification" content="ZzhVyEFwb7w3e0-uOTltm8Jsck2F5StVihD0exw2fsA">
     54    <meta name="google-analytics" content="UA-3769691-2">
     55
     56<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" />
     57<meta content="/&lt;user-name&gt;/&lt;repo-name&gt;/blob/show" data-pjax-transient="true" name="analytics-location" />
     58
     59
     60
     61  <meta class="js-ga-set" name="dimension1" content="Logged Out">
     62
     63
     64
     65        <meta name="hostname" content="github.com">
     66    <meta name="user-login" content="">
     67
     68        <meta name="expected-hostname" content="github.com">
     69      <meta name="js-proxy-site-detection-payload" content="YmM2NmFhNmJmZjU3OTZhZjZiMWI2MzA3N2E0M2Y2Zjc1OTM5ODcxYjQ1OWEyYzFkMjg3Y2NkOTI1ODhiMzZmNXx7InJlbW90ZV9hZGRyZXNzIjoiNTAuNTMuMTU2LjE2MiIsInJlcXVlc3RfaWQiOiIzMjM1OUNBMjozRjZBOjc4OTgzMjo1NkJBNkIzNyJ9">
     70
     71      <link rel="mask-icon" href="https://assets-cdn.github.com/pinned-octocat.svg" color="#4078c0">
     72      <link rel="icon" type="image/x-icon" href="https://assets-cdn.github.com/favicon.ico">
     73
     74    <meta content="9ca3979ba13c29867416f58457ff40e521a92049" name="form-nonce" />
     75
     76    <meta http-equiv="x-pjax-version" content="88e74eaed8727c649ee4773743f8723a">
     77
     78     
     79  <meta name="description" content="wp-cli.github.com - wp-cli.org website">
     80  <meta name="go-import" content="github.com/wp-cli/wp-cli.github.com git https://github.com/wp-cli/wp-cli.github.com.git">
     81
     82  <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" />
     83  <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">
     84
     85
     86      <link rel="canonical" href="https://github.com/wp-cli/wp-cli.github.com/blob/master/behat-data/codeispoetry.png" data-pjax-transient>
     87  </head>
     88
     89
     90  <body class="logged_out   env-production  vis-public page-blob">
     91    <a href="#start-of-content" tabindex="1" class="accessibility-aid js-skip-to-content">Skip to content</a>
     92
     93   
     94   
     95   
     96
     97
     98
     99     
     100      <div class="header header-logged-out" role="banner">
     101  <div class="container clearfix">
     102
     103    <a class="header-logo-wordmark" href="https://github.com/" data-ga-click="(Logged out) Header, go to homepage, icon:logo-wordmark">
     104      <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.38c0.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>
     105    </a>
     106
     107    <div class="header-actions" role="navigation">
     108        <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>
     109      <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>
     110    </div>
     111
     112    <div class="site-search repo-scope js-site-search" role="search">
     113      <!-- </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>
     114  <label class="js-chromeless-input-container form-control">
     115    <div class="scope-badge">This repository</div>
     116    <input type="text"
     117      class="js-site-search-focus js-site-search-field is-clearable chromeless-input"
     118      data-hotkey="s"
     119      name="q"
     120      placeholder="Search"
     121      aria-label="Search this repository"
     122      data-global-scope-placeholder="Search GitHub"
     123      data-repo-scope-placeholder="Search"
     124      tabindex="1"
     125      autocapitalize="off">
     126  </label>
     127</form>
     128    </div>
     129
     130      <ul class="header-nav left" role="navigation">
     131          <li class="header-nav-item">
     132            <a class="header-nav-link" href="/explore" data-ga-click="(Logged out) Header, go to explore, text:explore">Explore</a>
     133          </li>
     134          <li class="header-nav-item">
     135            <a class="header-nav-link" href="/features" data-ga-click="(Logged out) Header, go to features, text:features">Features</a>
     136          </li>
     137          <li class="header-nav-item">
     138            <a class="header-nav-link" href="https://enterprise.github.com/" data-ga-click="(Logged out) Header, go to enterprise, text:enterprise">Enterprise</a>
     139          </li>
     140          <li class="header-nav-item">
     141            <a class="header-nav-link" href="/pricing" data-ga-click="(Logged out) Header, go to pricing, text:pricing">Pricing</a>
     142          </li>
     143      </ul>
     144
     145  </div>
     146</div>
     147
     148
     149
     150    <div id="start-of-content" class="accessibility-aid"></div>
     151
     152      <div id="js-flash-container">
     153</div>
     154
     155
     156    <div role="main" class="main-content">
     157        <div itemscope itemtype="http://schema.org/WebPage">
     158    <div id="js-repo-pjax-container" class="context-loader-container js-repo-nav-next" data-pjax-container>
     159     
     160<div class="pagehead repohead instapaper_ignore readability-menu experiment-repo-nav">
     161  <div class="container repohead-details-container">
     162
     163   
     164
     165<ul class="pagehead-actions">
     166
     167  <li>
     168      <a href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com"
     169    class="btn btn-sm btn-with-count tooltipped tooltipped-n"
     170    aria-label="You must be signed in to watch a repository" rel="nofollow">
     171    <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>
     172    Watch
     173  </a>
     174  <a class="social-count" href="/wp-cli/wp-cli.github.com/watchers">
     175    13
     176  </a>
     177
     178  </li>
     179
     180  <li>
     181      <a href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com"
     182    class="btn btn-sm btn-with-count tooltipped tooltipped-n"
     183    aria-label="You must be signed in to star a repository" rel="nofollow">
     184    <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>
     185    Star
     186  </a>
     187
     188    <a class="social-count js-social-count" href="/wp-cli/wp-cli.github.com/stargazers">
     189      6
     190    </a>
     191
     192  </li>
     193
     194  <li>
     195      <a href="/login?return_to=%2Fwp-cli%2Fwp-cli.github.com"
     196        class="btn btn-sm btn-with-count tooltipped tooltipped-n"
     197        aria-label="You must be signed in to fork a repository" rel="nofollow">
     198        <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>
     199        Fork
     200      </a>
     201
     202    <a href="/wp-cli/wp-cli.github.com/network" class="social-count">
     203      27
     204    </a>
     205  </li>
     206</ul>
     207
     208    <h1 itemscope itemtype="http://data-vocabulary.org/Breadcrumb" class="entry-title public ">
     209  <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>
     210  <span class="author"><a href="/wp-cli" class="url fn" itemprop="url" rel="author"><span itemprop="title">wp-cli</span></a></span><!--
     211--><span class="path-divider">/</span><!--
     212--><strong><a href="/wp-cli/wp-cli.github.com" data-pjax="#js-repo-pjax-container">wp-cli.github.com</a></strong>
     213
     214  <span class="page-context-loader">
     215    <img alt="" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" />
     216  </span>
     217
     218</h1>
     219
     220  </div>
     221  <div class="container">
     222   
     223<nav class="reponav js-repo-nav js-sidenav-container-pjax js-octicon-loaders"
     224     role="navigation"
     225     data-pjax="#js-repo-pjax-container">
     226
     227  <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">
     228    <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>
     229    Code
     230</a>
     231
     232  <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">
     233    <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>
     234    Pull requests
     235    <span class="counter">3</span>
     236</a>
     237
     238  <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">
     239    <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>
     240    Pulse
     241</a>
     242  <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">
     243    <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>
     244    Graphs
     245</a>
     246
     247</nav>
     248
     249  </div>
     250</div>
     251
     252<div class="container new-discussion-timeline experiment-repo-nav">
     253  <div class="repository-content">
     254
     255   
     256
     257<a href="/wp-cli/wp-cli.github.com/blob/663ab6d46c67329aac052ee829cfced92c98a597/behat-data/codeispoetry.png" class="hidden js-permalink-shortcut" data-hotkey="y">Permalink</a>
     258
     259<!-- blob contrib key: blob_contributors:v21:03324a4d6ead46e4f13ad85cbdf84ead -->
     260
     261<div class="file-navigation js-zeroclipboard-container">
     262 
     263<div class="select-menu js-menu-container js-select-menu left">
     264  <button class="btn btn-sm select-menu-button js-menu-target css-truncate" data-hotkey="w"
     265    title="master"
     266    type="button" aria-label="Switch branches or tags" tabindex="0" aria-haspopup="true">
     267    <i>Branch:</i>
     268    <span class="js-select-button css-truncate-target">master</span>
     269  </button>
     270
     271  <div class="select-menu-modal-holder js-menu-content js-navigation-container" data-pjax aria-hidden="true">
     272
     273    <div class="select-menu-modal">
     274      <div class="select-menu-header">
     275        <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>
     276        <span class="select-menu-title">Switch branches/tags</span>
     277      </div>
     278
     279      <div class="select-menu-filters">
     280        <div class="select-menu-text-filter">
     281          <input type="text" aria-label="Filter branches/tags" id="context-commitish-filter-field" class="js-filterable-field js-navigation-enable" placeholder="Filter branches/tags">
     282        </div>
     283        <div class="select-menu-tabs">
     284          <ul>
     285            <li class="select-menu-tab">
     286              <a href="#" data-tab-filter="branches" data-filter-placeholder="Filter branches/tags" class="js-select-menu-tab" role="tab">Branches</a>
     287            </li>
     288            <li class="select-menu-tab">
     289              <a href="#" data-tab-filter="tags" data-filter-placeholder="Find a tag…" class="js-select-menu-tab" role="tab">Tags</a>
     290            </li>
     291          </ul>
     292        </div>
     293      </div>
     294
     295      <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="branches" role="menu">
     296
     297        <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
     298
     299
     300            <a class="select-menu-item js-navigation-item js-navigation-open "
     301               href="/wp-cli/wp-cli.github.com/blob/add-global-search/behat-data/codeispoetry.png"
     302               data-name="add-global-search"
     303               data-skip-pjax="true"
     304               rel="nofollow">
     305              <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>
     306              <span class="select-menu-item-text css-truncate-target" title="add-global-search">
     307                add-global-search
     308              </span>
     309            </a>
     310            <a class="select-menu-item js-navigation-item js-navigation-open "
     311               href="/wp-cli/wp-cli.github.com/blob/deb-package/behat-data/codeispoetry.png"
     312               data-name="deb-package"
     313               data-skip-pjax="true"
     314               rel="nofollow">
     315              <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>
     316              <span class="select-menu-item-text css-truncate-target" title="deb-package">
     317                deb-package
     318              </span>
     319            </a>
     320            <a class="select-menu-item js-navigation-item js-navigation-open "
     321               href="/wp-cli/wp-cli.github.com/blob/feature/rework-globals-section/behat-data/codeispoetry.png"
     322               data-name="feature/rework-globals-section"
     323               data-skip-pjax="true"
     324               rel="nofollow">
     325              <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>
     326              <span class="select-menu-item-text css-truncate-target" title="feature/rework-globals-section">
     327                feature/rework-globals-section
     328              </span>
     329            </a>
     330            <a class="select-menu-item js-navigation-item js-navigation-open "
     331               href="/wp-cli/wp-cli.github.com/blob/fix-html-command-flag-minus/behat-data/codeispoetry.png"
     332               data-name="fix-html-command-flag-minus"
     333               data-skip-pjax="true"
     334               rel="nofollow">
     335              <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>
     336              <span class="select-menu-item-text css-truncate-target" title="fix-html-command-flag-minus">
     337                fix-html-command-flag-minus
     338              </span>
     339            </a>
     340            <a class="select-menu-item js-navigation-item js-navigation-open "
     341               href="/wp-cli/wp-cli.github.com/blob/highlighting/behat-data/codeispoetry.png"
     342               data-name="highlighting"
     343               data-skip-pjax="true"
     344               rel="nofollow">
     345              <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>
     346              <span class="select-menu-item-text css-truncate-target" title="highlighting">
     347                highlighting
     348              </span>
     349            </a>
     350            <a class="select-menu-item js-navigation-item js-navigation-open selected"
     351               href="/wp-cli/wp-cli.github.com/blob/master/behat-data/codeispoetry.png"
     352               data-name="master"
     353               data-skip-pjax="true"
     354               rel="nofollow">
     355              <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>
     356              <span class="select-menu-item-text css-truncate-target" title="master">
     357                master
     358              </span>
     359            </a>
     360            <a class="select-menu-item js-navigation-item js-navigation-open "
     361               href="/wp-cli/wp-cli.github.com/blob/phpdoc/behat-data/codeispoetry.png"
     362               data-name="phpdoc"
     363               data-skip-pjax="true"
     364               rel="nofollow">
     365              <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>
     366              <span class="select-menu-item-text css-truncate-target" title="phpdoc">
     367                phpdoc
     368              </span>
     369            </a>
     370            <a class="select-menu-item js-navigation-item js-navigation-open "
     371               href="/wp-cli/wp-cli.github.com/blob/post-changes-hightlighting/behat-data/codeispoetry.png"
     372               data-name="post-changes-hightlighting"
     373               data-skip-pjax="true"
     374               rel="nofollow">
     375              <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>
     376              <span class="select-menu-item-text css-truncate-target" title="post-changes-hightlighting">
     377                post-changes-hightlighting
     378              </span>
     379            </a>
     380            <a class="select-menu-item js-navigation-item js-navigation-open "
     381               href="/wp-cli/wp-cli.github.com/blob/post-contributing/behat-data/codeispoetry.png"
     382               data-name="post-contributing"
     383               data-skip-pjax="true"
     384               rel="nofollow">
     385              <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>
     386              <span class="select-menu-item-text css-truncate-target" title="post-contributing">
     387                post-contributing
     388              </span>
     389            </a>
     390            <a class="select-menu-item js-navigation-item js-navigation-open "
     391               href="/wp-cli/wp-cli.github.com/blob/redesign-2014/behat-data/codeispoetry.png"
     392               data-name="redesign-2014"
     393               data-skip-pjax="true"
     394               rel="nofollow">
     395              <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>
     396              <span class="select-menu-item-text css-truncate-target" title="redesign-2014">
     397                redesign-2014
     398              </span>
     399            </a>
     400        </div>
     401
     402          <div class="select-menu-no-results">Nothing to show</div>
     403      </div>
     404
     405      <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="tags">
     406        <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
     407
     408
     409        </div>
     410
     411        <div class="select-menu-no-results">Nothing to show</div>
     412      </div>
     413
     414    </div>
     415  </div>
     416</div>
     417
     418  <div class="btn-group right">
     419    <a href="/wp-cli/wp-cli.github.com/find/master"
     420          class="js-show-file-finder btn btn-sm"
     421          data-pjax
     422          data-hotkey="t">
     423      Find file
     424    </a>
     425    <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>
     426  </div>
     427  <div class="breadcrumb js-zeroclipboard-target">
     428    <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>
     429  </div>
     430</div>
     431
     432
     433  <div class="commit-tease">
     434      <span class="right">
     435        <a class="commit-tease-sha" href="/wp-cli/wp-cli.github.com/commit/9a03d83f0b0cfe167a8927cf199f4113cb5bd7f3" data-pjax>
     436          9a03d83
     437        </a>
     438        <time datetime="2016-02-03T13:13:36Z" is="relative-time">Feb 3, 2016</time>
     439      </span>
     440      <div>
     441        <img alt="@danielbachhuber" class="avatar" height="20" src="https://avatars3.githubusercontent.com/u/36432?v=3&amp;s=40" width="20" />
     442        <a href="/danielbachhuber" class="user-mention" rel="contributor">danielbachhuber</a>
     443          <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>
     444      </div>
     445
     446    <div class="commit-tease-contributors">
     447      <a class="muted-link contributors-toggle" href="#blob_contributors_box" rel="facebox">
     448        <strong>1</strong>
     449         contributor
     450      </a>
     451     
     452    </div>
     453
     454    <div id="blob_contributors_box" style="display:none">
     455      <h2 class="facebox-header" data-facebox-id="facebox-header">Users who have contributed to this file</h2>
     456      <ul class="facebox-user-list" data-facebox-id="facebox-description">
     457          <li class="facebox-user-list-item">
     458            <img alt="@danielbachhuber" height="24" src="https://avatars1.githubusercontent.com/u/36432?v=3&amp;s=48" width="24" />
     459            <a href="/danielbachhuber">danielbachhuber</a>
     460          </li>
     461      </ul>
     462    </div>
     463  </div>
     464
     465<div class="file">
     466  <div class="file-header">
     467  <div class="file-actions">
     468
     469    <div class="btn-group">
     470      <a href="/wp-cli/wp-cli.github.com/raw/master/behat-data/codeispoetry.png" class="btn btn-sm " id="raw-url">Raw</a>
     471      <a href="/wp-cli/wp-cli.github.com/commits/master/behat-data/codeispoetry.png" class="btn btn-sm " rel="nofollow">History</a>
     472    </div>
     473
     474
     475        <!-- </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>
     476          <button class="btn-octicon btn-octicon-danger tooltipped tooltipped-nw" type="submit"
     477            aria-label="You must be signed in to make or propose changes" data-disable-with>
     478            <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>
     479          </button>
     480</form>  </div>
     481
     482  <div class="file-info">
     483    15.6 KB
     484  </div>
     485</div>
     486
     487 
     488
     489  <div class="blob-wrapper data type-text">
     490      <div class="image">
     491          <span class="border-wrap"><img src="/wp-cli/wp-cli.github.com/blob/master/behat-data/codeispoetry.png?raw=true" alt="codeispoetry.png"></span>
     492      </div>
     493  </div>
     494
     495</div>
     496
     497<a href="#jump-to-line" rel="facebox[.linejump]" data-hotkey="l" style="display:none">Jump to Line</a>
     498<div id="jump-to-line" style="display:none">
     499  <!-- </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>
     500    <input class="linejump-input js-jump-to-line-field" type="text" placeholder="Jump to line&hellip;" aria-label="Jump to line" autofocus>
     501    <button type="submit" class="btn">Go</button>
     502</form></div>
     503
     504  </div>
     505  <div class="modal-backdrop"></div>
     506</div>
     507
     508
     509    </div>
     510  </div>
     511
     512    </div>
     513
     514        <div class="container">
     515  <div class="site-footer" role="contentinfo">
     516    <ul class="site-footer-links right">
     517        <li><a href="https://status.github.com/" data-ga-click="Footer, go to status, text:status">Status</a></li>
     518      <li><a href="https://developer.github.com" data-ga-click="Footer, go to api, text:api">API</a></li>
     519      <li><a href="https://training.github.com" data-ga-click="Footer, go to training, text:training">Training</a></li>
     520      <li><a href="https://shop.github.com" data-ga-click="Footer, go to shop, text:shop">Shop</a></li>
     521        <li><a href="https://github.com/blog" data-ga-click="Footer, go to blog, text:blog">Blog</a></li>
     522        <li><a href="https://github.com/about" data-ga-click="Footer, go to about, text:about">About</a></li>
     523        <li><a href="https://github.com/pricing" data-ga-click="Footer, go to pricing, text:pricing">Pricing</a></li>
     524
     525    </ul>
     526
     527    <a href="https://github.com" aria-label="Homepage">
     528      <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>
     529</a>
     530    <ul class="site-footer-links">
     531      <li>&copy; 2016 <span title="0.03841s from github-fe118-cp1-prd.iad.github.net">GitHub</span>, Inc.</li>
     532        <li><a href="https://github.com/site/terms" data-ga-click="Footer, go to terms, text:terms">Terms</a></li>
     533        <li><a href="https://github.com/site/privacy" data-ga-click="Footer, go to privacy, text:privacy">Privacy</a></li>
     534        <li><a href="https://github.com/security" data-ga-click="Footer, go to security, text:security">Security</a></li>
     535        <li><a href="https://github.com/contact" data-ga-click="Footer, go to contact, text:contact">Contact</a></li>
     536        <li><a href="https://help.github.com" data-ga-click="Footer, go to help, text:help">Help</a></li>
     537    </ul>
     538  </div>
     539</div>
     540
     541
     542
     543   
     544   
     545   
     546
     547    <div id="ajax-error-message" class="flash flash-error">
     548      <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>
     549      <button type="button" class="flash-close js-flash-close js-ajax-error-dismiss" aria-label="Dismiss error">
     550        <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>
     551      </button>
     552      Something went wrong with that request. Please try again.
     553    </div>
     554
     555
     556      <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/compat-ef12e71982f00805539829712436829b02370b2e561de18ece6796130cbb9bbe.js"></script>
     557      <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/frameworks-ee521b8e9facac68ff27e93fc3ae0f8ed811d7bf9e434e84f4b9ea227780b084.js"></script>
     558      <script async="async" crossorigin="anonymous" src="https://assets-cdn.github.com/assets/github-863d0e4c2905010278cfd87e9c7c738e812530db73c36c274a185354977a2e41.js"></script>
     559     
     560     
     561     
     562    <div class="js-stale-session-flash stale-session-flash flash flash-warn flash-banner hidden">
     563      <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>
     564      <span class="signed-in-tab-flash">You signed in with another tab or window. <a href="">Reload</a> to refresh your session.</span>
     565      <span class="signed-out-tab-flash">You signed out in another tab or window. <a href="">Reload</a> to refresh your session.</span>
     566    </div>
     567    <div class="facebox" id="facebox" style="display:none;">
     568  <div class="facebox-popup">
     569    <div class="facebox-content" role="dialog" aria-labelledby="facebox-header" aria-describedby="facebox-description">
     570    </div>
     571    <button type="button" class="facebox-close js-facebox-close" aria-label="Close modal">
     572      <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>
     573    </button>
     574  </div>
     575</div>
     576
     577  </body>
     578</html>
     579
  • tests/phpunit/tests/rest-api/rest-attachments-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Attachments_Controller functionality
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Controller_Testcase {
     13
     14        public function setUp() {
     15                parent::setUp();
     16
     17                $this->editor_id = $this->factory->user->create( array(
     18                        'role' => 'editor',
     19                ) );
     20                $this->author_id = $this->factory->user->create( array(
     21                        'role' => 'author',
     22                ) );
     23                $this->contributor_id = $this->factory->user->create( array(
     24                        'role' => 'contributor',
     25                ) );
     26
     27                // Add an uploader role to test upload capabilities.
     28                add_role( 'uploader', 'File upload role' );
     29                $role = get_role( 'uploader' );
     30                $role->add_cap( 'upload_files' );
     31                $role->add_cap( 'read' );
     32                $role->add_cap( 'level_0' );
     33                $this->uploader_id = $this->factory->user->create( array(
     34                        'role' => 'uploader',
     35                ) );
     36
     37                $orig_file = dirname( __FILE__ ) . '/data/canola.jpg';
     38                $this->test_file = '/tmp/canola.jpg';
     39                copy( $orig_file, $this->test_file );
     40                $orig_file2 = dirname( __FILE__ ) . '/data/codeispoetry.png';
     41                $this->test_file2 = '/tmp/codeispoetry.png';
     42                copy( $orig_file2, $this->test_file2 );
     43
     44        }
     45
     46        public function test_register_routes() {
     47                $routes = $this->server->get_routes();
     48                $this->assertArrayHasKey( '/wp/v2/media', $routes );
     49                $this->assertCount( 2, $routes['/wp/v2/media'] );
     50                $this->assertArrayHasKey( '/wp/v2/media/(?P<id>[\d]+)', $routes );
     51                $this->assertCount( 3, $routes['/wp/v2/media/(?P<id>[\d]+)'] );
     52        }
     53
     54        public static function disposition_provider() {
     55                return array(
     56                        // Types
     57                        array( 'attachment; filename="foo.jpg"', 'foo.jpg' ),
     58                        array( 'inline; filename="foo.jpg"', 'foo.jpg' ),
     59                        array( 'form-data; filename="foo.jpg"', 'foo.jpg' ),
     60
     61                        // Formatting
     62                        array( 'attachment; filename="foo.jpg"', 'foo.jpg' ),
     63                        array( 'attachment; filename=foo.jpg', 'foo.jpg' ),
     64                        array( 'attachment;filename="foo.jpg"', 'foo.jpg' ),
     65                        array( 'attachment;filename=foo.jpg', 'foo.jpg' ),
     66                        array( 'attachment; filename = "foo.jpg"', 'foo.jpg' ),
     67                        array( 'attachment; filename = foo.jpg', 'foo.jpg' ),
     68                        array( "attachment;\tfilename\t=\t\"foo.jpg\"", 'foo.jpg' ),
     69                        array( "attachment;\tfilename\t=\tfoo.jpg", 'foo.jpg' ),
     70                        array( 'attachment; filename = my foo picture.jpg', 'my foo picture.jpg' ),
     71
     72                        // Extensions
     73                        array( 'form-data; name="myfile"; filename="foo.jpg"', 'foo.jpg' ),
     74                        array( 'form-data; name="myfile"; filename="foo.jpg"; something="else"', 'foo.jpg' ),
     75                        array( 'form-data; name=myfile; filename=foo.jpg; something=else', 'foo.jpg' ),
     76                        array( 'form-data; name=myfile; filename=my foo.jpg; something=else', 'my foo.jpg' ),
     77
     78                        // Invalid
     79                        array( 'filename="foo.jpg"', null ),
     80                        array( 'filename-foo.jpg', null ),
     81                        array( 'foo.jpg', null ),
     82                        array( 'unknown; notfilename="foo.jpg"', null ),
     83                );
     84        }
     85
     86        /**
     87         * @dataProvider disposition_provider
     88         */
     89        public function test_parse_disposition( $header, $expected ) {
     90                $header_list = array( $header );
     91                $parsed = WP_REST_Attachments_Controller::get_filename_from_disposition( $header_list );
     92                $this->assertEquals( $expected, $parsed );
     93        }
     94
     95        public function test_context_param() {
     96                // Collection
     97                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
     98                $response = $this->server->dispatch( $request );
     99                $data = $response->get_data();
     100                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     101                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     102                // Single
     103                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     104                        'post_mime_type' => 'image/jpeg',
     105                        'post_excerpt'   => 'A sample caption',
     106                ) );
     107                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media/' . $attachment_id );
     108                $response = $this->server->dispatch( $request );
     109                $data = $response->get_data();
     110                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     111                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     112        }
     113
     114        public function test_registered_query_params() {
     115                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
     116                $response = $this->server->dispatch( $request );
     117                $data = $response->get_data();
     118                $keys = array_keys( $data['endpoints'][0]['args'] );
     119                sort( $keys );
     120                $this->assertEquals( array(
     121                        'after',
     122                        'author',
     123                        'author_exclude',
     124                        'before',
     125                        'context',
     126                        'exclude',
     127                        'filter',
     128                        'include',
     129                        'media_type',
     130                        'mime_type',
     131                        'offset',
     132                        'order',
     133                        'orderby',
     134                        'page',
     135                        'parent',
     136                        'parent_exclude',
     137                        'per_page',
     138                        'search',
     139                        'slug',
     140                        'status',
     141                        ), $keys );
     142                $media_types = array(
     143                        'application',
     144                        'video',
     145                        'image',
     146                        'audio',
     147                );
     148                if ( ! is_multisite() ) {
     149                        $media_types[] = 'text';
     150                }
     151                $this->assertEqualSets( $media_types, $data['endpoints'][0]['args']['media_type']['enum'] );
     152        }
     153
     154        public function test_get_items() {
     155                wp_set_current_user( 0 );
     156                $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     157                        'post_mime_type' => 'image/jpeg',
     158                        'post_excerpt'   => 'A sample caption',
     159                ) );
     160                $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) );
     161                $id2 = $this->factory->attachment->create_object( $this->test_file, $draft_post, array(
     162                        'post_mime_type' => 'image/jpeg',
     163                        'post_excerpt'   => 'A sample caption',
     164                ) );
     165                $published_post = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     166                $id3 = $this->factory->attachment->create_object( $this->test_file, $published_post, array(
     167                        'post_mime_type' => 'image/jpeg',
     168                        'post_excerpt'   => 'A sample caption',
     169                ) );
     170                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     171                $response = $this->server->dispatch( $request );
     172                $data = $response->get_data();
     173                $this->assertCount( 2, $data );
     174                $ids = wp_list_pluck( $data, 'id' );
     175                $this->assertTrue( in_array( $id1, $ids, true ) );
     176                $this->assertFalse( in_array( $id2, $ids, true ) );
     177                $this->assertTrue( in_array( $id3, $ids, true ) );
     178
     179                $this->check_get_posts_response( $response );
     180        }
     181
     182        public function test_get_items_logged_in_editor() {
     183                wp_set_current_user( $this->editor_id );
     184                $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     185                        'post_mime_type' => 'image/jpeg',
     186                        'post_excerpt'   => 'A sample caption',
     187                ) );
     188                $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) );
     189                $id2 = $this->factory->attachment->create_object( $this->test_file, $draft_post, array(
     190                        'post_mime_type' => 'image/jpeg',
     191                        'post_excerpt'   => 'A sample caption',
     192                ) );
     193                $published_post = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     194                $id3 = $this->factory->attachment->create_object( $this->test_file, $published_post, array(
     195                        'post_mime_type' => 'image/jpeg',
     196                        'post_excerpt'   => 'A sample caption',
     197                ) );
     198                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     199                $response = $this->server->dispatch( $request );
     200
     201                $data = $response->get_data();
     202                $this->assertCount( 3, $data );
     203                $ids = wp_list_pluck( $data, 'id' );
     204                $this->assertTrue( in_array( $id1, $ids, true ) );
     205                $this->assertTrue( in_array( $id2, $ids, true ) );
     206                $this->assertTrue( in_array( $id3, $ids, true ) );
     207        }
     208
     209        public function test_get_items_media_type() {
     210                $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     211                        'post_mime_type' => 'image/jpeg',
     212                ) );
     213                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     214                $response = $this->server->dispatch( $request );
     215                $data = $response->get_data();
     216                $this->assertEquals( $id1, $data[0]['id'] );
     217                // media_type=video
     218                $request->set_param( 'media_type', 'video' );
     219                $response = $this->server->dispatch( $request );
     220                $this->assertCount( 0, $response->get_data() );
     221                // media_type=image
     222                $request->set_param( 'media_type', 'image' );
     223                $response = $this->server->dispatch( $request );
     224                $data = $response->get_data();
     225                $this->assertEquals( $id1, $data[0]['id'] );
     226        }
     227
     228        public function test_get_items_mime_type() {
     229                $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     230                        'post_mime_type' => 'image/jpeg',
     231                ) );
     232                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     233                $response = $this->server->dispatch( $request );
     234                $data = $response->get_data();
     235                $this->assertEquals( $id1, $data[0]['id'] );
     236                // mime_type=image/png
     237                $request->set_param( 'mime_type', 'image/png' );
     238                $response = $this->server->dispatch( $request );
     239                $this->assertCount( 0, $response->get_data() );
     240                // mime_type=image/jpeg
     241                $request->set_param( 'mime_type', 'image/jpeg' );
     242                $response = $this->server->dispatch( $request );
     243                $data = $response->get_data();
     244                $this->assertEquals( $id1, $data[0]['id'] );
     245        }
     246
     247        public function test_get_items_parent() {
     248                $post_id = $this->factory->post->create( array( 'post_title' => 'Test Post' ) );
     249                $attachment_id = $this->factory->attachment->create_object( $this->test_file, $post_id, array(
     250                        'post_mime_type' => 'image/jpeg',
     251                        'post_excerpt'   => 'A sample caption',
     252                ) );
     253                $attachment_id2 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     254                        'post_mime_type' => 'image/jpeg',
     255                        'post_excerpt'   => 'A sample caption',
     256                ) );
     257                // all attachments
     258                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     259                $response = $this->server->dispatch( $request );
     260                $this->assertEquals( 2, count( $response->get_data() ) );
     261                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     262                // attachments without a parent
     263                $request->set_param( 'parent', 0 );
     264                $response = $this->server->dispatch( $request );
     265                $data = $response->get_data();
     266                $this->assertEquals( 1, count( $data ) );
     267                $this->assertEquals( $attachment_id2, $data[0]['id'] );
     268                // attachments with parent=post_id
     269                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     270                $request->set_param( 'parent', $post_id );
     271                $response = $this->server->dispatch( $request );
     272                $data = $response->get_data();
     273                $this->assertEquals( 1, count( $data ) );
     274                $this->assertEquals( $attachment_id, $data[0]['id'] );
     275                // attachments with invalid parent
     276                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     277                $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     278                $response = $this->server->dispatch( $request );
     279                $data = $response->get_data();
     280                $this->assertEquals( 0, count( $data ) );
     281        }
     282
     283        public function test_get_items_invalid_status_param_is_discarded() {
     284                wp_set_current_user( $this->editor_id );
     285                $this->factory->attachment->create_object( $this->test_file, 0, array(
     286                        'post_mime_type' => 'image/jpeg',
     287                        'post_excerpt'   => 'A sample caption',
     288                ) );
     289                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     290                $request->set_param( 'status', 'publish' );
     291                $request->set_param( 'context', 'edit' );
     292                $response = $this->server->dispatch( $request );
     293                $data = $response->get_data();
     294                $this->assertCount( 1, $data );
     295                $this->assertEquals( 'inherit', $data[0]['status'] );
     296        }
     297
     298        public function test_get_items_private_status() {
     299                // Logged out users can't make the request
     300                wp_set_current_user( 0 );
     301                $attachment_id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     302                        'post_mime_type' => 'image/jpeg',
     303                        'post_excerpt'   => 'A sample caption',
     304                        'post_status'    => 'private',
     305                ) );
     306                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     307                $request->set_param( 'status', 'private' );
     308                $response = $this->server->dispatch( $request );
     309                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     310                // Properly authorized users can make the request
     311                wp_set_current_user( $this->editor_id );
     312                $response = $this->server->dispatch( $request );
     313                $this->assertEquals( 200, $response->get_status() );
     314                $data = $response->get_data();
     315                $this->assertEquals( $attachment_id1, $data[0]['id'] );
     316        }
     317
     318        public function test_get_items_invalid_date() {
     319                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     320                $request->set_param( 'after', rand_str() );
     321                $request->set_param( 'before', rand_str() );
     322                $response = $this->server->dispatch( $request );
     323                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     324        }
     325
     326        public function test_get_items_valid_date() {
     327                $id1 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     328                        'post_date'      => '2016-01-15T00:00:00Z',
     329                        'post_mime_type' => 'image/jpeg',
     330                        'post_excerpt'   => 'A sample caption',
     331                ) );
     332                $id2 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     333                        'post_date'      => '2016-01-16T00:00:00Z',
     334                        'post_mime_type' => 'image/jpeg',
     335                        'post_excerpt'   => 'A sample caption',
     336                ) );
     337                $id3 = $this->factory->attachment->create_object( $this->test_file, 0, array(
     338                        'post_date'      => '2016-01-17T00:00:00Z',
     339                        'post_mime_type' => 'image/jpeg',
     340                        'post_excerpt'   => 'A sample caption',
     341                ) );
     342                $request = new WP_REST_Request( 'GET', '/wp/v2/media' );
     343                $request->set_param( 'after', '2016-01-15T00:00:00Z' );
     344                $request->set_param( 'before', '2016-01-17T00:00:00Z' );
     345                $response = $this->server->dispatch( $request );
     346                $data = $response->get_data();
     347                $this->assertCount( 1, $data );
     348                $this->assertEquals( $id2, $data[0]['id'] );
     349        }
     350
     351        public function test_get_item() {
     352                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     353                        'post_mime_type' => 'image/jpeg',
     354                        'post_excerpt'   => 'A sample caption',
     355                ) );
     356                update_post_meta( $attachment_id, '_wp_attachment_image_alt', 'Sample alt text' );
     357                $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
     358                $response = $this->server->dispatch( $request );
     359                $this->check_get_post_response( $response );
     360                $data = $response->get_data();
     361                $this->assertEquals( 'image/jpeg', $data['mime_type'] );
     362        }
     363
     364        public function test_get_item_sizes() {
     365                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     366                        'post_mime_type' => 'image/jpeg',
     367                        'post_excerpt'   => 'A sample caption',
     368                ), $this->test_file );
     369
     370                add_image_size( 'rest-api-test', 119, 119, true );
     371                wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $this->test_file ) );
     372
     373                $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
     374                $response = $this->server->dispatch( $request );
     375                $data = $response->get_data();
     376                $image_src = wp_get_attachment_image_src( $attachment_id, 'rest-api-test' );
     377                $original_image_src = wp_get_attachment_image_src( $attachment_id, 'full' );
     378                remove_image_size( 'rest-api-test' );
     379
     380                $this->assertEquals( $image_src[0], $data['media_details']['sizes']['rest-api-test']['source_url'] );
     381                $this->assertEquals( 'image/jpeg', $data['media_details']['sizes']['rest-api-test']['mime_type'] );
     382                $this->assertEquals( $original_image_src[0], $data['media_details']['sizes']['full']['source_url'] );
     383                $this->assertEquals( 'image/jpeg', $data['media_details']['sizes']['full']['mime_type'] );
     384        }
     385
     386        public function test_get_item_sizes_with_no_url() {
     387                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     388                        'post_mime_type' => 'image/jpeg',
     389                        'post_excerpt'   => 'A sample caption',
     390                ), $this->test_file );
     391
     392                add_image_size( 'rest-api-test', 119, 119, true );
     393                wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $this->test_file ) );
     394
     395                add_filter( 'wp_get_attachment_image_src', '__return_false' );
     396
     397                $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
     398                $response = $this->server->dispatch( $request );
     399                $data = $response->get_data();
     400                remove_filter( 'wp_get_attachment_image_src', '__return_false' );
     401                remove_image_size( 'rest-api-test' );
     402
     403                $this->assertFalse( isset( $data['media_details']['sizes']['rest-api-test']['source_url'] ) );
     404        }
     405
     406        public function test_get_item_private_post() {
     407                wp_set_current_user( 0 );
     408                $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) );
     409                $id1 = $this->factory->attachment->create_object( $this->test_file, $draft_post, array(
     410                        'post_mime_type' => 'image/jpeg',
     411                        'post_excerpt'   => 'A sample caption',
     412                ) );
     413                $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $id1 );
     414                $response = $this->server->dispatch( $request );
     415                $this->assertEquals( 403, $response->get_status() );
     416        }
     417
     418        public function test_create_item() {
     419                wp_set_current_user( $this->author_id );
     420                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     421                $request->set_header( 'Content-Type', 'image/jpeg' );
     422                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     423                $request->set_body( file_get_contents( $this->test_file ) );
     424                $response = $this->server->dispatch( $request );
     425                $data = $response->get_data();
     426                $this->assertEquals( 201, $response->get_status() );
     427                $this->assertEquals( 'image', $data['media_type'] );
     428                $this->assertEquals( 'A field of amazing canola', $data['title']['rendered'] );
     429                $this->assertEquals( 'The description for the image', $data['caption'] );
     430        }
     431
     432        public function test_create_item_default_filename_title() {
     433                wp_set_current_user( $this->author_id );
     434                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     435                $request->set_file_params( array(
     436                        'file' => array(
     437                                'file'     => file_get_contents( $this->test_file2 ),
     438                                'name'     => 'codeispoetry.jpg',
     439                                'size'     => filesize( $this->test_file2 ),
     440                                'tmp_name' => $this->test_file2,
     441                        ),
     442                ) );
     443                $request->set_header( 'Content-MD5', md5_file( $this->test_file2 ) );
     444                $response = $this->server->dispatch( $request );
     445                $this->assertEquals( 201, $response->get_status() );
     446                $data = $response->get_data();
     447                $this->assertEquals( 'codeispoetry', $data['title']['raw'] );
     448        }
     449
     450        public function test_create_item_with_files() {
     451                wp_set_current_user( $this->author_id );
     452                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     453                $request->set_file_params( array(
     454                        'file' => array(
     455                                'file'     => file_get_contents( $this->test_file ),
     456                                'name'     => 'canola.jpg',
     457                                'size'     => filesize( $this->test_file ),
     458                                'tmp_name' => $this->test_file,
     459                        ),
     460                ) );
     461                $request->set_header( 'Content-MD5', md5_file( $this->test_file ) );
     462                $response = $this->server->dispatch( $request );
     463                $this->assertEquals( 201, $response->get_status() );
     464        }
     465
     466        public function test_create_item_with_upload_files_role() {
     467                wp_set_current_user( $this->uploader_id );
     468                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     469                $request->set_file_params( array(
     470                        'file' => array(
     471                                'file'     => file_get_contents( $this->test_file ),
     472                                'name'     => 'canola.jpg',
     473                                'size'     => filesize( $this->test_file ),
     474                                'tmp_name' => $this->test_file,
     475                        ),
     476                ) );
     477                $request->set_header( 'Content-MD5', md5_file( $this->test_file ) );
     478                $response = $this->server->dispatch( $request );
     479                $this->assertEquals( 201, $response->get_status() );
     480        }
     481
     482        public function test_create_item_empty_body() {
     483                wp_set_current_user( $this->author_id );
     484                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     485                $response = $this->server->dispatch( $request );
     486                $this->assertErrorResponse( 'rest_upload_no_data', $response, 400 );
     487        }
     488
     489        public function test_create_item_missing_content_type() {
     490                wp_set_current_user( $this->author_id );
     491                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     492                $request->set_body( file_get_contents( $this->test_file ) );
     493                $response = $this->server->dispatch( $request );
     494                $this->assertErrorResponse( 'rest_upload_no_content_type', $response, 400 );
     495        }
     496
     497        public function test_create_item_missing_content_disposition() {
     498                wp_set_current_user( $this->author_id );
     499                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     500                $request->set_header( 'Content-Type', 'image/jpeg' );
     501                $request->set_body( file_get_contents( $this->test_file ) );
     502                $response = $this->server->dispatch( $request );
     503                $this->assertErrorResponse( 'rest_upload_no_content_disposition', $response, 400 );
     504        }
     505
     506        public function test_create_item_bad_md5_header() {
     507                wp_set_current_user( $this->author_id );
     508                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     509                $request->set_header( 'Content-Type', 'image/jpeg' );
     510                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     511                $request->set_header( 'Content-MD5', 'abc123' );
     512                $request->set_body( file_get_contents( $this->test_file ) );
     513                $response = $this->server->dispatch( $request );
     514                $this->assertErrorResponse( 'rest_upload_hash_mismatch', $response, 412 );
     515        }
     516
     517        public function test_create_item_with_files_bad_md5_header() {
     518                wp_set_current_user( $this->author_id );
     519                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     520                $request->set_file_params( array(
     521                        'file' => array(
     522                                'file'     => file_get_contents( $this->test_file ),
     523                                'name'     => 'canola.jpg',
     524                                'size'     => filesize( $this->test_file ),
     525                                'tmp_name' => $this->test_file,
     526                        ),
     527                ) );
     528                $request->set_header( 'Content-MD5', 'abc123' );
     529                $response = $this->server->dispatch( $request );
     530                $this->assertErrorResponse( 'rest_upload_hash_mismatch', $response, 412 );
     531        }
     532
     533        public function test_create_item_invalid_upload_files_capability() {
     534                wp_set_current_user( $this->contributor_id );
     535                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     536                $response = $this->server->dispatch( $request );
     537                $this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
     538        }
     539
     540        public function test_create_item_invalid_edit_permissions() {
     541                $post_id = $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
     542                wp_set_current_user( $this->author_id );
     543                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     544                $request->set_param( 'post', $post_id );
     545                $response = $this->server->dispatch( $request );
     546                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     547        }
     548
     549        public function test_create_item_invalid_upload_permissions() {
     550                $post_id = $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
     551                wp_set_current_user( $this->uploader_id );
     552                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     553                $request->set_param( 'post', $post_id );
     554                $response = $this->server->dispatch( $request );
     555                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     556        }
     557
     558        public function test_create_item_invalid_post_type() {
     559                $attachment_id = $this->factory->post->create( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => 0 ) );
     560                wp_set_current_user( $this->editor_id );
     561                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     562                $request->set_header( 'Content-Type', 'image/jpeg' );
     563                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     564                $request->set_body( file_get_contents( $this->test_file ) );
     565                $request->set_param( 'post', $attachment_id );
     566                $response = $this->server->dispatch( $request );
     567                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     568        }
     569
     570        public function test_create_item_alt_text() {
     571                wp_set_current_user( $this->author_id );
     572                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     573                $request->set_header( 'Content-Type', 'image/jpeg' );
     574                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     575
     576                $request->set_body( file_get_contents( $this->test_file ) );
     577                $request->set_param( 'alt_text', 'test alt text' );
     578                $response = $this->server->dispatch( $request );
     579                $attachment = $response->get_data();
     580                $this->assertEquals( 'test alt text', $attachment['alt_text'] );
     581        }
     582
     583        public function test_create_item_unsafe_alt_text() {
     584                wp_set_current_user( $this->author_id );
     585                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     586                $request->set_header( 'Content-Type', 'image/jpeg' );
     587                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     588                $request->set_body( file_get_contents( $this->test_file ) );
     589                $request->set_param( 'alt_text', '<script>alert(document.cookie)</script>' );
     590                $response = $this->server->dispatch( $request );
     591                $attachment = $response->get_data();
     592                $this->assertEquals( '', $attachment['alt_text'] );
     593        }
     594
     595        public function test_update_item() {
     596                wp_set_current_user( $this->editor_id );
     597                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     598                        'post_mime_type' => 'image/jpeg',
     599                        'post_excerpt'   => 'A sample caption',
     600                        'post_author'    => $this->editor_id,
     601                ) );
     602                $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
     603                $request->set_param( 'title', 'My title is very cool' );
     604                $request->set_param( 'caption', 'This is a better caption.' );
     605                $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' );
     606                $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' );
     607                $response = $this->server->dispatch( $request );
     608                $data = $response->get_data();
     609                $attachment = get_post( $data['id'] );
     610                $this->assertEquals( 'My title is very cool', $data['title']['raw'] );
     611                $this->assertEquals( 'My title is very cool', $attachment->post_title );
     612                $this->assertEquals( 'This is a better caption.', $data['caption'] );
     613                $this->assertEquals( 'This is a better caption.', $attachment->post_excerpt );
     614                $this->assertEquals( 'Without a description, my attachment is descriptionless.', $data['description'] );
     615                $this->assertEquals( 'Without a description, my attachment is descriptionless.', $attachment->post_content );
     616                $this->assertEquals( 'Alt text is stored outside post schema.', $data['alt_text'] );
     617                $this->assertEquals( 'Alt text is stored outside post schema.', get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ) );
     618        }
     619
     620        public function test_update_item_parent() {
     621                wp_set_current_user( $this->editor_id );
     622                $original_parent = $this->factory->post->create( array() );
     623                $attachment_id = $this->factory->attachment->create_object( $this->test_file, $original_parent, array(
     624                        'post_mime_type' => 'image/jpeg',
     625                        'post_excerpt'   => 'A sample caption',
     626                        'post_author'    => $this->editor_id,
     627                ) );
     628
     629                $attachment = get_post( $attachment_id );
     630                $this->assertEquals( $original_parent, $attachment->post_parent );
     631
     632                $new_parent = $this->factory->post->create( array() );
     633                $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
     634                $request->set_param( 'post', $new_parent );
     635                $this->server->dispatch( $request );
     636
     637                $attachment = get_post( $attachment_id );
     638                $this->assertEquals( $new_parent, $attachment->post_parent );
     639        }
     640
     641        public function test_update_item_invalid_permissions() {
     642                wp_set_current_user( $this->author_id );
     643                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     644                        'post_mime_type' => 'image/jpeg',
     645                        'post_excerpt'   => 'A sample caption',
     646                        'post_author'    => $this->editor_id,
     647                ) );
     648                $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
     649                $request->set_param( 'caption', 'This is a better caption.' );
     650                $response = $this->server->dispatch( $request );
     651                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     652        }
     653
     654        public function test_update_item_invalid_post_type() {
     655                $attachment_id = $this->factory->post->create( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => 0 ) );
     656                wp_set_current_user( $this->editor_id );
     657                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     658                        'post_mime_type' => 'image/jpeg',
     659                        'post_excerpt'   => 'A sample caption',
     660                        'post_author'    => $this->editor_id,
     661                ) );
     662                $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id );
     663                $request->set_param( 'post', $attachment_id );
     664                $response = $this->server->dispatch( $request );
     665                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     666        }
     667
     668        public function test_delete_item() {
     669                wp_set_current_user( $this->editor_id );
     670                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     671                        'post_mime_type' => 'image/jpeg',
     672                        'post_excerpt'   => 'A sample caption',
     673                ) );
     674                $request = new WP_REST_Request( 'DELETE', '/wp/v2/media/' . $attachment_id );
     675                $request['force'] = true;
     676                $response = $this->server->dispatch( $request );
     677                $this->assertEquals( 200, $response->get_status() );
     678        }
     679
     680        public function test_delete_item_no_trash() {
     681                wp_set_current_user( $this->editor_id );
     682                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     683                        'post_mime_type' => 'image/jpeg',
     684                        'post_excerpt'   => 'A sample caption',
     685                ) );
     686
     687                // Attempt trashing
     688                $request = new WP_REST_Request( 'DELETE', '/wp/v2/media/' . $attachment_id );
     689                $response = $this->server->dispatch( $request );
     690                $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 );
     691
     692                // Ensure the post still exists
     693                $post = get_post( $attachment_id );
     694                $this->assertNotEmpty( $post );
     695        }
     696
     697        public function test_delete_item_invalid_delete_permissions() {
     698                wp_set_current_user( $this->author_id );
     699                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     700                        'post_mime_type' => 'image/jpeg',
     701                        'post_excerpt'   => 'A sample caption',
     702                        'post_author'    => $this->editor_id,
     703                ) );
     704                $request = new WP_REST_Request( 'DELETE', '/wp/v2/media/' . $attachment_id );
     705                $response = $this->server->dispatch( $request );
     706                $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
     707        }
     708
     709        public function test_prepare_item() {
     710                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     711                        'post_mime_type' => 'image/jpeg',
     712                        'post_excerpt'   => 'A sample caption',
     713                        'post_author'    => $this->editor_id,
     714                ) );
     715
     716                $attachment = get_post( $attachment_id );
     717                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/media/%d', $attachment_id ) );
     718                $response = $this->server->dispatch( $request );
     719                $data = $response->get_data();
     720                $this->check_post_data( $attachment, $data, 'view', $response->get_links() );
     721                $this->check_post_data( $attachment, $data, 'embed', $response->get_links() );
     722        }
     723
     724        public function test_get_item_schema() {
     725                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
     726                $response = $this->server->dispatch( $request );
     727                $data = $response->get_data();
     728                $properties = $data['schema']['properties'];
     729                $this->assertEquals( 23, count( $properties ) );
     730                $this->assertArrayHasKey( 'author', $properties );
     731                $this->assertArrayHasKey( 'alt_text', $properties );
     732                $this->assertArrayHasKey( 'caption', $properties );
     733                $this->assertArrayHasKey( 'description', $properties );
     734                $this->assertArrayHasKey( 'comment_status', $properties );
     735                $this->assertArrayHasKey( 'date', $properties );
     736                $this->assertArrayHasKey( 'date_gmt', $properties );
     737                $this->assertArrayHasKey( 'guid', $properties );
     738                $this->assertArrayHasKey( 'id', $properties );
     739                $this->assertArrayHasKey( 'link', $properties );
     740                $this->assertArrayHasKey( 'media_type', $properties );
     741                $this->assertArrayHasKey( 'meta', $properties );
     742                $this->assertArrayHasKey( 'mime_type', $properties );
     743                $this->assertArrayHasKey( 'media_details', $properties );
     744                $this->assertArrayHasKey( 'modified', $properties );
     745                $this->assertArrayHasKey( 'modified_gmt', $properties );
     746                $this->assertArrayHasKey( 'post', $properties );
     747                $this->assertArrayHasKey( 'ping_status', $properties );
     748                $this->assertArrayHasKey( 'status', $properties );
     749                $this->assertArrayHasKey( 'slug', $properties );
     750                $this->assertArrayHasKey( 'source_url', $properties );
     751                $this->assertArrayHasKey( 'title', $properties );
     752                $this->assertArrayHasKey( 'type', $properties );
     753        }
     754
     755        public function test_get_additional_field_registration() {
     756
     757                $schema = array(
     758                        'type'        => 'integer',
     759                        'description' => 'Some integer of mine',
     760                        'enum'        => array( 1, 2, 3, 4 ),
     761                        'context'     => array( 'view', 'edit' ),
     762                );
     763
     764                register_rest_field( 'attachment', 'my_custom_int', array(
     765                        'schema'          => $schema,
     766                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     767                ) );
     768
     769                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' );
     770
     771                $response = $this->server->dispatch( $request );
     772                $data = $response->get_data();
     773                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     774                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     775
     776                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     777                        'post_mime_type' => 'image/jpeg',
     778                        'post_excerpt'   => 'A sample caption',
     779                ) );
     780
     781                $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id );
     782
     783                $response = $this->server->dispatch( $request );
     784                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     785
     786                global $wp_rest_additional_fields;
     787                $wp_rest_additional_fields = array();
     788        }
     789
     790        public function test_additional_field_update_errors() {
     791                $schema = array(
     792                        'type'        => 'integer',
     793                        'description' => 'Some integer of mine',
     794                        'enum'        => array( 1, 2, 3, 4 ),
     795                        'context'     => array( 'view', 'edit' ),
     796                );
     797
     798                register_rest_field( 'attachment', 'my_custom_int', array(
     799                        'schema'          => $schema,
     800                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     801                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     802                ) );
     803
     804                wp_set_current_user( $this->editor_id );
     805                $attachment_id = $this->factory->attachment->create_object( $this->test_file, 0, array(
     806                        'post_mime_type' => 'image/jpeg',
     807                        'post_excerpt'   => 'A sample caption',
     808                        'post_author'    => $this->editor_id,
     809                ) );
     810                // Check for error on update.
     811                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/media/%d', $attachment_id ) );
     812                $request->set_body_params(array(
     813                        'my_custom_int' => 'returnError',
     814                ));
     815
     816                $response = $this->server->dispatch( $request );
     817
     818                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     819
     820                global $wp_rest_additional_fields;
     821                $wp_rest_additional_fields = array();
     822        }
     823
     824        public function additional_field_get_callback( $object, $request ) {
     825                return 123;
     826        }
     827
     828        public function additional_field_update_callback( $value, $attachment ) {
     829                if ( 'returnError' === $value ) {
     830                        return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
     831                }
     832        }
     833
     834        public function tearDown() {
     835                parent::tearDown();
     836                if ( file_exists( $this->test_file ) ) {
     837                        unlink( $this->test_file );
     838                }
     839                if ( file_exists( $this->test_file2 ) ) {
     840                        unlink( $this->test_file2 );
     841                }
     842        }
     843
     844        protected function check_post_data( $attachment, $data, $context = 'view', $links ) {
     845                parent::check_post_data( $attachment, $data, $context, $links );
     846
     847                $this->assertEquals( get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ), $data['alt_text'] );
     848                $this->assertEquals( $attachment->post_excerpt, $data['caption'] );
     849                $this->assertEquals( $attachment->post_content, $data['description'] );
     850                $this->assertTrue( isset( $data['media_details'] ) );
     851
     852                if ( $attachment->post_parent ) {
     853                        $this->assertEquals( $attachment->post_parent, $data['post'] );
     854                } else {
     855                        $this->assertNull( $data['post'] );
     856                }
     857
     858                $this->assertEquals( wp_get_attachment_url( $attachment->ID ), $data['source_url'] );
     859
     860        }
     861
     862}
  • tests/phpunit/tests/rest-api/rest-categories-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Terms_Controller functionality, used for
     4 * Categories.
     5 *
     6 * @package WordPress
     7 * @subpackage REST API
     8 */
     9
     10/**
     11 * @group restapi
     12 */
     13class WP_Test_REST_Categories_Controller extends WP_Test_REST_Controller_Testcase {
     14
     15        public function setUp() {
     16                parent::setUp();
     17                $this->administrator = $this->factory->user->create( array(
     18                        'role' => 'administrator',
     19                ) );
     20                $this->subscriber = $this->factory->user->create( array(
     21                        'role' => 'subscriber',
     22                ) );
     23        }
     24
     25        public function test_register_routes() {
     26                $routes = $this->server->get_routes();
     27                $this->assertArrayHasKey( '/wp/v2/categories', $routes );
     28                $this->assertArrayHasKey( '/wp/v2/categories/(?P<id>[\d]+)', $routes );
     29        }
     30
     31        public function test_context_param() {
     32                // Collection
     33                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
     34                $response = $this->server->dispatch( $request );
     35                $data = $response->get_data();
     36                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     37                $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     38                // Single
     39                $category1 = $this->factory->category->create( array( 'name' => 'Season 5' ) );
     40                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories/' . $category1 );
     41                $response = $this->server->dispatch( $request );
     42                $data = $response->get_data();
     43                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     44                $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     45        }
     46
     47        public function test_registered_query_params() {
     48                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
     49                $response = $this->server->dispatch( $request );
     50                $data = $response->get_data();
     51                $keys = array_keys( $data['endpoints'][0]['args'] );
     52                sort( $keys );
     53                $this->assertEquals( array(
     54                        'context',
     55                        'exclude',
     56                        'hide_empty',
     57                        'include',
     58                        'order',
     59                        'orderby',
     60                        'page',
     61                        'parent',
     62                        'per_page',
     63                        'post',
     64                        'search',
     65                        'slug',
     66                        ), $keys );
     67        }
     68
     69        public function test_get_items() {
     70                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     71                $response = $this->server->dispatch( $request );
     72                $this->check_get_taxonomy_terms_response( $response );
     73        }
     74
     75        public function test_get_items_invalid_permission_for_context() {
     76                wp_set_current_user( 0 );
     77                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     78                $request->set_param( 'context', 'edit' );
     79                $response = $this->server->dispatch( $request );
     80                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     81        }
     82
     83        public function test_get_items_hide_empty_arg() {
     84                $post_id = $this->factory->post->create();
     85                $category1 = $this->factory->category->create( array( 'name' => 'Season 5' ) );
     86                $category2 = $this->factory->category->create( array( 'name' => 'The Be Sharps' ) );
     87                wp_set_object_terms( $post_id, array( $category1, $category2 ), 'category' );
     88                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     89                $request->set_param( 'hide_empty', true );
     90                $response = $this->server->dispatch( $request );
     91                $data = $response->get_data();
     92                $this->assertEquals( 2, count( $data ) );
     93                $this->assertEquals( 'Season 5', $data[0]['name'] );
     94                $this->assertEquals( 'The Be Sharps', $data[1]['name'] );
     95        }
     96
     97        public function test_get_items_parent_zero_arg() {
     98                $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) );
     99                $parent2 = $this->factory->category->create( array( 'name' => 'Marge' ) );
     100                $this->factory->category->create(
     101                        array(
     102                                'name'   => 'Bart',
     103                                'parent' => $parent1,
     104                        )
     105                );
     106                $this->factory->category->create(
     107                        array(
     108                                'name'   => 'Lisa',
     109                                'parent' => $parent2,
     110                        )
     111                );
     112                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     113                $request->set_param( 'parent', 0 );
     114                $response = $this->server->dispatch( $request );
     115
     116                $this->assertEquals( 200, $response->get_status() );
     117                $data = $response->get_data();
     118
     119                $args = array(
     120                        'hide_empty' => false,
     121                        'parent'     => 0,
     122                );
     123                $categories = get_terms( 'category', $args );
     124                $this->assertEquals( count( $categories ), count( $data ) );
     125        }
     126
     127        public function test_get_items_parent_zero_arg_string() {
     128                $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) );
     129                $parent2 = $this->factory->category->create( array( 'name' => 'Marge' ) );
     130                $this->factory->category->create(
     131                        array(
     132                                'name'   => 'Bart',
     133                                'parent' => $parent1,
     134                        )
     135                );
     136                $this->factory->category->create(
     137                        array(
     138                                'name'   => 'Lisa',
     139                                'parent' => $parent2,
     140                        )
     141                );
     142                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     143                $request->set_param( 'parent', '0' );
     144                $response = $this->server->dispatch( $request );
     145
     146                $this->assertEquals( 200, $response->get_status() );
     147                $data = $response->get_data();
     148
     149                $args = array(
     150                        'hide_empty' => false,
     151                        'parent'     => 0,
     152                );
     153                $categories = get_terms( 'category', $args );
     154                $this->assertEquals( count( $categories ), count( $data ) );
     155        }
     156
     157        public function test_get_items_by_parent_non_found() {
     158                $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) );
     159
     160                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     161                $request->set_param( 'parent', $parent1 );
     162                $response = $this->server->dispatch( $request );
     163
     164                $this->assertEquals( 200, $response->get_status() );
     165                $data = $response->get_data();
     166
     167                $this->assertEquals( array(), $data );
     168        }
     169
     170        public function test_get_items_invalid_page() {
     171                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     172                $request->set_param( 'page', 0 );
     173                $response = $this->server->dispatch( $request );
     174                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     175                $data = $response->get_data();
     176                $first_error = array_shift( $data['data']['params'] );
     177                $this->assertContains( 'page must be greater than 1 (inclusive)', $first_error );
     178        }
     179
     180        public function test_get_items_include_query() {
     181                $id1 = $this->factory->category->create();
     182                $this->factory->category->create();
     183                $id3 = $this->factory->category->create();
     184                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     185                // Orderby=>asc
     186                $request->set_param( 'include', array( $id3, $id1 ) );
     187                $response = $this->server->dispatch( $request );
     188                $data = $response->get_data();
     189                $this->assertEquals( 2, count( $data ) );
     190                $this->assertEquals( $id1, $data[0]['id'] );
     191                // Orderby=>include
     192                $request->set_param( 'orderby', 'include' );
     193                $response = $this->server->dispatch( $request );
     194                $data = $response->get_data();
     195                $this->assertEquals( 2, count( $data ) );
     196                $this->assertEquals( $id3, $data[0]['id'] );
     197        }
     198
     199        public function test_get_items_exclude_query() {
     200                $id1 = $this->factory->category->create();
     201                $id2 = $this->factory->category->create();
     202                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     203                $response = $this->server->dispatch( $request );
     204                $data = $response->get_data();
     205                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     206                $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     207                $request->set_param( 'exclude', array( $id2 ) );
     208                $response = $this->server->dispatch( $request );
     209                $data = $response->get_data();
     210                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     211                $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     212        }
     213
     214        public function test_get_items_orderby_args() {
     215                $this->factory->category->create( array( 'name' => 'Apple' ) );
     216                $this->factory->category->create( array( 'name' => 'Banana' ) );
     217                /*
     218                 * Tests:
     219                 * - orderby
     220                 * - order
     221                 * - per_page
     222                 */
     223                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     224                $request->set_param( 'orderby', 'name' );
     225                $request->set_param( 'order', 'desc' );
     226                $request->set_param( 'per_page', 1 );
     227                $response = $this->server->dispatch( $request );
     228                $this->assertEquals( 200, $response->get_status() );
     229                $data = $response->get_data();
     230                $this->assertEquals( 1, count( $data ) );
     231                $this->assertEquals( 'Uncategorized', $data[0]['name'] );
     232                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     233                $request->set_param( 'orderby', 'name' );
     234                $request->set_param( 'order', 'asc' );
     235                $request->set_param( 'per_page', 2 );
     236                $response = $this->server->dispatch( $request );
     237                $this->assertEquals( 200, $response->get_status() );
     238                $data = $response->get_data();
     239                $this->assertEquals( 2, count( $data ) );
     240                $this->assertEquals( 'Apple', $data[0]['name'] );
     241        }
     242
     243        public function test_get_items_orderby_id() {
     244                $this->factory->category->create( array( 'name' => 'Cantaloupe' ) );
     245                $this->factory->category->create( array( 'name' => 'Apple' ) );
     246                $this->factory->category->create( array( 'name' => 'Banana' ) );
     247                // defaults to orderby=name, order=asc
     248                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     249                $response = $this->server->dispatch( $request );
     250                $this->assertEquals( 200, $response->get_status() );
     251                $data = $response->get_data();
     252                $this->assertEquals( 'Apple', $data[0]['name'] );
     253                $this->assertEquals( 'Banana', $data[1]['name'] );
     254                $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
     255                $this->assertEquals( 'Uncategorized', $data[3]['name'] );
     256                // orderby=id, with default order=asc
     257                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     258                $request->set_param( 'orderby', 'id' );
     259                $response = $this->server->dispatch( $request );
     260                $this->assertEquals( 200, $response->get_status() );
     261                $data = $response->get_data();
     262                $this->assertEquals( 'Uncategorized', $data[0]['name'] );
     263                $this->assertEquals( 'Cantaloupe', $data[1]['name'] );
     264                $this->assertEquals( 'Apple', $data[2]['name'] );
     265                $this->assertEquals( 'Banana', $data[3]['name'] );
     266                // orderby=id, order=desc
     267                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     268                $request->set_param( 'orderby', 'id' );
     269                $request->set_param( 'order', 'desc' );
     270                $response = $this->server->dispatch( $request );
     271                $data = $response->get_data();
     272                $this->assertEquals( 200, $response->get_status() );
     273                $this->assertEquals( 'Banana', $data[0]['name'] );
     274                $this->assertEquals( 'Apple', $data[1]['name'] );
     275                $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
     276        }
     277
     278        protected function post_with_categories() {
     279                $post_id = $this->factory->post->create();
     280                $category1 = $this->factory->category->create( array(
     281                        'name' => 'DC',
     282                        'description' => 'Purveyor of fine detective comics',
     283                ) );
     284                $category2 = $this->factory->category->create( array(
     285                        'name' => 'Marvel',
     286                        'description' => 'Home of the Marvel Universe',
     287                ) );
     288                $category3 = $this->factory->category->create( array(
     289                        'name' => 'Image',
     290                        'description' => 'American independent comic publisher',
     291                ) );
     292                wp_set_object_terms( $post_id, array( $category1, $category2, $category3 ), 'category' );
     293
     294                return $post_id;
     295        }
     296
     297        public function test_get_items_post_args() {
     298                $post_id = $this->post_with_categories();
     299
     300                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     301                $request->set_param( 'post', $post_id );
     302                $response = $this->server->dispatch( $request );
     303                $this->assertEquals( 200, $response->get_status() );
     304
     305                $data = $response->get_data();
     306                $this->assertEquals( 3, count( $data ) );
     307
     308                // Check ordered by name by default
     309                $names = wp_list_pluck( $data, 'name' );
     310                $this->assertEquals( array( 'DC', 'Image', 'Marvel' ), $names );
     311        }
     312
     313        public function test_get_items_post_ordered_by_description() {
     314                $post_id = $this->post_with_categories();
     315
     316                // Regular request
     317                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     318                $request->set_param( 'post', $post_id );
     319                $request->set_param( 'orderby', 'description' );
     320                $response = $this->server->dispatch( $request );
     321                $this->assertEquals( 200, $response->get_status() );
     322
     323                $data = $response->get_data();
     324                $this->assertEquals( 3, count( $data ) );
     325                $names = wp_list_pluck( $data, 'name' );
     326                $this->assertEquals( array( 'Image', 'Marvel', 'DC' ), $names, 'Terms should be ordered by description' );
     327
     328                // Flip the order
     329                $request->set_param( 'order', 'desc' );
     330                $response = $this->server->dispatch( $request );
     331                $this->assertEquals( 200, $response->get_status() );
     332
     333                $data = $response->get_data();
     334                $this->assertEquals( 3, count( $data ) );
     335                $names = wp_list_pluck( $data, 'name' );
     336                $this->assertEquals( array( 'DC', 'Marvel', 'Image' ), $names, 'Terms should be reverse-ordered by description' );
     337        }
     338
     339        public function test_get_items_post_ordered_by_id() {
     340                $post_id = $this->post_with_categories();
     341
     342                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     343                $request->set_param( 'post', $post_id );
     344                $request->set_param( 'orderby', 'id' );
     345                $response = $this->server->dispatch( $request );
     346                $this->assertEquals( 200, $response->get_status() );
     347
     348                $data = $response->get_data();
     349                $this->assertEquals( 3, count( $data ) );
     350                $names = wp_list_pluck( $data, 'name' );
     351                $this->assertEquals( array( 'DC', 'Marvel', 'Image' ), $names );
     352        }
     353
     354        public function test_get_items_custom_tax_post_args() {
     355                register_taxonomy( 'batman', 'post', array( 'show_in_rest' => true ) );
     356                $controller = new WP_REST_Terms_Controller( 'batman' );
     357                $controller->register_routes();
     358                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'batman' ) );
     359                $term2 = $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'batman' ) );
     360                $this->factory->term->create( array( 'name' => 'Car', 'taxonomy' => 'batman' ) );
     361                $post_id = $this->factory->post->create();
     362                wp_set_object_terms( $post_id, array( $term1, $term2 ), 'batman' );
     363
     364                $request = new WP_REST_Request( 'GET', '/wp/v2/batman' );
     365                $request->set_param( 'post', $post_id );
     366                $response = $this->server->dispatch( $request );
     367                $this->assertEquals( 200, $response->get_status() );
     368
     369                $data = $response->get_data();
     370                $this->assertEquals( 2, count( $data ) );
     371                $this->assertEquals( 'Cape', $data[0]['name'] );
     372        }
     373
     374        public function test_get_items_search_args() {
     375                $this->factory->category->create( array( 'name' => 'Apple' ) );
     376                $this->factory->category->create( array( 'name' => 'Banana' ) );
     377                /*
     378                 * Tests:
     379                 * - search
     380                 */
     381                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     382                $request->set_param( 'search', 'App' );
     383                $response = $this->server->dispatch( $request );
     384                $this->assertEquals( 200, $response->get_status() );
     385                $data = $response->get_data();
     386                $this->assertEquals( 1, count( $data ) );
     387                $this->assertEquals( 'Apple', $data[0]['name'] );
     388                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     389                $request->set_param( 'search', 'Garbage' );
     390                $response = $this->server->dispatch( $request );
     391                $this->assertEquals( 200, $response->get_status() );
     392                $data = $response->get_data();
     393                $this->assertEquals( 0, count( $data ) );
     394        }
     395
     396        public function test_get_items_slug_arg() {
     397                $this->factory->category->create( array( 'name' => 'Apple' ) );
     398                $this->factory->category->create( array( 'name' => 'Banana' ) );
     399                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     400                $request->set_param( 'slug', 'apple' );
     401                $response = $this->server->dispatch( $request );
     402                $this->assertEquals( 200, $response->get_status() );
     403                $data = $response->get_data();
     404                $this->assertEquals( 1, count( $data ) );
     405                $this->assertEquals( 'Apple', $data[0]['name'] );
     406        }
     407
     408        public function test_get_terms_parent_arg() {
     409                $category1 = $this->factory->category->create( array( 'name' => 'Parent' ) );
     410                $this->factory->category->create( array( 'name' => 'Child', 'parent' => $category1 ) );
     411                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     412                $request->set_param( 'parent', $category1 );
     413                $response = $this->server->dispatch( $request );
     414                $data = $response->get_data();
     415                $this->assertEquals( 1, count( $data ) );
     416                $this->assertEquals( 'Child', $data[0]['name'] );
     417        }
     418
     419        public function test_get_terms_private_taxonomy() {
     420                register_taxonomy( 'robin', 'post', array( 'public' => false ) );
     421                $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
     422                $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'robin' ) );
     423
     424                $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin' );
     425                $response = $this->server->dispatch( $request );
     426                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     427        }
     428
     429        public function test_get_terms_invalid_taxonomy() {
     430                $request = new WP_REST_Request( 'GET', '/wp/v2/invalid-taxonomy' );
     431                $response = $this->server->dispatch( $request );
     432                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     433        }
     434
     435        public function test_get_terms_pagination_headers() {
     436                // Start of the index + Uncategorized default term
     437                for ( $i = 0; $i < 49; $i++ ) {
     438                        $this->factory->category->create( array(
     439                                'name'   => "Category {$i}",
     440                                ) );
     441                }
     442                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     443                $response = $this->server->dispatch( $request );
     444                $headers = $response->get_headers();
     445                $this->assertEquals( 50, $headers['X-WP-Total'] );
     446                $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
     447                $this->assertCount( 10, $response->get_data() );
     448                $next_link = add_query_arg( array(
     449                        'page'    => 2,
     450                        ), rest_url( 'wp/v2/categories' ) );
     451                $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
     452                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     453                // 3rd page
     454                $this->factory->category->create( array(
     455                                'name'   => 'Category 51',
     456                                ) );
     457                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     458                $request->set_param( 'page', 3 );
     459                $response = $this->server->dispatch( $request );
     460                $headers = $response->get_headers();
     461                $this->assertEquals( 51, $headers['X-WP-Total'] );
     462                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     463                $this->assertCount( 10, $response->get_data() );
     464                $prev_link = add_query_arg( array(
     465                        'page'    => 2,
     466                        ), rest_url( 'wp/v2/categories' ) );
     467                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     468                $next_link = add_query_arg( array(
     469                        'page'    => 4,
     470                        ), rest_url( 'wp/v2/categories' ) );
     471                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     472                // Last page
     473                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     474                $request->set_param( 'page', 6 );
     475                $response = $this->server->dispatch( $request );
     476                $headers = $response->get_headers();
     477                $this->assertEquals( 51, $headers['X-WP-Total'] );
     478                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     479                $this->assertCount( 1, $response->get_data() );
     480                $prev_link = add_query_arg( array(
     481                        'page'    => 5,
     482                        ), rest_url( 'wp/v2/categories' ) );
     483                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     484                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     485                // Out of bounds
     486                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     487                $request->set_param( 'page', 8 );
     488                $response = $this->server->dispatch( $request );
     489                $headers = $response->get_headers();
     490                $this->assertEquals( 51, $headers['X-WP-Total'] );
     491                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     492                $this->assertCount( 0, $response->get_data() );
     493                $prev_link = add_query_arg( array(
     494                        'page'    => 6,
     495                        ), rest_url( 'wp/v2/categories' ) );
     496                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     497                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     498        }
     499
     500        public function test_get_items_per_page_exceeds_number_of_items() {
     501                // Start of the index + Uncategorized default term
     502                for ( $i = 0; $i < 17; $i++ ) {
     503                        $this->factory->category->create( array(
     504                                'name'   => "Category {$i}",
     505                                ) );
     506                }
     507                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     508                $request->set_param( 'page', 1 );
     509                $request->set_param( 'per_page', 100 );
     510                $response = $this->server->dispatch( $request );
     511                $headers = $response->get_headers();
     512                $this->assertEquals( 18, $headers['X-WP-Total'] );
     513                $this->assertEquals( 1, $headers['X-WP-TotalPages'] );
     514                $this->assertCount( 18, $response->get_data() );
     515                $request = new WP_REST_Request( 'GET', '/wp/v2/categories' );
     516                $request->set_param( 'page', 2 );
     517                $request->set_param( 'per_page', 100 );
     518                $response = $this->server->dispatch( $request );
     519                $headers = $response->get_headers();
     520                $this->assertEquals( 18, $headers['X-WP-Total'] );
     521                $this->assertEquals( 1, $headers['X-WP-TotalPages'] );
     522                $this->assertCount( 0, $response->get_data() );
     523        }
     524
     525        public function test_get_item() {
     526                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/1' );
     527                $response = $this->server->dispatch( $request );
     528                $this->check_get_taxonomy_term_response( $response );
     529        }
     530
     531        public function test_get_term_invalid_taxonomy() {
     532                $request = new WP_REST_Request( 'GET', '/wp/v2/invalid-taxonomy/1' );
     533                $response = $this->server->dispatch( $request );
     534                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     535        }
     536
     537        public function test_get_term_invalid_term() {
     538                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     539                $response = $this->server->dispatch( $request );
     540                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     541        }
     542
     543        public function test_get_item_invalid_permission_for_context() {
     544                wp_set_current_user( 0 );
     545                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/1' );
     546                $request->set_param( 'context', 'edit' );
     547                $response = $this->server->dispatch( $request );
     548                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     549        }
     550
     551        public function test_get_term_private_taxonomy() {
     552                register_taxonomy( 'robin', 'post', array( 'public' => false ) );
     553                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
     554
     555                $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin/' . $term1 );
     556                $response = $this->server->dispatch( $request );
     557                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     558        }
     559
     560        public function test_get_item_incorrect_taxonomy() {
     561                register_taxonomy( 'robin', 'post' );
     562                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
     563                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $term1 );
     564                $response = $this->server->dispatch( $request );
     565                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     566        }
     567
     568        public function test_create_item() {
     569                wp_set_current_user( $this->administrator );
     570                $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
     571                $request->set_param( 'name', 'My Awesome Term' );
     572                $request->set_param( 'description', 'This term is so awesome.' );
     573                $request->set_param( 'slug', 'so-awesome' );
     574                $response = $this->server->dispatch( $request );
     575                $this->assertEquals( 201, $response->get_status() );
     576                $headers = $response->get_headers();
     577                $data = $response->get_data();
     578                $this->assertContains( '/wp/v2/categories/' . $data['id'], $headers['Location'] );
     579                $this->assertEquals( 'My Awesome Term', $data['name'] );
     580                $this->assertEquals( 'This term is so awesome.', $data['description'] );
     581                $this->assertEquals( 'so-awesome', $data['slug'] );
     582        }
     583
     584        public function test_create_item_invalid_taxonomy() {
     585                wp_set_current_user( $this->administrator );
     586                $request = new WP_REST_Request( 'POST', '/wp/v2/invalid-taxonomy' );
     587                $request->set_param( 'name', 'Invalid Taxonomy' );
     588                $response = $this->server->dispatch( $request );
     589                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     590        }
     591
     592        public function test_create_item_incorrect_permissions() {
     593                wp_set_current_user( $this->subscriber );
     594                $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
     595                $request->set_param( 'name', 'Incorrect permissions' );
     596                $response = $this->server->dispatch( $request );
     597                $this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
     598        }
     599
     600        public function test_create_item_missing_arguments() {
     601                wp_set_current_user( $this->administrator );
     602                $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
     603                $response = $this->server->dispatch( $request );
     604                $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 );
     605        }
     606
     607        public function test_create_item_with_parent() {
     608                wp_set_current_user( $this->administrator );
     609                $parent = wp_insert_term( 'test-category', 'category' );
     610                $request = new WP_REST_Request( 'POST', '/wp/v2/categories' );
     611                $request->set_param( 'name', 'My Awesome Term' );
     612                $request->set_param( 'parent', $parent['term_id'] );
     613                $response = $this->server->dispatch( $request );
     614                $this->assertEquals( 201, $response->get_status() );
     615                $data = $response->get_data();
     616                $this->assertEquals( $parent['term_id'], $data['parent'] );
     617        }
     618
     619        public function test_create_item_invalid_parent() {
     620                wp_set_current_user( $this->administrator );
     621                $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
     622
     623                $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
     624                $request->set_param( 'name', 'My Awesome Term' );
     625                $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     626                $response = $this->server->dispatch( $request );
     627                $this->assertErrorResponse( 'rest_term_invalid', $response, 400 );
     628        }
     629
     630        public function test_update_item() {
     631                wp_set_current_user( $this->administrator );
     632                $orig_args = array(
     633                        'name'        => 'Original Name',
     634                        'description' => 'Original Description',
     635                        'slug'        => 'original-slug',
     636                        );
     637                $term = get_term_by( 'id', $this->factory->category->create( $orig_args ), 'category' );
     638                $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
     639                $request->set_param( 'name', 'New Name' );
     640                $request->set_param( 'description', 'New Description' );
     641                $request->set_param( 'slug', 'new-slug' );
     642                $response = $this->server->dispatch( $request );
     643                $this->assertEquals( 200, $response->get_status() );
     644                $data = $response->get_data();
     645                $this->assertEquals( 'New Name', $data['name'] );
     646                $this->assertEquals( 'New Description', $data['description'] );
     647                $this->assertEquals( 'new-slug', $data['slug'] );
     648        }
     649
     650        public function test_update_item_invalid_taxonomy() {
     651                wp_set_current_user( $this->administrator );
     652                $request = new WP_REST_Request( 'POST', '/wp/v2/invalid-taxonomy/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     653                $request->set_param( 'name', 'Invalid Taxonomy' );
     654                $response = $this->server->dispatch( $request );
     655                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     656        }
     657
     658        public function test_update_item_invalid_term() {
     659                wp_set_current_user( $this->administrator );
     660                $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     661                $request->set_param( 'name', 'Invalid Term' );
     662                $response = $this->server->dispatch( $request );
     663                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     664        }
     665
     666        public function test_update_item_incorrect_permissions() {
     667                wp_set_current_user( $this->subscriber );
     668                $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
     669                $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
     670                $request->set_param( 'name', 'Incorrect permissions' );
     671                $response = $this->server->dispatch( $request );
     672                $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
     673        }
     674
     675        public function test_update_item_parent() {
     676                wp_set_current_user( $this->administrator );
     677                $parent = get_term_by( 'id', $this->factory->category->create(), 'category' );
     678                $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
     679
     680                $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_taxonomy_id );
     681                $request->set_param( 'parent', $parent->term_id );
     682                $response = $this->server->dispatch( $request );
     683                $this->assertEquals( 200, $response->get_status() );
     684
     685                $data = $response->get_data();
     686                $this->assertEquals( $parent->term_id, $data['parent'] );
     687        }
     688
     689        public function test_update_item_invalid_parent() {
     690                wp_set_current_user( $this->administrator );
     691                $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
     692
     693                $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id );
     694                $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     695                $response = $this->server->dispatch( $request );
     696                $this->assertErrorResponse( 'rest_term_invalid', $response, 400 );
     697        }
     698
     699        public function test_delete_item() {
     700                wp_set_current_user( $this->administrator );
     701                $term = get_term_by( 'id', $this->factory->category->create( array( 'name' => 'Deleted Category' ) ), 'category' );
     702                $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id );
     703                $request->set_param( 'force', true );
     704                $response = $this->server->dispatch( $request );
     705                $this->assertEquals( 200, $response->get_status() );
     706                $data = $response->get_data();
     707                $this->assertEquals( 'Deleted Category', $data['name'] );
     708        }
     709
     710        public function test_delete_item_force_false() {
     711                wp_set_current_user( $this->administrator );
     712                $term = get_term_by( 'id', $this->factory->category->create( array( 'name' => 'Deleted Category' ) ), 'category' );
     713                $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id );
     714                // force defaults to false
     715                $response = $this->server->dispatch( $request );
     716                $this->assertEquals( 501, $response->get_status() );
     717        }
     718
     719        public function test_delete_item_invalid_taxonomy() {
     720                wp_set_current_user( $this->administrator );
     721                $request = new WP_REST_Request( 'DELETE', '/wp/v2/invalid-taxonomy/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     722                $response = $this->server->dispatch( $request );
     723                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     724        }
     725
     726        public function test_delete_item_invalid_term() {
     727                wp_set_current_user( $this->administrator );
     728                $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     729                $response = $this->server->dispatch( $request );
     730                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     731        }
     732
     733        public function test_delete_item_incorrect_permissions() {
     734                wp_set_current_user( $this->subscriber );
     735                $term = get_term_by( 'id', $this->factory->category->create(), 'category' );
     736                $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id );
     737                $response = $this->server->dispatch( $request );
     738                $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
     739        }
     740
     741        public function test_prepare_item() {
     742                $term = get_term( 1, 'category' );
     743
     744                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/1' );
     745                $response = $this->server->dispatch( $request );
     746                $data = $response->get_data();
     747
     748                $this->check_taxonomy_term( $term, $data, $response->get_links() );
     749        }
     750
     751        public function test_prepare_taxonomy_term_child() {
     752                $child = $this->factory->category->create( array(
     753                        'parent' => 1,
     754                ) );
     755                $term = get_term( $child, 'category' );
     756
     757                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $child );
     758                $response = $this->server->dispatch( $request );
     759                $data = $response->get_data();
     760
     761                $this->check_taxonomy_term( $term, $data, $response->get_links() );
     762
     763                $this->assertEquals( 1, $data['parent'] );
     764
     765                $links = $response->get_links();
     766                $this->assertEquals( rest_url( 'wp/v2/categories/1' ), $links['up'][0]['href'] );
     767        }
     768
     769        public function test_get_item_schema() {
     770                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
     771                $response = $this->server->dispatch( $request );
     772                $data = $response->get_data();
     773                $properties = $data['schema']['properties'];
     774                $this->assertEquals( 9, count( $properties ) );
     775                $this->assertArrayHasKey( 'id', $properties );
     776                $this->assertArrayHasKey( 'count', $properties );
     777                $this->assertArrayHasKey( 'description', $properties );
     778                $this->assertArrayHasKey( 'link', $properties );
     779                $this->assertArrayHasKey( 'meta', $properties );
     780                $this->assertArrayHasKey( 'name', $properties );
     781                $this->assertArrayHasKey( 'parent', $properties );
     782                $this->assertArrayHasKey( 'slug', $properties );
     783                $this->assertArrayHasKey( 'taxonomy', $properties );
     784                $this->assertEquals( array_keys( get_taxonomies() ), $properties['taxonomy']['enum'] );
     785        }
     786
     787        public function test_get_additional_field_registration() {
     788
     789                $schema = array(
     790                        'type'        => 'integer',
     791                        'description' => 'Some integer of mine',
     792                        'enum'        => array( 1, 2, 3, 4 ),
     793                        'context'     => array( 'view', 'edit' ),
     794                );
     795
     796                register_rest_field( 'category', 'my_custom_int', array(
     797                        'schema'          => $schema,
     798                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     799                ) );
     800
     801                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' );
     802
     803                $response = $this->server->dispatch( $request );
     804                $data = $response->get_data();
     805                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     806                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     807
     808                $category_id = $this->factory->category->create();
     809                $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $category_id );
     810
     811                $response = $this->server->dispatch( $request );
     812                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     813
     814                global $wp_rest_additional_fields;
     815                $wp_rest_additional_fields = array();
     816        }
     817
     818        public function additional_field_get_callback( $object, $request ) {
     819                return 123;
     820        }
     821
     822        public function tearDown() {
     823                _unregister_taxonomy( 'batman' );
     824                _unregister_taxonomy( 'robin' );
     825                parent::tearDown();
     826        }
     827
     828        protected function check_get_taxonomy_terms_response( $response ) {
     829                $this->assertEquals( 200, $response->get_status() );
     830                $data = $response->get_data();
     831                $args = array(
     832                        'hide_empty' => false,
     833                );
     834                $categories = get_terms( 'category', $args );
     835                $this->assertEquals( count( $categories ), count( $data ) );
     836                $this->assertEquals( $categories[0]->term_id, $data[0]['id'] );
     837                $this->assertEquals( $categories[0]->name, $data[0]['name'] );
     838                $this->assertEquals( $categories[0]->slug, $data[0]['slug'] );
     839                $this->assertEquals( $categories[0]->taxonomy, $data[0]['taxonomy'] );
     840                $this->assertEquals( $categories[0]->description, $data[0]['description'] );
     841                $this->assertEquals( $categories[0]->count, $data[0]['count'] );
     842        }
     843
     844        protected function check_taxonomy_term( $term, $data, $links ) {
     845                $this->assertEquals( $term->term_id, $data['id'] );
     846                $this->assertEquals( $term->name, $data['name'] );
     847                $this->assertEquals( $term->slug, $data['slug'] );
     848                $this->assertEquals( $term->description, $data['description'] );
     849                $this->assertEquals( get_term_link( $term ),  $data['link'] );
     850                $this->assertEquals( $term->count, $data['count'] );
     851                $taxonomy = get_taxonomy( $term->taxonomy );
     852                if ( $taxonomy->hierarchical ) {
     853                        $this->assertEquals( $term->parent, $data['parent'] );
     854                } else {
     855                        $this->assertFalse( isset( $term->parent ) );
     856                }
     857
     858                $relations = array(
     859                        'self',
     860                        'collection',
     861                        'about',
     862                        'https://api.w.org/post_type',
     863                );
     864
     865                if ( ! empty( $data['parent'] ) ) {
     866                        $relations[] = 'up';
     867                }
     868
     869                $this->assertEqualSets( $relations, array_keys( $links ) );
     870                $this->assertContains( 'wp/v2/taxonomies/' . $term->taxonomy, $links['about'][0]['href'] );
     871                $this->assertEquals( add_query_arg( 'categories', $term->term_id, rest_url( 'wp/v2/posts' ) ), $links['https://api.w.org/post_type'][0]['href'] );
     872        }
     873
     874        protected function check_get_taxonomy_term_response( $response ) {
     875
     876                $this->assertEquals( 200, $response->get_status() );
     877
     878                $data = $response->get_data();
     879                $category = get_term( 1, 'category' );
     880                $this->check_taxonomy_term( $category, $data, $response->get_links() );
     881        }
     882}
  • tests/phpunit/tests/rest-api/rest-comments-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Comments_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9 /**
     10  * @group restapi
     11  */
     12class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        protected $admin_id;
     15        protected $subscriber_id;
     16
     17        protected $post_id;
     18        protected $private_id;
     19        protected $draft_id;
     20        protected $trash_id;
     21
     22        protected $approved_id;
     23        protected $hold_id;
     24
     25        protected $endpoint;
     26
     27        public function setUp() {
     28                parent::setUp();
     29
     30                $this->admin_id = $this->factory->user->create( array(
     31                        'role' => 'administrator',
     32                ));
     33                $this->subscriber_id = $this->factory->user->create( array(
     34                        'role' => 'subscriber',
     35                ));
     36                $this->author_id = $this->factory->user->create( array(
     37                        'role'         => 'author',
     38                        'display_name' => 'Sea Captain',
     39                        'first_name'   => 'Horatio',
     40                        'last_name'    => 'McCallister',
     41                        'user_email'   => 'captain@thefryingdutchman.com',
     42                        'user_url'     => 'http://thefryingdutchman.com',
     43                ));
     44
     45                $this->post_id = $this->factory->post->create();
     46                $this->private_id = $this->factory->post->create( array(
     47                        'post_status' => 'private',
     48                ));
     49                $this->draft_id = $this->factory->post->create( array(
     50                        'post_status' => 'draft',
     51                ));
     52                $this->trash_id = $this->factory->post->create( array(
     53                        'post_status' => 'trash',
     54                ));
     55
     56                $this->approved_id = $this->factory->comment->create( array(
     57                        'comment_approved' => 1,
     58                        'comment_post_ID'  => $this->post_id,
     59                        'user_id'          => 0,
     60                ));
     61                $this->hold_id = $this->factory->comment->create( array(
     62                        'comment_approved' => 0,
     63                        'comment_post_ID'  => $this->post_id,
     64                        'user_id'          => $this->subscriber_id,
     65                ));
     66
     67                $this->endpoint = new WP_REST_Comments_Controller;
     68        }
     69
     70        public function tearDown() {
     71                parent::tearDown();
     72        }
     73
     74        public function test_register_routes() {
     75                $routes = $this->server->get_routes();
     76
     77                $this->assertArrayHasKey( '/wp/v2/comments', $routes );
     78                $this->assertCount( 2, $routes['/wp/v2/comments'] );
     79                $this->assertArrayHasKey( '/wp/v2/comments/(?P<id>[\d]+)', $routes );
     80                $this->assertCount( 3, $routes['/wp/v2/comments/(?P<id>[\d]+)'] );
     81        }
     82
     83        public function test_context_param() {
     84                // Collection
     85                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
     86                $response = $this->server->dispatch( $request );
     87                $data = $response->get_data();
     88                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     89                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     90                // Single
     91                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments/' . $this->approved_id );
     92                $response = $this->server->dispatch( $request );
     93                $data = $response->get_data();
     94                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     95                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     96        }
     97
     98        public function test_registered_query_params() {
     99                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
     100                $response = $this->server->dispatch( $request );
     101                $data = $response->get_data();
     102                $keys = array_keys( $data['endpoints'][0]['args'] );
     103                sort( $keys );
     104                $this->assertEquals( array(
     105                        'after',
     106                        'author',
     107                        'author_email',
     108                        'author_exclude',
     109                        'before',
     110                        'context',
     111                        'exclude',
     112                        'include',
     113                        'karma',
     114                        'offset',
     115                        'order',
     116                        'orderby',
     117                        'page',
     118                        'parent',
     119                        'parent_exclude',
     120                        'per_page',
     121                        'post',
     122                        'search',
     123                        'status',
     124                        'type',
     125                        ), $keys );
     126        }
     127
     128        public function test_get_items() {
     129                $this->factory->comment->create_post_comments( $this->post_id, 6 );
     130
     131                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     132
     133                $response = $this->server->dispatch( $request );
     134                $this->assertEquals( 200, $response->get_status() );
     135
     136                $comments = $response->get_data();
     137                // We created 6 comments in this method, plus $this->approved_id.
     138                $this->assertCount( 7, $comments );
     139        }
     140
     141        public function test_get_items_without_private_post_permission() {
     142                wp_set_current_user( 0 );
     143
     144                $args = array(
     145                        'comment_approved' => 1,
     146                        'comment_post_ID'  => $this->private_id,
     147                );
     148                $private_comment = $this->factory->comment->create( $args );
     149
     150                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     151
     152                $response = $this->server->dispatch( $request );
     153                $this->assertEquals( 200, $response->get_status() );
     154
     155                $collection_data = $response->get_data();
     156                $this->assertFalse( in_array( $private_comment, wp_list_pluck( $collection_data, 'id' ), true ) );
     157        }
     158
     159        public function test_get_items_with_private_post_permission() {
     160                wp_set_current_user( $this->admin_id );
     161
     162                $args = array(
     163                        'comment_approved' => 1,
     164                        'comment_post_ID'  => $this->private_id,
     165                );
     166                $private_comment = $this->factory->comment->create( $args );
     167
     168                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     169
     170                $response = $this->server->dispatch( $request );
     171                $this->assertEquals( 200, $response->get_status() );
     172
     173                $collection_data = $response->get_data();
     174                $this->assertTrue( in_array( $private_comment, wp_list_pluck( $collection_data, 'id' ), true ) );
     175        }
     176
     177        public function test_get_items_with_invalid_post() {
     178                wp_set_current_user( 0 );
     179
     180                $comment_id = $this->factory->comment->create( array(
     181                        'comment_approved' => 1,
     182                        'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
     183                ));
     184
     185                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     186
     187                $response = $this->server->dispatch( $request );
     188                $this->assertEquals( 200, $response->get_status() );
     189
     190                $collection_data = $response->get_data();
     191                $this->assertFalse( in_array( $comment_id, wp_list_pluck( $collection_data, 'id' ), true ) );
     192
     193                wp_delete_comment( $comment_id );
     194        }
     195
     196        public function test_get_items_with_invalid_post_permission() {
     197                wp_set_current_user( $this->admin_id );
     198
     199                $comment_id = $this->factory->comment->create( array(
     200                        'comment_approved' => 1,
     201                        'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
     202                ));
     203
     204                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     205
     206                $response = $this->server->dispatch( $request );
     207                $this->assertEquals( 200, $response->get_status() );
     208
     209                $collection_data = $response->get_data();
     210                $this->assertTrue( in_array( $comment_id, wp_list_pluck( $collection_data, 'id' ), true ) );
     211
     212                wp_delete_comment( $comment_id );
     213        }
     214
     215        public function test_get_items_no_permission_for_context() {
     216                wp_set_current_user( 0 );
     217                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     218                $request->set_param( 'context', 'edit' );
     219                $response = $this->server->dispatch( $request );
     220                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     221        }
     222
     223        public function test_get_items_no_post() {
     224                $this->factory->comment->create_post_comments( 0, 2 );
     225                wp_set_current_user( $this->admin_id );
     226                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     227                $request->set_param( 'post', 0 );
     228                $response = $this->server->dispatch( $request );
     229                $this->assertEquals( 200, $response->get_status() );
     230                $comments = $response->get_data();
     231                $this->assertCount( 2, $comments );
     232        }
     233
     234        public function test_get_items_no_permission_for_no_post() {
     235                wp_set_current_user( 0 );
     236                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     237                $request->set_param( 'post', 0 );
     238                $response = $this->server->dispatch( $request );
     239                $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
     240        }
     241
     242        public function test_get_items_edit_context() {
     243                wp_set_current_user( $this->admin_id );
     244                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     245                $request->set_param( 'context', 'edit' );
     246                $response = $this->server->dispatch( $request );
     247                $this->assertEquals( 200, $response->get_status() );
     248        }
     249
     250        public function test_get_items_for_post() {
     251                $second_post_id = $this->factory->post->create();
     252                $this->factory->comment->create_post_comments( $second_post_id, 2 );
     253
     254                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     255                $request->set_query_params( array(
     256                        'post' => $second_post_id,
     257                ) );
     258
     259                $response = $this->server->dispatch( $request );
     260                $this->assertEquals( 200, $response->get_status() );
     261
     262                $comments = $response->get_data();
     263                $this->assertCount( 2, $comments );
     264        }
     265
     266        public function test_get_items_include_query() {
     267                wp_set_current_user( $this->admin_id );
     268                $args = array(
     269                        'comment_approved' => 1,
     270                        'comment_post_ID'  => $this->post_id,
     271                );
     272                $id1 = $this->factory->comment->create( $args );
     273                $this->factory->comment->create( $args );
     274                $id3 = $this->factory->comment->create( $args );
     275                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     276                // Order=>asc
     277                $request->set_param( 'order', 'asc' );
     278                $request->set_param( 'include', array( $id3, $id1 ) );
     279                $response = $this->server->dispatch( $request );
     280                $data = $response->get_data();
     281                $this->assertEquals( 2, count( $data ) );
     282                $this->assertEquals( $id1, $data[0]['id'] );
     283                // Orderby=>include
     284                $request->set_param( 'orderby', 'include' );
     285                $response = $this->server->dispatch( $request );
     286                $data = $response->get_data();
     287                $this->assertEquals( 2, count( $data ) );
     288                $this->assertEquals( $id3, $data[0]['id'] );
     289        }
     290
     291        public function test_get_items_exclude_query() {
     292                wp_set_current_user( $this->admin_id );
     293                $args = array(
     294                        'comment_approved' => 1,
     295                        'comment_post_ID'  => $this->post_id,
     296                );
     297                $id1 = $this->factory->comment->create( $args );
     298                $id2 = $this->factory->comment->create( $args );
     299                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     300                $response = $this->server->dispatch( $request );
     301                $data = $response->get_data();
     302                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     303                $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     304                $request->set_param( 'exclude', array( $id2 ) );
     305                $response = $this->server->dispatch( $request );
     306                $data = $response->get_data();
     307                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     308                $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     309        }
     310
     311        public function test_get_items_offset_query() {
     312                wp_set_current_user( $this->admin_id );
     313                $args = array(
     314                        'comment_approved' => 1,
     315                        'comment_post_ID'  => $this->post_id,
     316                );
     317                $this->factory->comment->create( $args );
     318                $this->factory->comment->create( $args );
     319                $this->factory->comment->create( $args );
     320                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     321                $request->set_param( 'offset', 1 );
     322                $response = $this->server->dispatch( $request );
     323                $this->assertCount( 3, $response->get_data() );
     324                // 'offset' works with 'per_page'
     325                $request->set_param( 'per_page', 2 );
     326                $response = $this->server->dispatch( $request );
     327                $this->assertCount( 2, $response->get_data() );
     328                // 'offset' takes priority over 'page'
     329                $request->set_param( 'page', 3 );
     330                $response = $this->server->dispatch( $request );
     331                $this->assertCount( 2, $response->get_data() );
     332        }
     333
     334        public function test_get_items_order_query() {
     335                wp_set_current_user( $this->admin_id );
     336                $args = array(
     337                        'comment_approved' => 1,
     338                        'comment_post_ID'  => $this->post_id,
     339                );
     340                $this->factory->comment->create( $args );
     341                $this->factory->comment->create( $args );
     342                $id3 = $this->factory->comment->create( $args );
     343                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     344                // order defaults to 'desc'
     345                $response = $this->server->dispatch( $request );
     346                $data = $response->get_data();
     347                $this->assertEquals( $id3, $data[0]['id'] );
     348                // order=>asc
     349                $request->set_param( 'order', 'asc' );
     350                $response = $this->server->dispatch( $request );
     351                $data = $response->get_data();
     352                $this->assertEquals( $this->approved_id, $data[0]['id'] );
     353        }
     354
     355        public function test_get_items_private_post_no_permissions() {
     356                wp_set_current_user( 0 );
     357                $post_id = $this->factory->post->create( array( 'post_status' => 'private' ) );
     358                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     359                $request->set_param( 'post', $post_id );
     360                $response = $this->server->dispatch( $request );
     361                $this->assertErrorResponse( 'rest_cannot_read_post', $response, 401 );
     362        }
     363
     364        public function test_get_items_author_arg() {
     365                // Authorized
     366                wp_set_current_user( $this->admin_id );
     367                $args = array(
     368                        'comment_approved' => 1,
     369                        'comment_post_ID'  => $this->post_id,
     370                        'user_id'          => $this->author_id,
     371                );
     372                $this->factory->comment->create( $args );
     373                $args['user_id'] = $this->subscriber_id;
     374                $this->factory->comment->create( $args );
     375                unset( $args['user_id'] );
     376                $this->factory->comment->create( $args );
     377
     378                // 'author' limits result to 1 of 3
     379                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     380                $request->set_param( 'author', $this->author_id );
     381                $response = $this->server->dispatch( $request );
     382                $this->assertEquals( 200, $response->get_status() );
     383                $comments = $response->get_data();
     384                $this->assertCount( 1, $comments );
     385                // Multiple authors are supported
     386                $request->set_param( 'author', array( $this->author_id, $this->subscriber_id ) );
     387                $response = $this->server->dispatch( $request );
     388                $this->assertEquals( 200, $response->get_status() );
     389                $comments = $response->get_data();
     390                $this->assertCount( 2, $comments );
     391                // Unavailable to unauthenticated; defaults to error
     392                wp_set_current_user( 0 );
     393                $response = $this->server->dispatch( $request );
     394                $this->assertErrorResponse( 'rest_forbidden_param', $response, 401 );
     395        }
     396
     397        public function test_get_items_author_exclude_arg() {
     398                // Authorized
     399                wp_set_current_user( $this->admin_id );
     400                $args = array(
     401                        'comment_approved' => 1,
     402                        'comment_post_ID'  => $this->post_id,
     403                        'user_id'          => $this->author_id,
     404                );
     405                $this->factory->comment->create( $args );
     406                $args['user_id'] = $this->subscriber_id;
     407                $this->factory->comment->create( $args );
     408                unset( $args['user_id'] );
     409                $this->factory->comment->create( $args );
     410
     411                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     412                $response = $this->server->dispatch( $request );
     413                $comments = $response->get_data();
     414                $this->assertCount( 4, $comments );
     415
     416                // 'author_exclude' limits result to 3 of 4
     417                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     418                $request->set_param( 'author_exclude', $this->author_id );
     419                $response = $this->server->dispatch( $request );
     420                $this->assertEquals( 200, $response->get_status() );
     421                $comments = $response->get_data();
     422                $this->assertCount( 3, $comments );
     423                // 'author_exclude' for both comment authors (2 of 4)
     424                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     425                $request->set_param( 'author_exclude', array( $this->author_id, $this->subscriber_id ) );
     426                $response = $this->server->dispatch( $request );
     427                $this->assertEquals( 200, $response->get_status() );
     428                $comments = $response->get_data();
     429                $this->assertCount( 2, $comments );
     430                // Unavailable to unauthenticated; defaults to error
     431                wp_set_current_user( 0 );
     432                $response = $this->server->dispatch( $request );
     433                $this->assertErrorResponse( 'rest_forbidden_param', $response, 401 );
     434        }
     435
     436        public function test_get_items_parent_arg() {
     437                $args = array(
     438                        'comment_approved'  => 1,
     439                        'comment_post_ID'   => $this->post_id,
     440                );
     441                $parent_id = $this->factory->comment->create( $args );
     442                $parent_id2 = $this->factory->comment->create( $args );
     443                $args['comment_parent'] = $parent_id;
     444                $this->factory->comment->create( $args );
     445                $args['comment_parent'] = $parent_id2;
     446                $this->factory->comment->create( $args );
     447                // All comments in the database
     448                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     449                $response = $this->server->dispatch( $request );
     450                $this->assertCount( 5, $response->get_data() );
     451                // Limit to the parent
     452                $request->set_param( 'parent', $parent_id );
     453                $response = $this->server->dispatch( $request );
     454                $this->assertCount( 1, $response->get_data() );
     455                // Limit to two parents
     456                $request->set_param( 'parent', array( $parent_id, $parent_id2 ) );
     457                $response = $this->server->dispatch( $request );
     458                $this->assertCount( 2, $response->get_data() );
     459        }
     460
     461        public function test_get_items_parent_exclude_arg() {
     462                $args = array(
     463                        'comment_approved'  => 1,
     464                        'comment_post_ID'   => $this->post_id,
     465                );
     466                $parent_id = $this->factory->comment->create( $args );
     467                $parent_id2 = $this->factory->comment->create( $args );
     468                $args['comment_parent'] = $parent_id;
     469                $this->factory->comment->create( $args );
     470                $args['comment_parent'] = $parent_id2;
     471                $this->factory->comment->create( $args );
     472                // All comments in the database
     473                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     474                $response = $this->server->dispatch( $request );
     475                $this->assertCount( 5, $response->get_data() );
     476                // Exclude this particular parent
     477                $request->set_param( 'parent_exclude', $parent_id );
     478                $response = $this->server->dispatch( $request );
     479                $this->assertCount( 4, $response->get_data() );
     480                // Exclude both comment parents
     481                $request->set_param( 'parent_exclude', array( $parent_id, $parent_id2 ) );
     482                $response = $this->server->dispatch( $request );
     483                $this->assertCount( 3, $response->get_data() );
     484        }
     485
     486        public function test_get_items_search_query() {
     487                wp_set_current_user( $this->admin_id );
     488                $args = array(
     489                        'comment_approved' => 1,
     490                        'comment_post_ID'  => $this->post_id,
     491                        'comment_content'  => 'foo',
     492                        'comment_author'   => 'Homer J Simpson',
     493                );
     494                $id1 = $this->factory->comment->create( $args );
     495                $args['comment_content'] = 'bar';
     496                $this->factory->comment->create( $args );
     497                $args['comment_content'] = 'burrito';
     498                $this->factory->comment->create( $args );
     499                // 3 comments, plus 1 created in construct
     500                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     501                $response = $this->server->dispatch( $request );
     502                $this->assertCount( 4, $response->get_data() );
     503                // One matching comments
     504                $request->set_param( 'search', 'foo' );
     505                $response = $this->server->dispatch( $request );
     506                $data = $response->get_data();
     507                $this->assertCount( 1, $data );
     508                $this->assertEquals( $id1, $data[0]['id'] );
     509        }
     510
     511        public function test_get_comments_pagination_headers() {
     512                wp_set_current_user( $this->admin_id );
     513                // Start of the index
     514                for ( $i = 0; $i < 49; $i++ ) {
     515                        $this->factory->comment->create( array(
     516                                'comment_content'   => "Comment {$i}",
     517                                'comment_post_ID'   => $this->post_id,
     518                                ) );
     519                }
     520                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     521                $response = $this->server->dispatch( $request );
     522                $headers = $response->get_headers();
     523                $this->assertEquals( 50, $headers['X-WP-Total'] );
     524                $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
     525                $next_link = add_query_arg( array(
     526                        'page'    => 2,
     527                        ), rest_url( '/wp/v2/comments' ) );
     528                $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
     529                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     530                // 3rd page
     531                $this->factory->comment->create( array(
     532                                'comment_content'   => 'Comment 51',
     533                                'comment_post_ID'   => $this->post_id,
     534                                ) );
     535                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     536                $request->set_param( 'page', 3 );
     537                $response = $this->server->dispatch( $request );
     538                $headers = $response->get_headers();
     539                $this->assertEquals( 51, $headers['X-WP-Total'] );
     540                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     541                $prev_link = add_query_arg( array(
     542                        'page'    => 2,
     543                        ), rest_url( '/wp/v2/comments' ) );
     544                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     545                $next_link = add_query_arg( array(
     546                        'page'    => 4,
     547                        ), rest_url( '/wp/v2/comments' ) );
     548                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     549                // Last page
     550                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     551                $request->set_param( 'page', 6 );
     552                $response = $this->server->dispatch( $request );
     553                $headers = $response->get_headers();
     554                $this->assertEquals( 51, $headers['X-WP-Total'] );
     555                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     556                $prev_link = add_query_arg( array(
     557                        'page'    => 5,
     558                        ), rest_url( '/wp/v2/comments' ) );
     559                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     560                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     561                // Out of bounds
     562                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     563                $request->set_param( 'page', 8 );
     564                $response = $this->server->dispatch( $request );
     565                $headers = $response->get_headers();
     566                $this->assertEquals( 51, $headers['X-WP-Total'] );
     567                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     568                $prev_link = add_query_arg( array(
     569                        'page'    => 6,
     570                        ), rest_url( '/wp/v2/comments' ) );
     571                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     572                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     573        }
     574
     575        public function test_get_comments_invalid_date() {
     576                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     577                $request->set_param( 'after', rand_str() );
     578                $request->set_param( 'before', rand_str() );
     579                $response = $this->server->dispatch( $request );
     580                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     581        }
     582
     583        public function test_get_comments_valid_date() {
     584                $comment1 = $this->factory->comment->create( array(
     585                        'comment_date'    => '2016-01-15T00:00:00Z',
     586                        'comment_post_ID' => $this->post_id,
     587                ) );
     588                $comment2 = $this->factory->comment->create( array(
     589                        'comment_date'    => '2016-01-16T00:00:00Z',
     590                        'comment_post_ID' => $this->post_id,
     591                ) );
     592                $comment3 = $this->factory->comment->create( array(
     593                        'comment_date'    => '2016-01-17T00:00:00Z',
     594                        'comment_post_ID' => $this->post_id,
     595                ) );
     596
     597                $request = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     598                $request->set_param( 'after', '2016-01-15T00:00:00Z' );
     599                $request->set_param( 'before', '2016-01-17T00:00:00Z' );
     600                $response = $this->server->dispatch( $request );
     601                $data = $response->get_data();
     602                $this->assertCount( 1, $data );
     603                $this->assertEquals( $comment2, $data[0]['id'] );
     604        }
     605
     606        public function test_get_item() {
     607                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     608
     609                $response = $this->server->dispatch( $request );
     610                $this->assertEquals( 200, $response->get_status() );
     611
     612                $data = $response->get_data();
     613                $this->check_comment_data( $data, 'view', $response->get_links() );
     614        }
     615
     616        public function test_prepare_item() {
     617                wp_set_current_user( $this->admin_id );
     618                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     619                $request->set_query_params( array(
     620                        'context' => 'edit',
     621                ) );
     622
     623                $response = $this->server->dispatch( $request );
     624                $this->assertEquals( 200, $response->get_status() );
     625
     626                $data = $response->get_data();
     627                $this->check_comment_data( $data, 'edit', $response->get_links() );
     628        }
     629
     630        public function test_get_comment_author_avatar_urls() {
     631                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     632
     633                $response = $this->server->dispatch( $request );
     634
     635                $data = $response->get_data();
     636                $this->assertArrayHasKey( 24,  $data['author_avatar_urls'] );
     637                $this->assertArrayHasKey( 48,  $data['author_avatar_urls'] );
     638                $this->assertArrayHasKey( 96,  $data['author_avatar_urls'] );
     639
     640                $comment = get_comment( $this->approved_id );
     641                /**
     642                 * Ignore the subdomain, since 'get_avatar_url randomly sets the Gravatar
     643                 * server when building the url string.
     644                 */
     645                $this->assertEquals( substr( get_avatar_url( $comment->comment_author_email ), 9 ), substr( $data['author_avatar_urls'][96], 9 ) );
     646        }
     647
     648        public function test_get_comment_invalid_id() {
     649                $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     650
     651                $response = $this->server->dispatch( $request );
     652                $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 );
     653        }
     654
     655        public function test_get_comment_invalid_context() {
     656                wp_set_current_user( 0 );
     657                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $this->approved_id ) );
     658                $request->set_param( 'context', 'edit' );
     659                $response = $this->server->dispatch( $request );
     660                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     661        }
     662
     663        public function test_get_comment_invalid_post_id() {
     664                wp_set_current_user( 0 );
     665                $comment_id = $this->factory->comment->create( array(
     666                        'comment_approved' => 1,
     667                        'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
     668                ));
     669                $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
     670
     671                $response = $this->server->dispatch( $request );
     672                $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
     673        }
     674
     675        public function test_get_comment_invalid_post_id_as_admin() {
     676                wp_set_current_user( $this->admin_id );
     677                $comment_id = $this->factory->comment->create( array(
     678                        'comment_approved' => 1,
     679                        'comment_post_ID'  => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
     680                ));
     681                $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id );
     682
     683                $response = $this->server->dispatch( $request );
     684                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     685        }
     686
     687        public function test_get_comment_not_approved() {
     688                wp_set_current_user( 0 );
     689
     690                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->hold_id ) );
     691
     692                $response = $this->server->dispatch( $request );
     693                $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
     694        }
     695
     696        public function test_get_comment_not_approved_same_user() {
     697                wp_set_current_user( $this->subscriber_id );
     698
     699                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $this->hold_id ) );
     700
     701                $response = $this->server->dispatch( $request );
     702                $this->assertEquals( 200, $response->get_status() );
     703        }
     704
     705        public function test_get_comment_with_children_link() {
     706                $comment_id_1 = $this->factory->comment->create( array(
     707                        'comment_approved' => 1,
     708                        'comment_post_ID'  => $this->post_id,
     709                        'user_id'          => $this->subscriber_id,
     710                ) );
     711
     712                $child_comment = $this->factory->comment->create( array(
     713                        'comment_approved' => 1,
     714                        'comment_parent'   => $comment_id_1,
     715                        'comment_post_ID'  => $this->post_id,
     716                        'user_id'          => $this->subscriber_id,
     717                ) );
     718
     719                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
     720                $response = $this->server->dispatch( $request );
     721                $this->assertEquals( 200, $response->get_status() );
     722                $this->assertArrayHasKey( 'children', $response->get_links() );
     723        }
     724
     725        public function test_get_comment_without_children_link() {
     726                $comment_id_1 = $this->factory->comment->create( array(
     727                        'comment_approved' => 1,
     728                        'comment_post_ID'  => $this->post_id,
     729                        'user_id'          => $this->subscriber_id,
     730                ) );
     731
     732                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
     733                $response = $this->server->dispatch( $request );
     734                $this->assertEquals( 200, $response->get_status() );
     735                $this->assertArrayNotHasKey( 'children', $response->get_links() );
     736        }
     737
     738        public function test_create_item() {
     739                wp_set_current_user( 0 );
     740
     741                $params = array(
     742                        'post'    => $this->post_id,
     743                        'author_name'  => 'Comic Book Guy',
     744                        'author_email' => 'cbg@androidsdungeon.com',
     745                        'author_url'   => 'http://androidsdungeon.com',
     746                        'content' => 'Worst Comment Ever!',
     747                        'date'    => '2014-11-07T10:14:25',
     748                );
     749
     750                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     751                $request->add_header( 'content-type', 'application/json' );
     752                $request->set_body( wp_json_encode( $params ) );
     753
     754                $response = $this->server->dispatch( $request );
     755                $this->assertEquals( 201, $response->get_status() );
     756
     757                $data = $response->get_data();
     758                $this->check_comment_data( $data, 'view', $response->get_links() );
     759                $this->assertEquals( 'hold', $data['status'] );
     760                $this->assertEquals( '2014-11-07T10:14:25', $data['date'] );
     761                $this->assertEquals( $this->post_id, $data['post'] );
     762        }
     763
     764        public function test_create_item_using_accepted_content_raw_value() {
     765                wp_set_current_user( 0 );
     766
     767                $params = array(
     768                        'post'         => $this->post_id,
     769                        'author_name'  => 'Reverend Lovejoy',
     770                        'author_email' => 'lovejoy@example.com',
     771                        'author_url'   => 'http://timothylovejoy.jr',
     772                        'content'      => array(
     773                                'raw' => 'Once something has been approved by the government, it\'s no longer immoral.',
     774                        ),
     775                );
     776
     777                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     778                $request->add_header( 'content-type', 'application/json' );
     779                $request->set_body( wp_json_encode( $params ) );
     780
     781                $response = $this->server->dispatch( $request );
     782                $this->assertEquals( 201, $response->get_status() );
     783
     784                $data = $response->get_data();
     785                $new_comment = get_comment( $data['id'] );
     786                $this->assertEquals( $params['content']['raw'], $new_comment->comment_content );
     787        }
     788
     789        public function test_create_comment_missing_required_author_name_and_email_per_option_value() {
     790                update_option( 'require_name_email', 1 );
     791
     792                $params = array(
     793                        'post'    => $this->post_id,
     794                        '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.',
     795                );
     796
     797                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     798                $request->add_header( 'content-type', 'application/json' );
     799                $request->set_body( wp_json_encode( $params ) );
     800
     801                $response = $this->server->dispatch( $request );
     802                $this->assertErrorResponse( 'rest_comment_author_data_required', $response, 400 );
     803
     804                update_option( 'require_name_email', 0 );
     805        }
     806
     807        public function test_create_comment_missing_required_author_name_per_option_value() {
     808                update_option( 'require_name_email', 1 );
     809
     810                $params = array(
     811                        'post'         => $this->post_id,
     812                        'author_email' => 'ekrabappel@springfield-elementary.edu',
     813                        '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.',
     814                );
     815
     816                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     817                $request->add_header( 'content-type', 'application/json' );
     818                $request->set_body( wp_json_encode( $params ) );
     819
     820                $response = $this->server->dispatch( $request );
     821                $this->assertErrorResponse( 'rest_comment_author_required', $response, 400 );
     822
     823                update_option( 'require_name_email', 0 );
     824        }
     825
     826        public function test_create_comment_missing_required_author_email_per_option_value() {
     827                update_option( 'require_name_email', 1 );
     828
     829                $params = array(
     830                        'post'        => $this->post_id,
     831                        'author_name' => 'Edna Krabappel',
     832                        '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.',
     833                );
     834
     835                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     836                $request->add_header( 'content-type', 'application/json' );
     837                $request->set_body( wp_json_encode( $params ) );
     838
     839                $response = $this->server->dispatch( $request );
     840                $this->assertErrorResponse( 'rest_comment_author_email_required', $response, 400 );
     841
     842                update_option( 'require_name_email', 0 );
     843        }
     844
     845        public function test_create_item_invalid_blank_content() {
     846                wp_set_current_user( 0 );
     847
     848                $params = array(
     849                        'post'         => $this->post_id,
     850                        'author_name'  => 'Reverend Lovejoy',
     851                        'author_email' => 'lovejoy@example.com',
     852                        'author_url'   => 'http://timothylovejoy.jr',
     853                        'content'      => '',
     854                );
     855
     856                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     857                $request->add_header( 'content-type', 'application/json' );
     858                $request->set_body( wp_json_encode( $params ) );
     859
     860                $response = $this->server->dispatch( $request );
     861                $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 );
     862        }
     863
     864        public function test_create_item_invalid_date() {
     865                wp_set_current_user( 0 );
     866
     867                $params = array(
     868                        'post'         => $this->post_id,
     869                        'author_name'  => 'Reverend Lovejoy',
     870                        'author_email' => 'lovejoy@example.com',
     871                        'author_url'   => 'http://timothylovejoy.jr',
     872                        'content'      => 'It\'s all over\, people! We don\'t have a prayer!',
     873                        'date'         => rand_str(),
     874                );
     875
     876                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     877                $request->add_header( 'content-type', 'application/json' );
     878                $request->set_body( wp_json_encode( $params ) );
     879
     880                $response = $this->server->dispatch( $request );
     881                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     882        }
     883
     884
     885        public function test_create_item_assign_different_user() {
     886                $subscriber_id = $this->factory->user->create( array(
     887                        'role' => 'subscriber',
     888                        'user_email' => 'cbg@androidsdungeon.com',
     889                ));
     890
     891                wp_set_current_user( $this->admin_id );
     892                $params = array(
     893                        'post'    => $this->post_id,
     894                        'author_name'  => 'Comic Book Guy',
     895                        'author_email' => 'cbg@androidsdungeon.com',
     896                        'author_url'   => 'http://androidsdungeon.com',
     897                        'author' => $subscriber_id,
     898                        'content' => 'Worst Comment Ever!',
     899                        'date'    => '2014-11-07T10:14:25',
     900                );
     901                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     902                $request->add_header( 'content-type', 'application/json' );
     903                $request->set_body( wp_json_encode( $params ) );
     904                $response = $this->server->dispatch( $request );
     905                $this->assertEquals( 201, $response->get_status() );
     906
     907                $data = $response->get_data();
     908                $this->assertEquals( $subscriber_id, $data['author'] );
     909                $this->assertEquals( '127.0.0.1', $data['author_ip'] );
     910        }
     911
     912        public function test_create_comment_without_type() {
     913                $post_id = $this->factory->post->create();
     914                wp_set_current_user( $this->admin_id );
     915
     916                $params = array(
     917                        'post'    => $post_id,
     918                        'author'       => $this->admin_id,
     919                        'author_name'  => 'Comic Book Guy',
     920                        'author_email' => 'cbg@androidsdungeon.com',
     921                        'author_url'   => 'http://androidsdungeon.com',
     922                        'content' => 'Worst Comment Ever!',
     923                        'date'    => '2014-11-07T10:14:25',
     924                );
     925
     926                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     927                $request->add_header( 'content-type', 'application/json' );
     928                $request->set_body( wp_json_encode( $params ) );
     929
     930                $response = $this->server->dispatch( $request );
     931                $this->assertEquals( 201, $response->get_status() );
     932
     933                $data = $response->get_data();
     934                $this->assertEquals( 'comment', $data['type'] );
     935
     936                $comment_id = $data['id'];
     937
     938                // Make sure the new comment is present in the collection.
     939                $collection = new WP_REST_Request( 'GET', '/wp/v2/comments' );
     940                $collection->set_param( 'post', $post_id );
     941                $collection_response = $this->server->dispatch( $collection );
     942                $collection_data = $collection_response->get_data();
     943                $this->assertEquals( $comment_id, $collection_data[0]['id'] );
     944        }
     945
     946        public function test_create_item_current_user() {
     947                $user_id = $this->factory->user->create( array(
     948                        'role' => 'subscriber',
     949                        'user_email' => 'lylelanley@example.com',
     950                        'first_name' => 'Lyle',
     951                        'last_name' => 'Lanley',
     952                        'display_name' => 'Lyle Lanley',
     953                        'user_url' => 'http://simpsons.wikia.com/wiki/Lyle_Lanley',
     954                ));
     955
     956                wp_set_current_user( $user_id );
     957
     958                $params = array(
     959                        'post' => $this->post_id,
     960                        'content' => "Well sir, there's nothing on earth like a genuine, bona fide, electrified, six-car Monorail!",
     961                );
     962
     963                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     964                $request->add_header( 'content-type', 'application/json' );
     965                $request->set_body( wp_json_encode( $params ) );
     966                $response = $this->server->dispatch( $request );
     967
     968                $this->assertEquals( 201, $response->get_status() );
     969                $data = $response->get_data();
     970                $this->assertEquals( $user_id, $data['author'] );
     971
     972                // Check author data matches
     973                $author = get_user_by( 'id', $user_id );
     974                $comment = get_comment( $data['id'] );
     975                $this->assertEquals( $author->display_name, $comment->comment_author );
     976                $this->assertEquals( $author->user_email, $comment->comment_author_email );
     977                $this->assertEquals( $author->user_url, $comment->comment_author_url );
     978        }
     979
     980        public function test_create_comment_other_user() {
     981                wp_set_current_user( $this->admin_id );
     982
     983                $params = array(
     984                        'post'    => $this->post_id,
     985                        'author_name'  => 'Homer Jay Simpson',
     986                        'author_email' => 'chunkylover53@aol.com',
     987                        'author_url'   => 'http://compuglobalhypermeganet.com',
     988                        'content' => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
     989                        'author'    => $this->subscriber_id,
     990                );
     991
     992                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     993                $request->add_header( 'content-type', 'application/json' );
     994                $request->set_body( wp_json_encode( $params ) );
     995                $response = $this->server->dispatch( $request );
     996
     997                $this->assertEquals( 201, $response->get_status() );
     998                $data = $response->get_data();
     999                $this->assertEquals( $this->subscriber_id, $data['author'] );
     1000                $this->assertEquals( 'Homer Jay Simpson', $data['author_name'] );
     1001                $this->assertEquals( 'chunkylover53@aol.com', $data['author_email'] );
     1002                $this->assertEquals( 'http://compuglobalhypermeganet.com', $data['author_url'] );
     1003        }
     1004
     1005        public function test_create_comment_other_user_without_permission() {
     1006                wp_set_current_user( $this->subscriber_id );
     1007
     1008                $params = array(
     1009                        'post'         => $this->post_id,
     1010                        'author_name'  => 'Homer Jay Simpson',
     1011                        'author_email' => 'chunkylover53@aol.com',
     1012                        'author_url'   => 'http://compuglobalhypermeganet.com',
     1013                        'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
     1014                        'author'       => $this->admin_id,
     1015                );
     1016
     1017                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1018                $request->add_header( 'content-type', 'application/json' );
     1019                $request->set_body( wp_json_encode( $params ) );
     1020                $response = $this->server->dispatch( $request );
     1021
     1022                $this->assertErrorResponse( 'rest_comment_invalid_author', $response, 403 );
     1023        }
     1024
     1025        public function test_create_comment_karma_without_permission() {
     1026                wp_set_current_user( $this->subscriber_id );
     1027
     1028                $params = array(
     1029                        'post'         => $this->post_id,
     1030                        'author_name'  => 'Homer Jay Simpson',
     1031                        'author_email' => 'chunkylover53@aol.com',
     1032                        'author_url'   => 'http://compuglobalhypermeganet.com',
     1033                        'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
     1034                        'author'       => $this->subscriber_id,
     1035                        'karma'        => 100,
     1036                );
     1037
     1038                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1039                $request->add_header( 'content-type', 'application/json' );
     1040                $request->set_body( wp_json_encode( $params ) );
     1041                $response = $this->server->dispatch( $request );
     1042
     1043                $this->assertErrorResponse( 'rest_comment_invalid_karma', $response, 403 );
     1044        }
     1045
     1046        public function test_create_comment_status_without_permission() {
     1047                wp_set_current_user( $this->subscriber_id );
     1048
     1049                $params = array(
     1050                        'post'         => $this->post_id,
     1051                        'author_name'  => 'Homer Jay Simpson',
     1052                        'author_email' => 'chunkylover53@aol.com',
     1053                        'author_url'   => 'http://compuglobalhypermeganet.com',
     1054                        'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
     1055                        'author'       => $this->subscriber_id,
     1056                        'status'        => 'approved',
     1057                );
     1058
     1059                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1060                $request->add_header( 'content-type', 'application/json' );
     1061                $request->set_body( wp_json_encode( $params ) );
     1062                $response = $this->server->dispatch( $request );
     1063
     1064                $this->assertErrorResponse( 'rest_comment_invalid_status', $response, 403 );
     1065        }
     1066
     1067        public function test_create_comment_with_status_and_IP() {
     1068                $post_id = $this->factory->post->create();
     1069                wp_set_current_user( $this->admin_id );
     1070
     1071                $params = array(
     1072                        'post'         => $post_id,
     1073                        'author_name'  => 'Comic Book Guy',
     1074                        'author_email' => 'cbg@androidsdungeon.com',
     1075                        'author_ip'    => '139.130.4.5',
     1076                        'author_url'   => 'http://androidsdungeon.com',
     1077                        'content'      => 'Worst Comment Ever!',
     1078                        'status'       => 'approved',
     1079                );
     1080
     1081                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1082                $request->add_header( 'content-type', 'application/json' );
     1083                $request->set_body( wp_json_encode( $params ) );
     1084
     1085                $response = $this->server->dispatch( $request );
     1086                $this->assertEquals( 201, $response->get_status() );
     1087
     1088                $data = $response->get_data();
     1089                $this->assertEquals( 'approved', $data['status'] );
     1090                $this->assertEquals( '139.130.4.5', $data['author_ip'] );
     1091        }
     1092
     1093        public function test_create_comment_invalid_author_IP() {
     1094                wp_set_current_user( $this->admin_id );
     1095
     1096                $params = array(
     1097                        'author_name'  => 'Comic Book Guy',
     1098                        'author_email' => 'cbg@androidsdungeon.com',
     1099                        'author_url'   => 'http://androidsdungeon.com',
     1100                        'author_ip'    => '867.5309',
     1101                        'content'      => 'Worst Comment Ever!',
     1102                        'status'       => 'approved',
     1103                );
     1104                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1105                $request->add_header( 'content-type', 'application/json' );
     1106                $request->set_body( wp_json_encode( $params ) );
     1107
     1108                $response = $this->server->dispatch( $request );
     1109
     1110                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1111        }
     1112
     1113        public function test_create_comment_no_post_id() {
     1114                wp_set_current_user( $this->admin_id );
     1115
     1116                $params = array(
     1117                        'author_name'  => 'Comic Book Guy',
     1118                        'author_email' => 'cbg@androidsdungeon.com',
     1119                        'author_url'   => 'http://androidsdungeon.com',
     1120                        'content'      => 'Worst Comment Ever!',
     1121                        'status'       => 'approved',
     1122                );
     1123                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1124                $request->add_header( 'content-type', 'application/json' );
     1125                $request->set_body( wp_json_encode( $params ) );
     1126
     1127                $response = $this->server->dispatch( $request );
     1128                $this->assertEquals( 201, $response->get_status() );
     1129        }
     1130
     1131        public function test_create_comment_no_post_id_no_permission() {
     1132                wp_set_current_user( $this->subscriber_id );
     1133
     1134                $params = array(
     1135                        'author_name'  => 'Homer Jay Simpson',
     1136                        'author_email' => 'chunkylover53@aol.com',
     1137                        'author_url'   => 'http://compuglobalhypermeganet.com',
     1138                        'content'      => 'Here\’s to alcohol: the cause of, and solution to, all of life\’s problems.',
     1139                        'author'       => $this->subscriber_id,
     1140                );
     1141                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1142                $request->add_header( 'content-type', 'application/json' );
     1143                $request->set_body( wp_json_encode( $params ) );
     1144
     1145                $response = $this->server->dispatch( $request );
     1146
     1147                $this->assertErrorResponse( 'rest_comment_invalid_post_id', $response, 403 );
     1148        }
     1149
     1150        public function test_create_comment_draft_post() {
     1151                wp_set_current_user( $this->subscriber_id );
     1152
     1153                $params = array(
     1154                        'post'         => $this->draft_id,
     1155                        'author_name'  => 'Ishmael',
     1156                        'author_email' => 'herman-melville@earthlink.net',
     1157                        'author_url'   => 'https://en.wikipedia.org/wiki/Herman_Melville',
     1158                        'content'      => 'Call me Ishmael.',
     1159                        'author'       => $this->subscriber_id,
     1160                );
     1161                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1162                $request->add_header( 'content-type', 'application/json' );
     1163                $request->set_body( wp_json_encode( $params ) );
     1164
     1165                $response = $this->server->dispatch( $request );
     1166
     1167                $this->assertErrorResponse( 'rest_comment_draft_post', $response, 403 );
     1168        }
     1169
     1170        public function test_create_comment_trash_post() {
     1171                wp_set_current_user( $this->subscriber_id );
     1172
     1173                $params = array(
     1174                        'post'         => $this->trash_id,
     1175                        'author_name'  => 'Ishmael',
     1176                        'author_email' => 'herman-melville@earthlink.net',
     1177                        'author_url'   => 'https://en.wikipedia.org/wiki/Herman_Melville',
     1178                        'content'      => 'Call me Ishmael.',
     1179                        'author'       => $this->subscriber_id,
     1180                );
     1181                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1182                $request->add_header( 'content-type', 'application/json' );
     1183                $request->set_body( wp_json_encode( $params ) );
     1184
     1185                $response = $this->server->dispatch( $request );
     1186
     1187                $this->assertErrorResponse( 'rest_comment_trash_post', $response, 403 );
     1188        }
     1189
     1190        public function test_create_comment_private_post_invalid_permission() {
     1191                wp_set_current_user( $this->subscriber_id );
     1192
     1193                $params = array(
     1194                        'post'         => $this->private_id,
     1195                        'author_name'  => 'Homer Jay Simpson',
     1196                        'author_email' => 'chunkylover53@aol.com',
     1197                        'author_url'   => 'http://compuglobalhypermeganet.com',
     1198                        'content'      => 'I\’d be a vegetarian if bacon grew on trees.',
     1199                        'author'       => $this->subscriber_id,
     1200                );
     1201                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1202                $request->add_header( 'content-type', 'application/json' );
     1203                $request->set_body( wp_json_encode( $params ) );
     1204
     1205                $response = $this->server->dispatch( $request );
     1206
     1207                $this->assertErrorResponse( 'rest_cannot_read_post', $response, 403 );
     1208        }
     1209
     1210        public function test_create_item_duplicate() {
     1211                global $wp_version;
     1212                if ( version_compare( $wp_version, '4.7-alpha', '<' ) ) {
     1213                        return $this->markTestSkipped( 'WordPress version not supported.' );
     1214                }
     1215
     1216                $this->factory->comment->create(
     1217                        array(
     1218                                'comment_post_ID'      => $this->post_id,
     1219                                'comment_author'       => 'Guy N. Cognito',
     1220                                'comment_author_email' => 'chunkylover53@aol.co.uk',
     1221                                'comment_content'      => 'Homer? Who is Homer? My name is Guy N. Cognito.',
     1222                        )
     1223                );
     1224                wp_set_current_user( 0 );
     1225
     1226                $params = array(
     1227                        'post'    => $this->post_id,
     1228                        'author_name'  => 'Guy N. Cognito',
     1229                        'author_email' => 'chunkylover53@aol.co.uk',
     1230                        'content' => 'Homer? Who is Homer? My name is Guy N. Cognito.',
     1231                );
     1232
     1233                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1234                $request->add_header( 'content-type', 'application/json' );
     1235                $request->set_body( wp_json_encode( $params ) );
     1236                $response = $this->server->dispatch( $request );
     1237
     1238                $this->assertEquals( 409, $response->get_status() );
     1239        }
     1240
     1241        public function test_create_comment_closed() {
     1242                $post_id = $this->factory->post->create( array(
     1243                        'comment_status' => 'closed',
     1244                ));
     1245                wp_set_current_user( 0 );
     1246
     1247                $params = array(
     1248                        'post'      => $post_id,
     1249                );
     1250
     1251                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1252                $request->add_header( 'content-type', 'application/json' );
     1253                $request->set_body( wp_json_encode( $params ) );
     1254                $response = $this->server->dispatch( $request );
     1255
     1256                $this->assertEquals( 403, $response->get_status() );
     1257        }
     1258
     1259        public function test_create_comment_require_login() {
     1260                wp_set_current_user( 0 );
     1261                update_option( 'comment_registration', 1 );
     1262                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1263                $request->set_param( 'post', $this->post_id );
     1264                $response = $this->server->dispatch( $request );
     1265                $this->assertEquals( 401, $response->get_status() );
     1266                $data = $response->get_data();
     1267                $this->assertEquals( 'rest_comment_login_required', $data['code'] );
     1268        }
     1269
     1270        public function test_create_item_invalid_author() {
     1271                wp_set_current_user( $this->admin_id );
     1272
     1273                $params = array(
     1274                        'post'         => $this->post_id,
     1275                        'author'       => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER,
     1276                        'content'      => 'It\'s all over\, people! We don\'t have a prayer!',
     1277                );
     1278
     1279                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1280                $request->add_header( 'content-type', 'application/json' );
     1281                $request->set_body( wp_json_encode( $params ) );
     1282
     1283                $response = $this->server->dispatch( $request );
     1284                $this->assertErrorResponse( 'rest_comment_author_invalid', $response, 400 );
     1285        }
     1286
     1287        public function test_create_item_pull_author_info() {
     1288                wp_set_current_user( $this->admin_id );
     1289
     1290                $author = new WP_User( $this->author_id );
     1291                $params = array(
     1292                        'post'         => $this->post_id,
     1293                        'author'       => $this->author_id,
     1294                        'content'      => 'It\'s all over\, people! We don\'t have a prayer!',
     1295                );
     1296
     1297                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1298                $request->add_header( 'content-type', 'application/json' );
     1299                $request->set_body( wp_json_encode( $params ) );
     1300
     1301                $response = $this->server->dispatch( $request );
     1302
     1303                $result = $response->get_data();
     1304                $this->assertSame( $this->author_id, $result['author'] );
     1305                $this->assertSame( 'Sea Captain', $result['author_name'] );
     1306                $this->assertSame( 'captain@thefryingdutchman.com', $result['author_email'] );
     1307                $this->assertSame( 'http://thefryingdutchman.com', $result['author_url'] );
     1308        }
     1309
     1310        public function test_create_comment_two_times() {
     1311                global $wp_version;
     1312                if ( version_compare( $wp_version, '4.7-alpha', '<' ) ) {
     1313                        return $this->markTestSkipped( 'WordPress version not supported.' );
     1314                }
     1315
     1316                wp_set_current_user( 0 );
     1317
     1318                $params = array(
     1319                        'post'    => $this->post_id,
     1320                        'author_name'  => 'Comic Book Guy',
     1321                        'author_email' => 'cbg@androidsdungeon.com',
     1322                        'author_url'   => 'http://androidsdungeon.com',
     1323                        'content' => 'Worst Comment Ever!',
     1324                );
     1325
     1326                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1327                $request->add_header( 'content-type', 'application/json' );
     1328                $request->set_body( wp_json_encode( $params ) );
     1329
     1330                $response = $this->server->dispatch( $request );
     1331                $this->assertEquals( 201, $response->get_status() );
     1332
     1333                $params = array(
     1334                        'post'    => $this->post_id,
     1335                        'author_name'  => 'Comic Book Guy',
     1336                        'author_email' => 'cbg@androidsdungeon.com',
     1337                        'author_url'   => 'http://androidsdungeon.com',
     1338                        'content'      => 'Shakes fist at sky',
     1339                );
     1340
     1341                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1342                $request->add_header( 'content-type', 'application/json' );
     1343                $request->set_body( wp_json_encode( $params ) );
     1344
     1345                $response = $this->server->dispatch( $request );
     1346                $this->assertEquals( 400, $response->get_status() );
     1347        }
     1348
     1349        public function test_update_item() {
     1350                $post_id = $this->factory->post->create();
     1351
     1352                wp_set_current_user( $this->admin_id );
     1353
     1354                $params = array(
     1355                        'author'       => $this->subscriber_id,
     1356                        'author_name'  => 'Disco Stu',
     1357                        'author_url'   => 'http://stusdisco.com',
     1358                        'author_email' => 'stu@stusdisco.com',
     1359                        'author_ip'    => '4.4.4.4',
     1360                        'content'      => 'Testing.',
     1361                        'date'         => '2014-11-07T10:14:25',
     1362                        'karma'        => 100,
     1363                        'post'         => $post_id,
     1364                );
     1365                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1366                $request->add_header( 'content-type', 'application/json' );
     1367                $request->set_body( wp_json_encode( $params ) );
     1368
     1369                $response = $this->server->dispatch( $request );
     1370                $this->assertEquals( 200, $response->get_status() );
     1371
     1372                $comment = $response->get_data();
     1373                $updated = get_comment( $this->approved_id );
     1374                $this->assertEquals( $params['content'], $comment['content']['raw'] );
     1375                $this->assertEquals( $params['author'], $comment['author'] );
     1376                $this->assertEquals( $params['author_name'], $comment['author_name'] );
     1377                $this->assertEquals( $params['author_url'], $comment['author_url'] );
     1378                $this->assertEquals( $params['author_email'], $comment['author_email'] );
     1379                $this->assertEquals( $params['author_ip'], $comment['author_ip'] );
     1380                $this->assertEquals( $params['post'], $comment['post'] );
     1381                $this->assertEquals( $params['karma'], $comment['karma'] );
     1382
     1383                $this->assertEquals( mysql_to_rfc3339( $updated->comment_date ), $comment['date'] );
     1384                $this->assertEquals( '2014-11-07T10:14:25', $comment['date'] );
     1385        }
     1386
     1387        public function test_update_comment_status() {
     1388                wp_set_current_user( $this->admin_id );
     1389
     1390                $comment_id = $this->factory->comment->create( array(
     1391                        'comment_approved' => 0,
     1392                        'comment_post_ID'  => $this->post_id,
     1393                ));
     1394
     1395                $params = array(
     1396                        'status' => 'approve',
     1397                );
     1398                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) );
     1399                $request->add_header( 'content-type', 'application/json' );
     1400                $request->set_body( wp_json_encode( $params ) );
     1401
     1402                $response = $this->server->dispatch( $request );
     1403                $this->assertEquals( 200, $response->get_status() );
     1404
     1405                $comment = $response->get_data();
     1406                $updated = get_comment( $comment_id );
     1407                $this->assertEquals( 'approved', $comment['status'] );
     1408                $this->assertEquals( 1, $updated->comment_approved );
     1409        }
     1410
     1411        public function test_update_comment_field_does_not_use_default_values() {
     1412                wp_set_current_user( $this->admin_id );
     1413
     1414                $comment_id = $this->factory->comment->create( array(
     1415                        'comment_approved' => 0,
     1416                        'comment_post_ID'  => $this->post_id,
     1417                        'comment_content'  => 'some content',
     1418                ));
     1419
     1420                $params = array(
     1421                        'status' => 'approve',
     1422                );
     1423                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) );
     1424                $request->add_header( 'content-type', 'application/json' );
     1425                $request->set_body( wp_json_encode( $params ) );
     1426
     1427                $response = $this->server->dispatch( $request );
     1428                $this->assertEquals( 200, $response->get_status() );
     1429
     1430                $comment = $response->get_data();
     1431                $updated = get_comment( $comment_id );
     1432                $this->assertEquals( 'approved', $comment['status'] );
     1433                $this->assertEquals( 1, $updated->comment_approved );
     1434                $this->assertEquals( 'some content', $updated->comment_content );
     1435        }
     1436
     1437        public function test_update_comment_date_gmt() {
     1438                wp_set_current_user( $this->admin_id );
     1439
     1440                $params = array(
     1441                        'date_gmt' => '2015-05-07T10:14:25',
     1442                        'content'  => 'I\'ll be deep in the cold, cold ground before I recognize Missouri.',
     1443                );
     1444                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1445                $request->add_header( 'content-type', 'application/json' );
     1446                $request->set_body( wp_json_encode( $params ) );
     1447
     1448                $response = $this->server->dispatch( $request );
     1449                $this->assertEquals( 200, $response->get_status() );
     1450
     1451                $comment = $response->get_data();
     1452                $updated = get_comment( $this->approved_id );
     1453                $this->assertEquals( $params['date_gmt'], $comment['date_gmt'] );
     1454                $this->assertEquals( $params['date_gmt'], mysql_to_rfc3339( $updated->comment_date_gmt ) );
     1455        }
     1456
     1457        public function test_update_comment_invalid_type() {
     1458                wp_set_current_user( $this->admin_id );
     1459
     1460                $params = array(
     1461                        'type' => 'trackback',
     1462                );
     1463                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1464                $request->add_header( 'content-type', 'application/json' );
     1465                $request->set_body( wp_json_encode( $params ) );
     1466
     1467                $response = $this->server->dispatch( $request );
     1468                $this->assertErrorResponse( 'rest_comment_invalid_type', $response, 404 );
     1469        }
     1470
     1471        public function test_update_comment_with_raw_property() {
     1472                wp_set_current_user( $this->admin_id );
     1473
     1474                $params = array(
     1475                        'content' => array(
     1476                                'raw' => 'What the heck kind of name is Persephone?',
     1477                        ),
     1478                );
     1479                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1480                $request->add_header( 'content-type', 'application/json' );
     1481                $request->set_body( wp_json_encode( $params ) );
     1482
     1483                $response = $this->server->dispatch( $request );
     1484
     1485                $this->assertEquals( 200, $response->get_status() );
     1486
     1487                $comment = $response->get_data();
     1488                $updated = get_comment( $this->approved_id );
     1489                $this->assertEquals( $params['content']['raw'], $updated->comment_content );
     1490        }
     1491
     1492        public function test_update_item_invalid_date() {
     1493                wp_set_current_user( $this->admin_id );
     1494
     1495                $params = array(
     1496                        'content' => rand_str(),
     1497                        'date'    => rand_str(),
     1498                );
     1499
     1500                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1501                $request->add_header( 'content-type', 'application/json' );
     1502                $request->set_body( wp_json_encode( $params ) );
     1503
     1504                $response = $this->server->dispatch( $request );
     1505                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1506        }
     1507
     1508        public function test_update_item_invalid_date_gmt() {
     1509                wp_set_current_user( $this->admin_id );
     1510
     1511                $params = array(
     1512                        'content'  => rand_str(),
     1513                        'date_gmt' => rand_str(),
     1514                );
     1515
     1516                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1517                $request->add_header( 'content-type', 'application/json' );
     1518                $request->set_body( wp_json_encode( $params ) );
     1519
     1520                $response = $this->server->dispatch( $request );
     1521                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1522        }
     1523
     1524        public function test_update_comment_invalid_id() {
     1525                wp_set_current_user( 0 );
     1526
     1527                $params = array(
     1528                        'content' => 'Oh, they have the internet on computers now!',
     1529                );
     1530                $request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     1531                $request->add_header( 'content-type', 'application/json' );
     1532                $request->set_body( wp_json_encode( $params ) );
     1533
     1534                $response = $this->server->dispatch( $request );
     1535                $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 );
     1536        }
     1537
     1538        public function test_update_comment_invalid_permission() {
     1539                wp_set_current_user( 0 );
     1540
     1541                $params = array(
     1542                        'content' => 'Disco Stu likes disco music.',
     1543                );
     1544                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $this->hold_id ) );
     1545                $request->add_header( 'content-type', 'application/json' );
     1546                $request->set_body( wp_json_encode( $params ) );
     1547
     1548                $response = $this->server->dispatch( $request );
     1549                $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
     1550        }
     1551
     1552        public function test_update_comment_private_post_invalid_permission() {
     1553                $private_comment_id = $this->factory->comment->create( array(
     1554                        'comment_approved' => 1,
     1555                        'comment_post_ID'  => $this->private_id,
     1556                        'user_id'          => 0,
     1557                ));
     1558
     1559                wp_set_current_user( $this->subscriber_id );
     1560
     1561                $params = array(
     1562                        'content' => 'Disco Stu likes disco music.',
     1563                );
     1564                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $private_comment_id ) );
     1565                $request->add_header( 'content-type', 'application/json' );
     1566                $request->set_body( wp_json_encode( $params ) );
     1567
     1568                $response = $this->server->dispatch( $request );
     1569                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     1570        }
     1571
     1572        public function test_update_comment_with_children_link() {
     1573                wp_set_current_user( $this->admin_id );
     1574                $comment_id_1 = $this->factory->comment->create( array(
     1575                        'comment_approved' => 1,
     1576                        'comment_post_ID'  => $this->post_id,
     1577                        'user_id'          => $this->subscriber_id,
     1578                ) );
     1579
     1580                $child_comment = $this->factory->comment->create( array(
     1581                        'comment_approved' => 1,
     1582                        'comment_post_ID'  => $this->post_id,
     1583                        'user_id'          => $this->subscriber_id,
     1584                ) );
     1585
     1586                // Check if comment 1 does not have the child link.
     1587                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
     1588                $response = $this->server->dispatch( $request );
     1589                $this->assertEquals( 200, $response->get_status() );
     1590                $this->assertArrayNotHasKey( 'children', $response->get_links() );
     1591
     1592                // Change the comment parent.
     1593                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%s', $child_comment ) );
     1594                $request->set_param( 'parent', $comment_id_1 );
     1595                $request->set_param( 'content', rand_str() );
     1596                $response = $this->server->dispatch( $request );
     1597                $this->assertEquals( 200, $response->get_status() );
     1598
     1599                // Check if comment 1 now has the child link.
     1600                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
     1601                $response = $this->server->dispatch( $request );
     1602                $this->assertEquals( 200, $response->get_status() );
     1603                $this->assertArrayHasKey( 'children', $response->get_links() );
     1604        }
     1605
     1606        public function test_delete_item() {
     1607                wp_set_current_user( $this->admin_id );
     1608
     1609                $comment_id = $this->factory->comment->create( array(
     1610                        'comment_approved' => 1,
     1611                        'comment_post_ID'  => $this->post_id,
     1612                        'user_id'          => $this->subscriber_id,
     1613                ));
     1614                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $comment_id ) );
     1615
     1616                $response = $this->server->dispatch( $request );
     1617                $this->assertEquals( 200, $response->get_status() );
     1618                $data = $response->get_data();
     1619                $this->assertEquals( $this->post_id, $data['post'] );
     1620        }
     1621
     1622        public function test_delete_item_skip_trash() {
     1623                wp_set_current_user( $this->admin_id );
     1624
     1625                $comment_id = $this->factory->comment->create( array(
     1626                        'comment_approved' => 1,
     1627                        'comment_post_ID'  => $this->post_id,
     1628                        'user_id'          => $this->subscriber_id,
     1629                ));
     1630                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $comment_id ) );
     1631                $request['force'] = true;
     1632
     1633                $response = $this->server->dispatch( $request );
     1634                $this->assertEquals( 200, $response->get_status() );
     1635                $data = $response->get_data();
     1636                $this->assertEquals( $this->post_id, $data['post'] );
     1637        }
     1638
     1639        public function test_delete_item_already_trashed() {
     1640                wp_set_current_user( $this->admin_id );
     1641
     1642                $comment_id = $this->factory->comment->create( array(
     1643                        'comment_approved' => 1,
     1644                        'comment_post_ID'  => $this->post_id,
     1645                        'user_id'          => $this->subscriber_id,
     1646                ));
     1647                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $comment_id ) );
     1648                $response = $this->server->dispatch( $request );
     1649                $this->assertEquals( 200, $response->get_status() );
     1650                $data = $response->get_data();
     1651                $response = $this->server->dispatch( $request );
     1652                $this->assertErrorResponse( 'rest_already_trashed', $response, 410 );
     1653        }
     1654
     1655        public function test_delete_comment_invalid_id() {
     1656                wp_set_current_user( $this->admin_id );
     1657
     1658                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ) );
     1659
     1660                $response = $this->server->dispatch( $request );
     1661                $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 );
     1662        }
     1663
     1664        public function test_delete_comment_without_permission() {
     1665                wp_set_current_user( $this->subscriber_id );
     1666
     1667                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1668
     1669                $response = $this->server->dispatch( $request );
     1670                $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
     1671        }
     1672
     1673        public function test_delete_child_comment_link() {
     1674                wp_set_current_user( $this->admin_id );
     1675                $comment_id_1 = $this->factory->comment->create( array(
     1676                        'comment_approved' => 1,
     1677                        'comment_post_ID'  => $this->post_id,
     1678                        'user_id'          => $this->subscriber_id,
     1679                ) );
     1680
     1681                $child_comment = $this->factory->comment->create( array(
     1682                        'comment_approved' => 1,
     1683                        'comment_parent'   => $comment_id_1,
     1684                        'comment_post_ID'  => $this->post_id,
     1685                        'user_id'          => $this->subscriber_id,
     1686                ) );
     1687
     1688                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/comments/%s', $child_comment ) );
     1689                $response = $this->server->dispatch( $request );
     1690                $this->assertEquals( 200, $response->get_status() );
     1691
     1692                // Verify children link is gone.
     1693                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $comment_id_1 ) );
     1694                $response = $this->server->dispatch( $request );
     1695                $this->assertEquals( 200, $response->get_status() );
     1696                $this->assertArrayNotHasKey( 'children', $response->get_links() );
     1697        }
     1698
     1699        public function test_get_item_schema() {
     1700                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
     1701                $response = $this->server->dispatch( $request );
     1702                $data = $response->get_data();
     1703                $properties = $data['schema']['properties'];
     1704                $this->assertEquals( 18, count( $properties ) );
     1705                $this->assertArrayHasKey( 'id', $properties );
     1706                $this->assertArrayHasKey( 'author', $properties );
     1707                $this->assertArrayHasKey( 'author_avatar_urls', $properties );
     1708                $this->assertArrayHasKey( 'author_email', $properties );
     1709                $this->assertArrayHasKey( 'author_ip', $properties );
     1710                $this->assertArrayHasKey( 'author_name', $properties );
     1711                $this->assertArrayHasKey( 'author_url', $properties );
     1712                $this->assertArrayHasKey( 'author_user_agent', $properties );
     1713                $this->assertArrayHasKey( 'content', $properties );
     1714                $this->assertArrayHasKey( 'date', $properties );
     1715                $this->assertArrayHasKey( 'date_gmt', $properties );
     1716                $this->assertArrayHasKey( 'karma', $properties );
     1717                $this->assertArrayHasKey( 'link', $properties );
     1718                $this->assertArrayHasKey( 'meta', $properties );
     1719                $this->assertArrayHasKey( 'parent', $properties );
     1720                $this->assertArrayHasKey( 'post', $properties );
     1721                $this->assertArrayHasKey( 'status', $properties );
     1722                $this->assertArrayHasKey( 'type', $properties );
     1723        }
     1724
     1725        public function test_get_item_schema_show_avatar() {
     1726                update_option( 'show_avatars', false );
     1727                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
     1728                $response = $this->server->dispatch( $request );
     1729                $data = $response->get_data();
     1730                $properties = $data['schema']['properties'];
     1731
     1732                $this->assertArrayNotHasKey( 'author_avatar_urls', $properties );
     1733        }
     1734
     1735        public function test_get_additional_field_registration() {
     1736
     1737                $schema = array(
     1738                        'type'        => 'integer',
     1739                        'description' => 'Some integer of mine',
     1740                        'enum'        => array( 1, 2, 3, 4 ),
     1741                        'context'     => array( 'view', 'edit' ),
     1742                );
     1743
     1744                register_rest_field( 'comment', 'my_custom_int', array(
     1745                        'schema'          => $schema,
     1746                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     1747                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     1748                ) );
     1749
     1750                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comments' );
     1751
     1752                $response = $this->server->dispatch( $request );
     1753                $data = $response->get_data();
     1754
     1755                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     1756                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     1757
     1758                $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $this->approved_id );
     1759
     1760                $response = $this->server->dispatch( $request );
     1761                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     1762
     1763                $request = new WP_REST_Request( 'POST', '/wp/v2/comments/' . $this->approved_id );
     1764                $request->set_body_params(array(
     1765                        'my_custom_int' => 123,
     1766                        'content'       => 'abc',
     1767                ));
     1768
     1769                wp_set_current_user( 1 );
     1770                $this->server->dispatch( $request );
     1771                $this->assertEquals( 123, get_comment_meta( $this->approved_id, 'my_custom_int', true ) );
     1772
     1773                $request = new WP_REST_Request( 'POST', '/wp/v2/comments' );
     1774                $request->set_body_params(array(
     1775                        'my_custom_int' => 123,
     1776                        'title'         => 'hello',
     1777                        'content'       => 'goodbye',
     1778                        'post'          => $this->post_id,
     1779                ));
     1780
     1781                $response = $this->server->dispatch( $request );
     1782
     1783                $this->assertEquals( 123, $response->data['my_custom_int'] );
     1784
     1785                global $wp_rest_additional_fields;
     1786                $wp_rest_additional_fields = array();
     1787        }
     1788
     1789        public function test_additional_field_update_errors() {
     1790                $schema = array(
     1791                        'type'        => 'integer',
     1792                        'description' => 'Some integer of mine',
     1793                        'enum'        => array( 1, 2, 3, 4 ),
     1794                        'context'     => array( 'view', 'edit' ),
     1795                );
     1796
     1797                register_rest_field( 'comment', 'my_custom_int', array(
     1798                        'schema'          => $schema,
     1799                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     1800                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     1801                ) );
     1802
     1803                wp_set_current_user( $this->admin_id );
     1804
     1805                // Check for error on update.
     1806                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', $this->approved_id ) );
     1807                $request->set_body_params(array(
     1808                        'my_custom_int' => 'returnError',
     1809                        'content' => 'abc',
     1810                ));
     1811
     1812                $response = $this->server->dispatch( $request );
     1813
     1814                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1815
     1816                global $wp_rest_additional_fields;
     1817                $wp_rest_additional_fields = array();
     1818        }
     1819
     1820        public function additional_field_get_callback( $object ) {
     1821                return get_comment_meta( $object['id'], 'my_custom_int', true );
     1822        }
     1823
     1824        public function additional_field_update_callback( $value, $comment ) {
     1825                if ( 'returnError' === $value ) {
     1826                        return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
     1827                }
     1828                update_comment_meta( $comment->comment_ID, 'my_custom_int', $value );
     1829        }
     1830
     1831        protected function check_comment_data( $data, $context, $links ) {
     1832                $comment = get_comment( $data['id'] );
     1833
     1834                $this->assertEquals( $comment->comment_ID, $data['id'] );
     1835                $this->assertEquals( $comment->comment_post_ID, $data['post'] );
     1836                $this->assertEquals( $comment->comment_parent, $data['parent'] );
     1837                $this->assertEquals( $comment->user_id, $data['author'] );
     1838                $this->assertEquals( $comment->comment_author, $data['author_name'] );
     1839                $this->assertEquals( $comment->comment_author_url, $data['author_url'] );
     1840                $this->assertEquals( wpautop( $comment->comment_content ), $data['content']['rendered'] );
     1841                $this->assertEquals( mysql_to_rfc3339( $comment->comment_date ), $data['date'] );
     1842                $this->assertEquals( mysql_to_rfc3339( $comment->comment_date_gmt ), $data['date_gmt'] );
     1843                $this->assertEquals( get_comment_link( $comment ), $data['link'] );
     1844                $this->assertContains( 'author_avatar_urls', $data );
     1845                $this->assertEqualSets( array(
     1846                        'self',
     1847                        'collection',
     1848                        'up',
     1849                ), array_keys( $links ) );
     1850
     1851                if ( 'edit' === $context ) {
     1852                        $this->assertEquals( $comment->comment_author_email, $data['author_email'] );
     1853                        $this->assertEquals( $comment->comment_author_IP, $data['author_ip'] );
     1854                        $this->assertEquals( $comment->comment_agent, $data['author_user_agent'] );
     1855                        $this->assertEquals( $comment->comment_content, $data['content']['raw'] );
     1856                        $this->assertEquals( $comment->comment_karma, $data['karma'] );
     1857                }
     1858
     1859                if ( 'edit' !== $context ) {
     1860                        $this->assertArrayNotHasKey( 'author_email', $data );
     1861                        $this->assertArrayNotHasKey( 'author_ip', $data );
     1862                        $this->assertArrayNotHasKey( 'author_user_agent', $data );
     1863                        $this->assertArrayNotHasKey( 'raw', $data['content'] );
     1864                        $this->assertArrayNotHasKey( 'karma', $data );
     1865                }
     1866        }
     1867}
  • tests/phpunit/tests/rest-api/rest-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Controller functionality
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Controller extends WP_Test_REST_TestCase {
     13
     14        public function setUp() {
     15                parent::setUp();
     16                $this->request = new WP_REST_Request( 'GET', '/wp/v2/testroute', array(
     17                        'args'     => array(
     18                                'someinteger'     => array(
     19                                        'type'        => 'integer',
     20                                ),
     21                                'someboolean'     => array(
     22                                        'type'        => 'boolean',
     23                                ),
     24                                'somestring'      => array(
     25                                        'type'        => 'string',
     26                                ),
     27                                'someenum'        => array(
     28                                        'type'        => 'string',
     29                                        'enum'        => array( 'a' ),
     30                                ),
     31                                'somedate'        => array(
     32                                        'type'        => 'string',
     33                                        'format'      => 'date-time',
     34                                ),
     35                                'someemail'       => array(
     36                                        'type'        => 'string',
     37                                        'format'      => 'email',
     38                                ),
     39                        ),
     40                ));
     41        }
     42
     43        public function test_validate_schema_type_integer() {
     44
     45                $this->assertTrue(
     46                        rest_validate_request_arg( '123', $this->request, 'someinteger' )
     47                );
     48
     49                $this->assertErrorResponse(
     50                        'rest_invalid_param',
     51                        rest_validate_request_arg( 'abc', $this->request, 'someinteger' )
     52                );
     53        }
     54
     55        public function test_validate_schema_type_boolean() {
     56
     57                $this->assertTrue(
     58                        rest_validate_request_arg( true, $this->request, 'someboolean' )
     59                );
     60                $this->assertTrue(
     61                        rest_validate_request_arg( false, $this->request, 'someboolean' )
     62                );
     63
     64                $this->assertTrue(
     65                        rest_validate_request_arg( 'true', $this->request, 'someboolean' )
     66                );
     67                $this->assertTrue(
     68                        rest_validate_request_arg( 'TRUE', $this->request, 'someboolean' )
     69                );
     70                $this->assertTrue(
     71                        rest_validate_request_arg( 'false', $this->request, 'someboolean' )
     72                );
     73                $this->assertTrue(
     74                        rest_validate_request_arg( 'False', $this->request, 'someboolean' )
     75                );
     76                $this->assertTrue(
     77                        rest_validate_request_arg( '1', $this->request, 'someboolean' )
     78                );
     79                $this->assertTrue(
     80                        rest_validate_request_arg( '0', $this->request, 'someboolean' )
     81                );
     82                $this->assertTrue(
     83                        rest_validate_request_arg( 1, $this->request, 'someboolean' )
     84                );
     85                $this->assertTrue(
     86                        rest_validate_request_arg( 0, $this->request, 'someboolean' )
     87                );
     88
     89                // Check sanitize testing.
     90                $this->assertEquals( false,
     91                        rest_sanitize_request_arg( 'false', $this->request, 'someboolean' )
     92                );
     93                $this->assertEquals( false,
     94                        rest_sanitize_request_arg( '0', $this->request, 'someboolean' )
     95                );
     96                $this->assertEquals( false,
     97                        rest_sanitize_request_arg( 0, $this->request, 'someboolean' )
     98                );
     99                $this->assertEquals( false,
     100                        rest_sanitize_request_arg( 'FALSE', $this->request, 'someboolean' )
     101                );
     102                $this->assertEquals( true,
     103                        rest_sanitize_request_arg( 'true', $this->request, 'someboolean' )
     104                );
     105                $this->assertEquals( true,
     106                        rest_sanitize_request_arg( '1', $this->request, 'someboolean' )
     107                );
     108                $this->assertEquals( true,
     109                        rest_sanitize_request_arg( 1, $this->request, 'someboolean' )
     110                );
     111                $this->assertEquals( true,
     112                        rest_sanitize_request_arg( 'TRUE', $this->request, 'someboolean' )
     113                );
     114
     115                $this->assertErrorResponse(
     116                        'rest_invalid_param',
     117                        rest_validate_request_arg( '123', $this->request, 'someboolean' )
     118                );
     119        }
     120
     121        public function test_validate_schema_type_string() {
     122
     123                $this->assertTrue(
     124                        rest_validate_request_arg( '123', $this->request, 'somestring' )
     125                );
     126
     127                $this->assertErrorResponse(
     128                        'rest_invalid_param',
     129                        rest_validate_request_arg( array( 'foo' => 'bar' ), $this->request, 'somestring' )
     130                );
     131        }
     132
     133        public function test_validate_schema_enum() {
     134
     135                $this->assertTrue(
     136                        rest_validate_request_arg( 'a', $this->request, 'someenum' )
     137                );
     138
     139                $this->assertErrorResponse(
     140                        'rest_invalid_param',
     141                        rest_validate_request_arg( 'd', $this->request, 'someenum' )
     142                );
     143        }
     144
     145        public function test_validate_schema_format_email() {
     146
     147                $this->assertTrue(
     148                        rest_validate_request_arg( 'joe@foo.bar', $this->request, 'someemail' )
     149                );
     150
     151                $this->assertErrorResponse(
     152                        'rest_invalid_email',
     153                        rest_validate_request_arg( 'd', $this->request, 'someemail' )
     154                );
     155        }
     156
     157        public function test_validate_schema_format_date_time() {
     158
     159                $this->assertTrue(
     160                        rest_validate_request_arg( '2010-01-01T12:00:00', $this->request, 'somedate' )
     161                );
     162
     163                $this->assertErrorResponse(
     164                        'rest_invalid_date',
     165                        rest_validate_request_arg( '2010-18-18T12:00:00', $this->request, 'somedate' )
     166                );
     167        }
     168
     169        public function test_get_endpoint_args_for_item_schema_description() {
     170                $controller = new WP_REST_Test_Controller();
     171                $args       = $controller->get_endpoint_args_for_item_schema();
     172                $this->assertEquals( 'A pretty string.', $args['somestring']['description'] );
     173                $this->assertFalse( isset( $args['someinteger']['description'] ) );
     174        }
     175
     176        public function test_get_endpoint_args_for_item_schema_arg_options() {
     177
     178                $controller = new WP_REST_Test_Controller();
     179                $args       = $controller->get_endpoint_args_for_item_schema();
     180
     181                $this->assertFalse( $args['someargoptions']['required'] );
     182                $this->assertEquals( '__return_true', $args['someargoptions']['sanitize_callback'] );
     183        }
     184
     185        public function test_get_endpoint_args_for_item_schema_default_value() {
     186
     187                $controller = new WP_REST_Test_Controller();
     188
     189                $args = $controller->get_endpoint_args_for_item_schema();
     190
     191                $this->assertEquals( 'a', $args['somedefault']['default'] );
     192        }
     193
     194        public $rest_the_post_filter_apply_count = 0;
     195
     196        public function test_get_post() {
     197                $post_id = $this->factory()->post->create( array( 'post_title' => 'Original' ) );
     198                $controller = new WP_REST_Test_Controller();
     199
     200                $post = $controller->get_post( $post_id );
     201                $this->assertEquals( 'Original', $post->post_title );
     202
     203                $filter_apply_count = $this->rest_the_post_filter_apply_count;
     204                add_filter( 'rest_the_post', array( $this, 'filter_rest_the_post_for_test_get_post' ), 10, 2 );
     205                $post = $controller->get_post( $post_id );
     206                $this->assertEquals( 'Overridden', $post->post_title );
     207                $this->assertEquals( 1 + $filter_apply_count, $this->rest_the_post_filter_apply_count );
     208        }
     209
     210        public function filter_rest_the_post_for_test_get_post( $post, $post_id ) {
     211                $this->assertInstanceOf( 'WP_Post', $post );
     212                $this->assertInternalType( 'int', $post_id );
     213                $post->post_title = 'Overridden';
     214                $this->rest_the_post_filter_apply_count += 1;
     215                return $post;
     216        }
     217}
  • tests/phpunit/tests/rest-api/rest-pages-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Posts_Controller functionality, used for
     4 * Pages
     5 *
     6 * @package WordPress
     7 * @subpackage REST API
     8 */
     9
     10 /**
     11  * @group restapi
     12  */
     13class WP_Test_REST_Pages_Controller extends WP_Test_REST_Post_Type_Controller_Testcase {
     14
     15        public function setUp() {
     16                parent::setUp();
     17
     18                $this->editor_id = $this->factory->user->create( array(
     19                        'role' => 'editor',
     20                ) );
     21                $this->author_id = $this->factory->user->create( array(
     22                        'role' => 'author',
     23                ) );
     24
     25                $this->has_setup_template = false;
     26                add_filter( 'theme_page_templates', array( $this, 'filter_theme_page_templates' ) );
     27        }
     28
     29        public function test_register_routes() {
     30
     31        }
     32
     33        public function test_context_param() {
     34                // Collection
     35                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' );
     36                $response = $this->server->dispatch( $request );
     37                $data = $response->get_data();
     38                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     39                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     40                // Single
     41                $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );
     42                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages/' . $page_id );
     43                $response = $this->server->dispatch( $request );
     44                $data = $response->get_data();
     45                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     46                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     47        }
     48
     49        public function test_registered_query_params() {
     50                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' );
     51                $response = $this->server->dispatch( $request );
     52                $data = $response->get_data();
     53                $keys = array_keys( $data['endpoints'][0]['args'] );
     54                sort( $keys );
     55                $this->assertEquals( array(
     56                        'after',
     57                        'author',
     58                        'author_exclude',
     59                        'before',
     60                        'context',
     61                        'exclude',
     62                        'filter',
     63                        'include',
     64                        'menu_order',
     65                        'offset',
     66                        'order',
     67                        'orderby',
     68                        'page',
     69                        'parent',
     70                        'parent_exclude',
     71                        'per_page',
     72                        'search',
     73                        'slug',
     74                        'status',
     75                        ), $keys );
     76        }
     77
     78        public function test_get_items() {
     79
     80        }
     81
     82        public function test_get_items_parent_query() {
     83                $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
     84                $id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1 ) );
     85                // No parent
     86                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     87                $response = $this->server->dispatch( $request );
     88                $data = $response->get_data();
     89                $this->assertEquals( 2, count( $data ) );
     90                // Filter to parent
     91                $request->set_param( 'parent', $id1 );
     92                $response = $this->server->dispatch( $request );
     93                $data = $response->get_data();
     94                $this->assertEquals( 1, count( $data ) );
     95                $this->assertEquals( $id2, $data[0]['id'] );
     96        }
     97
     98        public function test_get_items_parents_query() {
     99                $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
     100                $id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1 ) );
     101                $id3 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
     102                $id4 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id3 ) );
     103                // No parent
     104                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     105                $response = $this->server->dispatch( $request );
     106                $data = $response->get_data();
     107                $this->assertEquals( 4, count( $data ) );
     108                // Filter to parents
     109                $request->set_param( 'parent', array( $id1, $id3 ) );
     110                $response = $this->server->dispatch( $request );
     111                $data = $response->get_data();
     112                $this->assertEquals( 2, count( $data ) );
     113                $this->assertEqualSets( array( $id2, $id4 ), wp_list_pluck( $data, 'id' ) );
     114        }
     115
     116        public function test_get_items_parent_exclude_query() {
     117                $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
     118                $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1 ) );
     119                // No parent
     120                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     121                $response = $this->server->dispatch( $request );
     122                $data = $response->get_data();
     123                $this->assertEquals( 2, count( $data ) );
     124                // Filter to parent
     125                $request->set_param( 'parent_exclude', $id1 );
     126                $response = $this->server->dispatch( $request );
     127                $data = $response->get_data();
     128                $this->assertEquals( 1, count( $data ) );
     129                $this->assertEquals( $id1, $data[0]['id'] );
     130        }
     131
     132        public function test_get_items_menu_order_query() {
     133                $id1 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
     134                $id2 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 2 ) );
     135                $id3 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 3 ) );
     136                $id4 = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 1 ) );
     137                // No parent
     138                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     139                $response = $this->server->dispatch( $request );
     140                $data = $response->get_data();
     141                $this->assertEqualSets( array( $id1, $id2, $id3, $id4 ), wp_list_pluck( $data, 'id' ) );
     142                // Filter to menu_order
     143                $request->set_param( 'menu_order', 1 );
     144                $response = $this->server->dispatch( $request );
     145                $data = $response->get_data();
     146                $this->assertEqualSets( array( $id4 ), wp_list_pluck( $data, 'id' ) );
     147                // Order by menu order
     148                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     149                $request->set_param( 'order', 'asc' );
     150                $request->set_param( 'orderby', 'menu_order' );
     151                $response = $this->server->dispatch( $request );
     152                $data = $response->get_data();
     153                $this->assertEquals( $id1, $data[0]['id'] );
     154                $this->assertEquals( $id4, $data[1]['id'] );
     155                $this->assertEquals( $id2, $data[2]['id'] );
     156                $this->assertEquals( $id3, $data[3]['id'] );
     157        }
     158
     159        public function test_get_items_min_max_pages_query() {
     160                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     161                $request->set_param( 'per_page', 0 );
     162                $response = $this->server->dispatch( $request );
     163                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     164                $data = $response->get_data();
     165                // Safe format for 4.4 and 4.5 https://core.trac.wordpress.org/ticket/35028
     166                $first_error = array_shift( $data['data']['params'] );
     167                $this->assertContains( 'per_page must be between 1 (inclusive) and 100 (inclusive)', $first_error );
     168                $request->set_param( 'per_page', 101 );
     169                $response = $this->server->dispatch( $request );
     170                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     171                $data = $response->get_data();
     172                $first_error = array_shift( $data['data']['params'] );
     173                $this->assertContains( 'per_page must be between 1 (inclusive) and 100 (inclusive)', $first_error );
     174        }
     175
     176        public function test_get_items_private_filter_query_var() {
     177                // Private query vars inaccessible to unauthorized users
     178                wp_set_current_user( 0 );
     179                $page_id = $this->factory->post->create( array( 'post_status' => 'publish', 'post_type' => 'page' ) );
     180                $draft_id = $this->factory->post->create( array( 'post_status' => 'draft', 'post_type' => 'page' ) );
     181                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     182                $request->set_param( 'filter', array( 'post_status' => 'draft' ) );
     183                $response = $this->server->dispatch( $request );
     184                $data = $response->get_data();
     185                $this->assertCount( 1, $data );
     186                $this->assertEquals( $page_id, $data[0]['id'] );
     187                // But they are accessible to authorized users
     188                wp_set_current_user( $this->editor_id );
     189                $response = $this->server->dispatch( $request );
     190                $data = $response->get_data();
     191                $this->assertCount( 1, $data );
     192                $this->assertEquals( $draft_id, $data[0]['id'] );
     193        }
     194
     195        public function test_get_items_invalid_date() {
     196                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     197                $request->set_param( 'after', rand_str() );
     198                $request->set_param( 'before', rand_str() );
     199                $response = $this->server->dispatch( $request );
     200                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     201        }
     202
     203        public function test_get_items_valid_date() {
     204                $post1 = $this->factory->post->create( array( 'post_date' => '2016-01-15T00:00:00Z', 'post_type' => 'page' ) );
     205                $post2 = $this->factory->post->create( array( 'post_date' => '2016-01-16T00:00:00Z', 'post_type' => 'page' ) );
     206                $post3 = $this->factory->post->create( array( 'post_date' => '2016-01-17T00:00:00Z', 'post_type' => 'page' ) );
     207                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     208                $request->set_param( 'after', '2016-01-15T00:00:00Z' );
     209                $request->set_param( 'before', '2016-01-17T00:00:00Z' );
     210                $response = $this->server->dispatch( $request );
     211                $data = $response->get_data();
     212                $this->assertCount( 1, $data );
     213                $this->assertEquals( $post2, $data[0]['id'] );
     214        }
     215
     216        public function test_get_item() {
     217
     218        }
     219
     220        public function test_get_item_invalid_post_type() {
     221                $post_id = $this->factory->post->create();
     222                $request = new WP_REST_Request( 'GET', '/wp/v2/pages/' . $post_id );
     223                $response = $this->server->dispatch( $request );
     224                $this->assertEquals( 404, $response->get_status() );
     225        }
     226
     227        public function test_create_item() {
     228
     229        }
     230
     231        public function test_create_item_with_template() {
     232                wp_set_current_user( $this->editor_id );
     233
     234                $request = new WP_REST_Request( 'POST', '/wp/v2/pages' );
     235                $params = $this->set_post_data( array(
     236                        'template'       => 'page-my-test-template.php',
     237                ) );
     238                $request->set_body_params( $params );
     239                $response = $this->server->dispatch( $request );
     240
     241                $data = $response->get_data();
     242                $new_post = get_post( $data['id'] );
     243                $this->assertEquals( 'page-my-test-template.php', $data['template'] );
     244                $this->assertEquals( 'page-my-test-template.php', get_page_template_slug( $new_post->ID ) );
     245        }
     246
     247        public function test_create_page_with_parent() {
     248                $page_id = $this->factory->post->create( array(
     249                        'type' => 'page',
     250                ) );
     251                wp_set_current_user( $this->editor_id );
     252
     253                $request = new WP_REST_Request( 'POST', '/wp/v2/pages' );
     254                $params = $this->set_post_data( array(
     255                        'parent' => $page_id,
     256                ) );
     257                $request->set_body_params( $params );
     258                $response = $this->server->dispatch( $request );
     259
     260                $this->assertEquals( 201, $response->get_status() );
     261
     262                $links = $response->get_links();
     263                $this->assertArrayHasKey( 'up', $links );
     264
     265                $data = $response->get_data();
     266                $new_post = get_post( $data['id'] );
     267                $this->assertEquals( $page_id, $data['parent'] );
     268                $this->assertEquals( $page_id, $new_post->post_parent );
     269        }
     270
     271        public function test_create_page_with_invalid_parent() {
     272                wp_set_current_user( $this->editor_id );
     273
     274                $request = new WP_REST_Request( 'POST', '/wp/v2/pages' );
     275                $params = $this->set_post_data( array(
     276                        'parent' => -1,
     277                ) );
     278                $request->set_body_params( $params );
     279                $response = $this->server->dispatch( $request );
     280
     281                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 400 );
     282        }
     283
     284        public function test_update_item() {
     285
     286        }
     287
     288        public function test_delete_item() {
     289
     290        }
     291
     292        public function test_prepare_item() {
     293
     294        }
     295
     296        public function test_get_pages_params() {
     297                $this->factory->post->create_many( 8, array(
     298                        'post_type' => 'page',
     299                ) );
     300
     301                $request = new WP_REST_Request( 'GET', '/wp/v2/pages' );
     302                $request->set_query_params( array(
     303                        'page'           => 2,
     304                        'per_page'       => 4,
     305                ) );
     306                $response = $this->server->dispatch( $request );
     307
     308                $this->assertEquals( 200, $response->get_status() );
     309
     310                $headers = $response->get_headers();
     311                $this->assertEquals( 8, $headers['X-WP-Total'] );
     312                $this->assertEquals( 2, $headers['X-WP-TotalPages'] );
     313
     314                $all_data = $response->get_data();
     315                $this->assertEquals( 4, count( $all_data ) );
     316                foreach ( $all_data as $post ) {
     317                        $this->assertEquals( 'page', $post['type'] );
     318                }
     319        }
     320
     321        public function test_update_page_menu_order() {
     322
     323                $page_id = $this->factory->post->create( array(
     324                        'post_type' => 'page',
     325                ) );
     326
     327                wp_set_current_user( $this->editor_id );
     328
     329                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/pages/%d', $page_id ) );
     330
     331                $request->set_body_params( array(
     332                        'menu_order' => 1,
     333                ) );
     334                $response = $this->server->dispatch( $request );
     335
     336                $new_data = $response->get_data();
     337                $this->assertEquals( 1, $new_data['menu_order'] );
     338        }
     339
     340        public function test_update_page_menu_order_to_zero() {
     341
     342                $page_id = $this->factory->post->create( array(
     343                        'post_type'  => 'page',
     344                        'menu_order' => 1,
     345                ) );
     346
     347                wp_set_current_user( $this->editor_id );
     348
     349                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/pages/%d', $page_id ) );
     350
     351                $request->set_body_params(array(
     352                        'menu_order' => 0,
     353                ));
     354                $response = $this->server->dispatch( $request );
     355
     356                $new_data = $response->get_data();
     357                $this->assertEquals( 0, $new_data['menu_order'] );
     358        }
     359
     360        public function test_get_item_schema() {
     361                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' );
     362                $response = $this->server->dispatch( $request );
     363                $data = $response->get_data();
     364                $properties = $data['schema']['properties'];
     365                $this->assertEquals( 21, count( $properties ) );
     366                $this->assertArrayHasKey( 'author', $properties );
     367                $this->assertArrayHasKey( 'comment_status', $properties );
     368                $this->assertArrayHasKey( 'content', $properties );
     369                $this->assertArrayHasKey( 'date', $properties );
     370                $this->assertArrayHasKey( 'date_gmt', $properties );
     371                $this->assertArrayHasKey( 'guid', $properties );
     372                $this->assertArrayHasKey( 'excerpt', $properties );
     373                $this->assertArrayHasKey( 'featured_media', $properties );
     374                $this->assertArrayHasKey( 'id', $properties );
     375                $this->assertArrayHasKey( 'link', $properties );
     376                $this->assertArrayHasKey( 'menu_order', $properties );
     377                $this->assertArrayHasKey( 'meta', $properties );
     378                $this->assertArrayHasKey( 'modified', $properties );
     379                $this->assertArrayHasKey( 'modified_gmt', $properties );
     380                $this->assertArrayHasKey( 'parent', $properties );
     381                $this->assertArrayHasKey( 'ping_status', $properties );
     382                $this->assertArrayHasKey( 'slug', $properties );
     383                $this->assertArrayHasKey( 'status', $properties );
     384                $this->assertArrayHasKey( 'template', $properties );
     385                $this->assertArrayHasKey( 'title', $properties );
     386                $this->assertArrayHasKey( 'type', $properties );
     387        }
     388
     389        public function tearDown() {
     390                parent::tearDown();
     391                remove_filter( 'theme_page_templates', array( $this, 'filter_theme_page_templates' ) );
     392        }
     393
     394        public function filter_theme_page_templates( $page_templates ) {
     395                return array(
     396                        'page-my-test-template.php' => 'My Test Template',
     397                );
     398                return $page_templates;
     399        }
     400
     401        protected function set_post_data( $args = array() ) {
     402                $args = parent::set_post_data( $args );
     403                $args['type'] = 'page';
     404                return $args;
     405        }
     406
     407}
  • tests/phpunit/tests/rest-api/rest-post-meta-fields.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Posts meta functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9 /**
     10  * @group restapi
     11  */
     12class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase {
     13        public function setUp() {
     14                parent::setUp();
     15
     16                register_meta( 'post', 'test_single', array(
     17                        'show_in_rest' => true,
     18                        'single' => true,
     19                ));
     20                register_meta( 'post', 'test_multi', array(
     21                        'show_in_rest' => true,
     22                        'single' => false,
     23                ));
     24                register_meta( 'post', 'test_bad_auth', array(
     25                        'show_in_rest' => true,
     26                        'single' => true,
     27                        'auth_callback' => '__return_false',
     28                ));
     29                register_meta( 'post', 'test_bad_auth_multi', array(
     30                        'show_in_rest' => true,
     31                        'single' => false,
     32                        'auth_callback' => '__return_false',
     33                ));
     34                register_meta( 'post', 'test_no_rest', array() );
     35                register_meta( 'post', 'test_rest_disabled', array(
     36                        'show_in_rest' => false,
     37                ));
     38                register_meta( 'post', 'test_custom_schema', array(
     39                        'single' => true,
     40                        'type' => 'integer',
     41                        'show_in_rest' => array(
     42                                'schema' => array(
     43                                        'type' => 'number',
     44                                ),
     45                        ),
     46                ));
     47                register_meta( 'post', 'test_invalid_type', array(
     48                        'single' => true,
     49                        'type' => false,
     50                        'show_in_rest' => true,
     51                ));
     52
     53                /** @var WP_REST_Server $wp_rest_server */
     54                global $wp_rest_server;
     55                $this->server = $wp_rest_server = new WP_Test_Spy_REST_Server;
     56                do_action( 'rest_api_init' );
     57
     58                $this->post_id = $this->factory->post->create();
     59        }
     60
     61        protected function grant_write_permission() {
     62                // Ensure we have write permission.
     63                $user = $this->factory->user->create( array(
     64                        'role' => 'editor',
     65                ));
     66                wp_set_current_user( $user );
     67        }
     68
     69        public function test_get_value() {
     70                add_post_meta( $this->post_id, 'test_single', 'testvalue' );
     71
     72                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     73                $response = $this->server->dispatch( $request );
     74
     75                $this->assertEquals( 200, $response->get_status() );
     76
     77                $data = $response->get_data();
     78                $this->assertArrayHasKey( 'meta', $data );
     79
     80                $meta = (array) $data['meta'];
     81                $this->assertArrayHasKey( 'test_single', $meta );
     82                $this->assertEquals( 'testvalue', $meta['test_single'] );
     83        }
     84
     85        /**
     86         * @depends test_get_value
     87         */
     88        public function test_get_multi_value() {
     89                add_post_meta( $this->post_id, 'test_multi', 'value1' );
     90                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     91
     92                $response = $this->server->dispatch( $request );
     93                $this->assertEquals( 200, $response->get_status() );
     94
     95                $data = $response->get_data();
     96                $meta = (array) $data['meta'];
     97                $this->assertArrayHasKey( 'test_multi', $meta );
     98                $this->assertInternalType( 'array', $meta['test_multi'] );
     99                $this->assertContains( 'value1', $meta['test_multi'] );
     100
     101                // Check after an update.
     102                add_post_meta( $this->post_id, 'test_multi', 'value2' );
     103
     104                $response = $this->server->dispatch( $request );
     105                $this->assertEquals( 200, $response->get_status() );
     106                $data = $response->get_data();
     107                $meta = (array) $data['meta'];
     108                $this->assertContains( 'value1', $meta['test_multi'] );
     109                $this->assertContains( 'value2', $meta['test_multi'] );
     110        }
     111
     112        /**
     113         * @depends test_get_value
     114         */
     115        public function test_get_unregistered() {
     116                add_post_meta( $this->post_id, 'test_unregistered', 'value1' );
     117                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     118
     119                $response = $this->server->dispatch( $request );
     120                $this->assertEquals( 200, $response->get_status() );
     121
     122                $data = $response->get_data();
     123                $meta = (array) $data['meta'];
     124                $this->assertArrayNotHasKey( 'test_unregistered', $meta );
     125        }
     126
     127        /**
     128         * @depends test_get_value
     129         */
     130        public function test_get_registered_no_api_access() {
     131                add_post_meta( $this->post_id, 'test_no_rest', 'for_the_wicked' );
     132                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     133
     134                $response = $this->server->dispatch( $request );
     135                $this->assertEquals( 200, $response->get_status() );
     136
     137                $data = $response->get_data();
     138                $meta = (array) $data['meta'];
     139                $this->assertArrayNotHasKey( 'test_no_rest', $meta );
     140        }
     141
     142        /**
     143         * @depends test_get_value
     144         */
     145        public function test_get_registered_api_disabled() {
     146                add_post_meta( $this->post_id, 'test_rest_disabled', 'sleepless_nights' );
     147                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     148
     149                $response = $this->server->dispatch( $request );
     150                $this->assertEquals( 200, $response->get_status() );
     151
     152                $data = $response->get_data();
     153                $meta = (array) $data['meta'];
     154                $this->assertArrayNotHasKey( 'test_rest_disabled', $meta );
     155        }
     156
     157        public function test_get_value_types() {
     158                register_meta( 'post', 'test_string', array(
     159                        'show_in_rest' => true,
     160                        'single' => true,
     161                        'type' => 'string',
     162                ));
     163                register_meta( 'post', 'test_number', array(
     164                        'show_in_rest' => true,
     165                        'single' => true,
     166                        'type' => 'number',
     167                ));
     168                register_meta( 'post', 'test_bool', array(
     169                        'show_in_rest' => true,
     170                        'single' => true,
     171                        'type' => 'boolean',
     172                ));
     173
     174                /** @var WP_REST_Server $wp_rest_server */
     175                global $wp_rest_server;
     176                $this->server = $wp_rest_server = new WP_Test_Spy_REST_Server;
     177                do_action( 'rest_api_init' );
     178
     179                add_post_meta( $this->post_id, 'test_string', 42 );
     180                add_post_meta( $this->post_id, 'test_number', '42' );
     181                add_post_meta( $this->post_id, 'test_bool', 1 );
     182
     183                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     184                $response = $this->server->dispatch( $request );
     185                $this->assertEquals( 200, $response->get_status() );
     186
     187                $data = $response->get_data();
     188                $meta = (array) $data['meta'];
     189
     190                $this->assertArrayHasKey( 'test_string', $meta );
     191                $this->assertInternalType( 'string', $meta['test_string'] );
     192                $this->assertSame( '42', $meta['test_string'] );
     193
     194                $this->assertArrayHasKey( 'test_number', $meta );
     195                $this->assertInternalType( 'float', $meta['test_number'] );
     196                $this->assertSame( 42.0, $meta['test_number'] );
     197
     198                $this->assertArrayHasKey( 'test_bool', $meta );
     199                $this->assertInternalType( 'boolean', $meta['test_bool'] );
     200                $this->assertSame( true, $meta['test_bool'] );
     201        }
     202
     203        /**
     204         * @depends test_get_value
     205         */
     206        public function test_set_value() {
     207                // Ensure no data exists currently.
     208                $values = get_post_meta( $this->post_id, 'test_single', false );
     209                $this->assertEmpty( $values );
     210
     211                $this->grant_write_permission();
     212
     213                $data = array(
     214                        'meta' => array(
     215                                'test_single' => 'test_value',
     216                        ),
     217                );
     218                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     219                $request->set_body_params( $data );
     220
     221                $response = $this->server->dispatch( $request );
     222                $this->assertEquals( 200, $response->get_status() );
     223
     224                $meta = get_post_meta( $this->post_id, 'test_single', false );
     225                $this->assertNotEmpty( $meta );
     226                $this->assertCount( 1, $meta );
     227                $this->assertEquals( 'test_value', $meta[0] );
     228
     229                $data = $response->get_data();
     230                $meta = (array) $data['meta'];
     231                $this->assertArrayHasKey( 'test_single', $meta );
     232                $this->assertEquals( 'test_value', $meta['test_single'] );
     233        }
     234
     235        /**
     236         * @depends test_get_value
     237         */
     238        public function test_set_duplicate_single_value() {
     239                // Start with an existing metakey and value.
     240                $values = update_post_meta( $this->post_id, 'test_single', 'test_value' );
     241                $this->assertEquals( 'test_value', get_post_meta( $this->post_id, 'test_single', true ) );
     242
     243                $this->grant_write_permission();
     244
     245                $data = array(
     246                        'meta' => array(
     247                                'test_single' => 'test_value',
     248                        ),
     249                );
     250                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     251                $request->set_body_params( $data );
     252
     253                $response = $this->server->dispatch( $request );
     254                $this->assertEquals( 200, $response->get_status() );
     255
     256                $meta = get_post_meta( $this->post_id, 'test_single', true );
     257                $this->assertNotEmpty( $meta );
     258                $this->assertEquals( 'test_value', $meta );
     259
     260                $data = $response->get_data();
     261                $meta = (array) $data['meta'];
     262                $this->assertArrayHasKey( 'test_single', $meta );
     263                $this->assertEquals( 'test_value', $meta['test_single'] );
     264        }
     265
     266        /**
     267         * @depends test_set_value
     268         */
     269        public function test_set_value_unauthenticated() {
     270                $data = array(
     271                        'meta' => array(
     272                                'test_single' => 'test_value',
     273                        ),
     274                );
     275
     276                wp_set_current_user( 0 );
     277
     278                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     279                $request->set_body_params( $data );
     280
     281                $response = $this->server->dispatch( $request );
     282                $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
     283
     284                // Check that the value wasn't actually updated.
     285                $this->assertEmpty( get_post_meta( $this->post_id, 'test_single', false ) );
     286        }
     287
     288        /**
     289         * @depends test_set_value
     290         */
     291        public function test_set_value_blocked() {
     292                $data = array(
     293                        'meta' => array(
     294                                'test_bad_auth' => 'test_value',
     295                        ),
     296                );
     297
     298                $this->grant_write_permission();
     299
     300                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     301                $request->set_body_params( $data );
     302
     303                $response = $this->server->dispatch( $request );
     304                $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
     305                $this->assertEmpty( get_post_meta( $this->post_id, 'test_bad_auth', false ) );
     306        }
     307
     308        /**
     309         * @depends test_set_value
     310         */
     311        public function test_set_value_db_error() {
     312                $data = array(
     313                        'meta' => array(
     314                                'test_single' => 'test_value',
     315                        ),
     316                );
     317
     318                $this->grant_write_permission();
     319
     320                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     321                $request->set_body_params( $data );
     322
     323                /**
     324                 * Disable showing error as the below is going to intentionally
     325                 * trigger a DB error.
     326                 */
     327                global $wpdb;
     328                $wpdb->suppress_errors = true;
     329                add_filter( 'query', array( $this, 'error_insert_query' ) );
     330
     331                $response = $this->server->dispatch( $request );
     332                remove_filter( 'query', array( $this, 'error_insert_query' ) );
     333                $wpdb->show_errors = true;
     334        }
     335
     336        public function test_set_value_multiple() {
     337                // Ensure no data exists currently.
     338                $values = get_post_meta( $this->post_id, 'test_multi', false );
     339                $this->assertEmpty( $values );
     340
     341                $this->grant_write_permission();
     342
     343                $data = array(
     344                        'meta' => array(
     345                                'test_multi' => array( 'val1' ),
     346                        ),
     347                );
     348                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     349                $request->set_body_params( $data );
     350
     351                $response = $this->server->dispatch( $request );
     352                $this->assertEquals( 200, $response->get_status() );
     353
     354                $meta = get_post_meta( $this->post_id, 'test_multi', false );
     355                $this->assertNotEmpty( $meta );
     356                $this->assertCount( 1, $meta );
     357                $this->assertEquals( 'val1', $meta[0] );
     358
     359                // Add another value.
     360                $data = array(
     361                        'meta' => array(
     362                                'test_multi' => array( 'val1', 'val2' ),
     363                        ),
     364                );
     365                $request->set_body_params( $data );
     366
     367                $response = $this->server->dispatch( $request );
     368                $this->assertEquals( 200, $response->get_status() );
     369
     370                $meta = get_post_meta( $this->post_id, 'test_multi', false );
     371                $this->assertNotEmpty( $meta );
     372                $this->assertCount( 2, $meta );
     373                $this->assertContains( 'val1', $meta );
     374                $this->assertContains( 'val2', $meta );
     375        }
     376
     377        /**
     378         * Test removing only one item with duplicate items.
     379         */
     380        public function test_set_value_remove_one() {
     381                add_post_meta( $this->post_id, 'test_multi', 'c' );
     382                add_post_meta( $this->post_id, 'test_multi', 'n' );
     383                add_post_meta( $this->post_id, 'test_multi', 'n' );
     384
     385                $this->grant_write_permission();
     386
     387                $data = array(
     388                        'meta' => array(
     389                                'test_multi' => array( 'c', 'n' ),
     390                        ),
     391                );
     392                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     393                $request->set_body_params( $data );
     394
     395                $response = $this->server->dispatch( $request );
     396                $this->assertEquals( 200, $response->get_status() );
     397
     398                $meta = get_post_meta( $this->post_id, 'test_multi', false );
     399                $this->assertNotEmpty( $meta );
     400                $this->assertCount( 2, $meta );
     401                $this->assertContains( 'c', $meta );
     402                $this->assertContains( 'n', $meta );
     403        }
     404
     405        /**
     406         * @depends test_set_value_multiple
     407         */
     408        public function test_set_value_multiple_unauthenticated() {
     409                // Ensure no data exists currently.
     410                $values = get_post_meta( $this->post_id, 'test_multi', false );
     411                $this->assertEmpty( $values );
     412
     413                wp_set_current_user( 0 );
     414
     415                $data = array(
     416                        'meta' => array(
     417                                'test_multi' => array( 'val1' ),
     418                        ),
     419                );
     420                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     421                $request->set_body_params( $data );
     422
     423                $response = $this->server->dispatch( $request );
     424                $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
     425
     426                $meta = get_post_meta( $this->post_id, 'test_multi', false );
     427                $this->assertEmpty( $meta );
     428        }
     429
     430        /**
     431         * @depends test_set_value_multiple
     432         */
     433        public function test_set_value_multiple_blocked() {
     434                $data = array(
     435                        'meta' => array(
     436                                'test_bad_auth_multi' => array( 'test_value' ),
     437                        ),
     438                );
     439
     440                $this->grant_write_permission();
     441
     442                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     443                $request->set_body_params( $data );
     444
     445                $response = $this->server->dispatch( $request );
     446                $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
     447                $this->assertEmpty( get_post_meta( $this->post_id, 'test_bad_auth_multi', false ) );
     448        }
     449
     450        public function test_add_multi_value_db_error() {
     451                // Ensure no data exists currently.
     452                $values = get_post_meta( $this->post_id, 'test_multi', false );
     453                $this->assertEmpty( $values );
     454
     455                $this->grant_write_permission();
     456
     457                $data = array(
     458                        'meta' => array(
     459                                'test_multi' => array( 'val1' ),
     460                        ),
     461                );
     462                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     463                $request->set_body_params( $data );
     464
     465                /**
     466                 * Disable showing error as the below is going to intentionally
     467                 * trigger a DB error.
     468                 */
     469                global $wpdb;
     470                $wpdb->suppress_errors = true;
     471                add_filter( 'query', array( $this, 'error_insert_query' ) );
     472
     473                $response = $this->server->dispatch( $request );
     474                remove_filter( 'query', array( $this, 'error_insert_query' ) );
     475                $wpdb->show_errors = true;
     476
     477                $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
     478        }
     479
     480        public function test_remove_multi_value_db_error() {
     481                add_post_meta( $this->post_id, 'test_multi', 'val1' );
     482                $values = get_post_meta( $this->post_id, 'test_multi', false );
     483                $this->assertEquals( array( 'val1' ), $values );
     484
     485                $this->grant_write_permission();
     486
     487                $data = array(
     488                        'meta' => array(
     489                                'test_multi' => array(),
     490                        ),
     491                );
     492                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     493                $request->set_body_params( $data );
     494
     495                /**
     496                 * Disable showing error as the below is going to intentionally
     497                 * trigger a DB error.
     498                 */
     499                global $wpdb;
     500                $wpdb->suppress_errors = true;
     501                add_filter( 'query', array( $this, 'error_delete_query' ) );
     502
     503                $response = $this->server->dispatch( $request );
     504                remove_filter( 'query', array( $this, 'error_delete_query' ) );
     505                $wpdb->show_errors = true;
     506
     507                $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
     508        }
     509
     510        public function test_delete_value() {
     511                add_post_meta( $this->post_id, 'test_single', 'val1' );
     512                $current = get_post_meta( $this->post_id, 'test_single', true );
     513                $this->assertEquals( 'val1', $current );
     514
     515                $this->grant_write_permission();
     516
     517                $data = array(
     518                        'meta' => array(
     519                                'test_single' => null,
     520                        ),
     521                );
     522                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     523                $request->set_body_params( $data );
     524
     525                $response = $this->server->dispatch( $request );
     526                $this->assertEquals( 200, $response->get_status() );
     527
     528                $meta = get_post_meta( $this->post_id, 'test_single', false );
     529                $this->assertEmpty( $meta );
     530        }
     531
     532        /**
     533         * @depends test_delete_value
     534         */
     535        public function test_delete_value_blocked() {
     536                add_post_meta( $this->post_id, 'test_bad_auth', 'val1' );
     537                $current = get_post_meta( $this->post_id, 'test_bad_auth', true );
     538                $this->assertEquals( 'val1', $current );
     539
     540                $this->grant_write_permission();
     541
     542                $data = array(
     543                        'meta' => array(
     544                                'test_bad_auth' => null,
     545                        ),
     546                );
     547                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     548                $request->set_body_params( $data );
     549
     550                $response = $this->server->dispatch( $request );
     551                $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
     552
     553                $meta = get_post_meta( $this->post_id, 'test_bad_auth', true );
     554                $this->assertEquals( 'val1', $meta );
     555        }
     556
     557        /**
     558         * @depends test_delete_value
     559         */
     560        public function test_delete_value_db_error() {
     561                add_post_meta( $this->post_id, 'test_single', 'val1' );
     562                $current = get_post_meta( $this->post_id, 'test_single', true );
     563                $this->assertEquals( 'val1', $current );
     564
     565                $this->grant_write_permission();
     566
     567                $data = array(
     568                        'meta' => array(
     569                                'test_single' => null,
     570                        ),
     571                );
     572                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     573                $request->set_body_params( $data );
     574                /**
     575                 * Disable showing error as the below is going to intentionally
     576                 * trigger a DB error.
     577                 */
     578                global $wpdb;
     579                $wpdb->suppress_errors = true;
     580                add_filter( 'query', array( $this, 'error_delete_query' ) );
     581
     582                $response = $this->server->dispatch( $request );
     583                remove_filter( 'query', array( $this, 'error_delete_query' ) );
     584                $wpdb->show_errors = true;
     585
     586                $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
     587        }
     588
     589        public function test_get_schema() {
     590                $request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     591                $response = $this->server->dispatch( $request );
     592
     593                $data = $response->get_data();
     594                $schema = $data['schema'];
     595
     596                $this->assertArrayHasKey( 'meta', $schema['properties'] );
     597                $meta_schema = $schema['properties']['meta']['properties'];
     598
     599                $this->assertArrayHasKey( 'test_single', $meta_schema );
     600                $this->assertEquals( 'string', $meta_schema['test_single']['type'] );
     601
     602                $this->assertArrayHasKey( 'test_multi', $meta_schema );
     603                $this->assertEquals( 'array', $meta_schema['test_multi']['type'] );
     604                $this->assertArrayHasKey( 'items', $meta_schema['test_multi'] );
     605                $this->assertEquals( 'string', $meta_schema['test_multi']['items']['type'] );
     606
     607                $this->assertArrayHasKey( 'test_custom_schema', $meta_schema );
     608                $this->assertEquals( 'number', $meta_schema['test_custom_schema']['type'] );
     609
     610                $this->assertArrayNotHasKey( 'test_no_rest', $meta_schema );
     611                $this->assertArrayNotHasKey( 'test_rest_disabled', $meta_schema );
     612                $this->assertArrayNotHasKey( 'test_invalid_type', $meta_schema );
     613        }
     614
     615        /**
     616         * Internal function used to disable an insert query which
     617         * will trigger a wpdb error for testing purposes.
     618         */
     619        public function error_insert_query( $query ) {
     620                if ( strpos( $query, 'INSERT' ) === 0 ) {
     621                        $query = '],';
     622                }
     623                return $query;
     624        }
     625
     626        /**
     627         * Internal function used to disable an insert query which
     628         * will trigger a wpdb error for testing purposes.
     629         */
     630        public function error_delete_query( $query ) {
     631                if ( strpos( $query, 'DELETE' ) === 0 ) {
     632                        $query = '],';
     633                }
     634                return $query;
     635        }
     636}
  • tests/phpunit/tests/rest-api/rest-post-statuses-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Posts_Statuses_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9 /**
     10  * @group restapi
     11  */
     12class WP_Test_REST_Post_Statuses_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        public function test_register_routes() {
     15                $routes = $this->server->get_routes();
     16                $this->assertArrayHasKey( '/wp/v2/statuses', $routes );
     17                $this->assertArrayHasKey( '/wp/v2/statuses/(?P<status>[\w-]+)', $routes );
     18        }
     19
     20        public function test_context_param() {
     21                // Collection
     22                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses' );
     23                $response = $this->server->dispatch( $request );
     24                $data = $response->get_data();
     25                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     26                $this->assertEqualSets( array( 'embed', 'view', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     27                // Single
     28                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses/publish' );
     29                $response = $this->server->dispatch( $request );
     30                $data = $response->get_data();
     31                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     32                $this->assertEqualSets( array( 'embed', 'view', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     33        }
     34
     35        public function test_get_items() {
     36                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' );
     37                $response = $this->server->dispatch( $request );
     38
     39                $data = $response->get_data();
     40                $statuses = get_post_stati( array( 'public' => true ), 'objects' );
     41                $this->assertEquals( 1, count( $data ) );
     42                $this->assertEquals( 'publish', $data['publish']['slug'] );
     43        }
     44
     45        public function test_get_items_logged_in() {
     46                $user_id = $this->factory->user->create( array( 'role' => 'author' ) );
     47                wp_set_current_user( $user_id );
     48
     49                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' );
     50                $response = $this->server->dispatch( $request );
     51
     52                $data = $response->get_data();
     53                $this->assertEquals( 6, count( $data ) );
     54                $this->assertEqualSets( array(
     55                        'publish',
     56                        'private',
     57                        'pending',
     58                        'draft',
     59                        'trash',
     60                        'future',
     61                ), array_keys( $data ) );
     62        }
     63
     64        public function test_get_items_unauthorized_context() {
     65                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' );
     66                $request->set_param( 'context', 'edit' );
     67                $response = $this->server->dispatch( $request );
     68                $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
     69        }
     70
     71        public function test_get_item() {
     72                $user_id = $this->factory->user->create( array( 'role' => 'author' ) );
     73                wp_set_current_user( $user_id );
     74                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/publish' );
     75                $request->set_param( 'context', 'edit' );
     76                $response = $this->server->dispatch( $request );
     77                $this->check_post_status_object_response( $response );
     78        }
     79
     80        public function test_get_item_invalid_status() {
     81                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/invalid' );
     82                $response = $this->server->dispatch( $request );
     83                $this->assertErrorResponse( 'rest_status_invalid', $response, 404 );
     84        }
     85
     86        public function test_get_item_invalid_access() {
     87                wp_set_current_user( 0 );
     88                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/draft' );
     89                $response = $this->server->dispatch( $request );
     90                $this->assertErrorResponse( 'rest_cannot_read_status', $response, 401 );
     91        }
     92
     93        public function test_get_item_invalid_internal() {
     94                $user_id = $this->factory->user->create();
     95                wp_set_current_user( $user_id );
     96
     97                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/inherit' );
     98                $response = $this->server->dispatch( $request );
     99                $this->assertErrorResponse( 'rest_cannot_read_status', $response, 403 );
     100        }
     101
     102        public function test_create_item() {
     103                /** Post statuses can't be created **/
     104        }
     105
     106        public function test_update_item() {
     107                /** Post statuses can't be updated **/
     108        }
     109
     110        public function test_delete_item() {
     111                /** Post statuses can't be deleted **/
     112        }
     113
     114        public function test_prepare_item() {
     115                $obj = get_post_status_object( 'publish' );
     116                $endpoint = new WP_REST_Post_Statuses_Controller;
     117                $request = new WP_REST_Request;
     118                $request->set_param( 'context', 'edit' );
     119                $data = $endpoint->prepare_item_for_response( $obj, $request );
     120                $this->check_post_status_obj( $obj, $data->get_data(), $data->get_links() );
     121        }
     122
     123        public function test_get_item_schema() {
     124                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses' );
     125                $response = $this->server->dispatch( $request );
     126                $data = $response->get_data();
     127                $properties = $data['schema']['properties'];
     128                $this->assertEquals( 7, count( $properties ) );
     129                $this->assertArrayHasKey( 'name', $properties );
     130                $this->assertArrayHasKey( 'private', $properties );
     131                $this->assertArrayHasKey( 'protected', $properties );
     132                $this->assertArrayHasKey( 'public', $properties );
     133                $this->assertArrayHasKey( 'queryable', $properties );
     134                $this->assertArrayHasKey( 'show_in_list', $properties );
     135                $this->assertArrayHasKey( 'slug', $properties );
     136        }
     137
     138        public function test_get_additional_field_registration() {
     139
     140                $schema = array(
     141                        'type'        => 'integer',
     142                        'description' => 'Some integer of mine',
     143                        'enum'        => array( 1, 2, 3, 4 ),
     144                        'context'     => array( 'view', 'edit' ),
     145                );
     146
     147                register_rest_field( 'status', 'my_custom_int', array(
     148                        'schema'          => $schema,
     149                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     150                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     151                ) );
     152
     153                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/statuses' );
     154
     155                $response = $this->server->dispatch( $request );
     156                $data = $response->get_data();
     157
     158                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     159                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     160
     161                $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/publish' );
     162
     163                $response = $this->server->dispatch( $request );
     164                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     165
     166                global $wp_rest_additional_fields;
     167                $wp_rest_additional_fields = array();
     168        }
     169
     170        public function additional_field_get_callback( $object ) {
     171                return 123;
     172        }
     173
     174        protected function check_post_status_obj( $status_obj, $data, $links ) {
     175                $this->assertEquals( $status_obj->label, $data['name'] );
     176                $this->assertEquals( $status_obj->private, $data['private'] );
     177                $this->assertEquals( $status_obj->protected, $data['protected'] );
     178                $this->assertEquals( $status_obj->public, $data['public'] );
     179                $this->assertEquals( $status_obj->publicly_queryable, $data['queryable'] );
     180                $this->assertEquals( $status_obj->show_in_admin_all_list, $data['show_in_list'] );
     181                $this->assertEquals( $status_obj->name, $data['slug'] );
     182                $this->assertEqualSets( array(
     183                        'archives',
     184                ), array_keys( $links ) );
     185        }
     186
     187        protected function check_post_status_object_response( $response ) {
     188                $this->assertEquals( 200, $response->get_status() );
     189                $data = $response->get_data();
     190                $obj = get_post_status_object( 'publish' );
     191                $this->check_post_status_obj( $obj, $data, $response->get_links() );
     192        }
     193
     194}
  • tests/phpunit/tests/rest-api/rest-post-types-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Posts_Types_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9 /**
     10  * @group restapi
     11  */
     12class WP_Test_REST_Post_Types_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        public function test_register_routes() {
     15                $routes = $this->server->get_routes();
     16                $this->assertArrayHasKey( '/wp/v2/types', $routes );
     17                $this->assertArrayHasKey( '/wp/v2/types/(?P<type>[\w-]+)', $routes );
     18        }
     19
     20        public function test_context_param() {
     21                // Collection
     22                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' );
     23                $response = $this->server->dispatch( $request );
     24                $data = $response->get_data();
     25                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     26                $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
     27                // Single
     28                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types/post' );
     29                $response = $this->server->dispatch( $request );
     30                $data = $response->get_data();
     31                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     32                $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
     33        }
     34
     35        public function test_get_items() {
     36                $request = new WP_REST_Request( 'GET', '/wp/v2/types' );
     37                $response = $this->server->dispatch( $request );
     38
     39                $data = $response->get_data();
     40                $post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
     41                $this->assertEquals( count( $post_types ), count( $data ) );
     42                $this->assertEquals( $post_types['post']->name, $data['post']['slug'] );
     43                $this->check_post_type_obj( 'view', $post_types['post'], $data['post'], $data['post']['_links'] );
     44                $this->assertEquals( $post_types['page']->name, $data['page']['slug'] );
     45                $this->check_post_type_obj( 'view', $post_types['page'], $data['page'], $data['page']['_links'] );
     46                $this->assertFalse( isset( $data['revision'] ) );
     47        }
     48
     49        public function test_get_items_invalid_permission_for_context() {
     50                wp_set_current_user( 0 );
     51                $request = new WP_REST_Request( 'GET', '/wp/v2/types' );
     52                $request->set_param( 'context', 'edit' );
     53                $response = $this->server->dispatch( $request );
     54                $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
     55        }
     56
     57        public function test_get_item() {
     58                $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
     59                $response = $this->server->dispatch( $request );
     60                $this->check_post_type_object_response( 'view', $response );
     61        }
     62
     63        public function test_get_item_invalid_type() {
     64                $request = new WP_REST_Request( 'GET', '/wp/v2/types/invalid' );
     65                $response = $this->server->dispatch( $request );
     66                $this->assertErrorResponse( 'rest_type_invalid', $response, 404 );
     67        }
     68
     69        public function test_get_item_edit_context() {
     70                $editor_id = $this->factory->user->create( array( 'role' => 'editor' ) );
     71                wp_set_current_user( $editor_id );
     72                $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
     73                $request->set_param( 'context', 'edit' );
     74                $response = $this->server->dispatch( $request );
     75                $this->check_post_type_object_response( 'edit', $response );
     76        }
     77
     78        public function test_get_item_invalid_permission_for_context() {
     79                wp_set_current_user( 0 );
     80                $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
     81                $request->set_param( 'context', 'edit' );
     82                $response = $this->server->dispatch( $request );
     83                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     84        }
     85
     86        public function test_create_item() {
     87                /** Post types can't be created **/
     88        }
     89
     90        public function test_update_item() {
     91                /** Post types can't be updated **/
     92        }
     93
     94        public function test_delete_item() {
     95                /** Post types can't be deleted **/
     96        }
     97
     98        public function test_prepare_item() {
     99                $obj = get_post_type_object( 'post' );
     100                $endpoint = new WP_REST_Post_Types_Controller;
     101                $request = new WP_REST_Request;
     102                $request->set_param( 'context', 'edit' );
     103                $response = $endpoint->prepare_item_for_response( $obj, $request );
     104                $this->check_post_type_obj( 'edit', $obj, $response->get_data(), $response->get_links() );
     105        }
     106
     107        public function test_get_item_schema() {
     108                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' );
     109                $response = $this->server->dispatch( $request );
     110                $data = $response->get_data();
     111                $properties = $data['schema']['properties'];
     112                $this->assertEquals( 6, count( $properties ) );
     113                $this->assertArrayHasKey( 'capabilities', $properties );
     114                $this->assertArrayHasKey( 'description', $properties );
     115                $this->assertArrayHasKey( 'hierarchical', $properties );
     116                $this->assertArrayHasKey( 'labels', $properties );
     117                $this->assertArrayHasKey( 'name', $properties );
     118                $this->assertArrayHasKey( 'slug', $properties );
     119        }
     120
     121        public function test_get_additional_field_registration() {
     122
     123                $schema = array(
     124                        'type'        => 'integer',
     125                        'description' => 'Some integer of mine',
     126                        'enum'        => array( 1, 2, 3, 4 ),
     127                        'context'     => array( 'view', 'edit' ),
     128                );
     129
     130                register_rest_field( 'type', 'my_custom_int', array(
     131                        'schema'          => $schema,
     132                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     133                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     134                ) );
     135
     136                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types/schema' );
     137
     138                $response = $this->server->dispatch( $request );
     139                $data = $response->get_data();
     140
     141                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     142                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     143
     144                $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' );
     145
     146                $response = $this->server->dispatch( $request );
     147                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     148
     149                global $wp_rest_additional_fields;
     150                $wp_rest_additional_fields = array();
     151        }
     152
     153        public function additional_field_get_callback( $object ) {
     154                return 123;
     155        }
     156
     157        protected function check_post_type_obj( $context, $post_type_obj, $data, $links ) {
     158                $this->assertEquals( $post_type_obj->label, $data['name'] );
     159                $this->assertEquals( $post_type_obj->name, $data['slug'] );
     160                $this->assertEquals( $post_type_obj->description, $data['description'] );
     161                $this->assertEquals( $post_type_obj->hierarchical, $data['hierarchical'] );
     162
     163                $links = test_rest_expand_compact_links( $links );
     164                $this->assertEquals( rest_url( 'wp/v2/types' ), $links['collection'][0]['href'] );
     165                $this->assertArrayHasKey( 'https://api.w.org/items', $links );
     166                if ( 'edit' === $context ) {
     167                        $this->assertEquals( $post_type_obj->cap, $data['capabilities'] );
     168                        $this->assertEquals( $post_type_obj->labels, $data['labels'] );
     169                } else {
     170                        $this->assertFalse( isset( $data['capabilities'] ) );
     171                        $this->assertFalse( isset( $data['labels'] ) );
     172                }
     173        }
     174
     175        protected function check_post_type_object_response( $context, $response ) {
     176                $this->assertEquals( 200, $response->get_status() );
     177                $data = $response->get_data();
     178                $obj = get_post_type_object( 'post' );
     179                $this->check_post_type_obj( $context, $obj, $data, $response->get_links() );
     180        }
     181
     182}
  • tests/phpunit/tests/rest-api/rest-posts-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Posts_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Testcase {
     13
     14        public function setUp() {
     15                parent::setUp();
     16
     17                $this->post_id = $this->factory->post->create();
     18
     19                $this->editor_id = $this->factory->user->create( array(
     20                        'role' => 'editor',
     21                ) );
     22                $this->author_id = $this->factory->user->create( array(
     23                        'role' => 'author',
     24                ) );
     25                $this->contributor_id = $this->factory->user->create( array(
     26                        'role' => 'contributor',
     27                ) );
     28
     29                register_post_type( 'youseeme', array( 'supports' => array(), 'show_in_rest' => true ) );
     30        }
     31
     32        public function test_register_routes() {
     33                $routes = $this->server->get_routes();
     34
     35                $this->assertArrayHasKey( '/wp/v2/posts', $routes );
     36                $this->assertCount( 2, $routes['/wp/v2/posts'] );
     37                $this->assertArrayHasKey( '/wp/v2/posts/(?P<id>[\d]+)', $routes );
     38                $this->assertCount( 3, $routes['/wp/v2/posts/(?P<id>[\d]+)'] );
     39        }
     40
     41        public function test_context_param() {
     42                // Collection
     43                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
     44                $response = $this->server->dispatch( $request );
     45                $data = $response->get_data();
     46                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     47                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     48                // Single
     49                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id );
     50                $response = $this->server->dispatch( $request );
     51                $data = $response->get_data();
     52                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     53                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     54        }
     55
     56        public function test_registered_query_params() {
     57                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
     58                $response = $this->server->dispatch( $request );
     59                $data = $response->get_data();
     60                $keys = array_keys( $data['endpoints'][0]['args'] );
     61                sort( $keys );
     62                $this->assertEquals( array(
     63                        'after',
     64                        'author',
     65                        'author_exclude',
     66                        'before',
     67                        'categories',
     68                        'context',
     69                        'exclude',
     70                        'filter',
     71                        'include',
     72                        'offset',
     73                        'order',
     74                        'orderby',
     75                        'page',
     76                        'per_page',
     77                        'search',
     78                        'slug',
     79                        'status',
     80                        'sticky',
     81                        'tags',
     82                        ), $keys );
     83        }
     84
     85        public function test_get_items() {
     86                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     87                $response = $this->server->dispatch( $request );
     88
     89                $this->check_get_posts_response( $response );
     90        }
     91
     92        /**
     93         * A valid query that returns 0 results should return an empty JSON list.
     94         *
     95         * @issue 862
     96         */
     97        public function test_get_items_empty_query() {
     98                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     99                $request->set_query_params( array(
     100                        'filter' => array( 'year' => 2008 ),
     101                ) );
     102                $response = $this->server->dispatch( $request );
     103                $this->assertEquals( array(), $response->get_data() );
     104                $this->assertEquals( 200, $response->get_status() );
     105        }
     106
     107        public function test_get_items_author_query() {
     108                $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
     109                $this->factory->post->create( array( 'post_author' => $this->author_id ) );
     110                // All 3 posts
     111                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     112                $response = $this->server->dispatch( $request );
     113                $this->assertEquals( 200, $response->get_status() );
     114                $this->assertEquals( 3, count( $response->get_data() ) );
     115                // 2 of 3 posts
     116                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     117                $request->set_param( 'author', array( $this->editor_id, $this->author_id ) );
     118                $response = $this->server->dispatch( $request );
     119                $this->assertEquals( 200, $response->get_status() );
     120                $data = $response->get_data();
     121                $this->assertEquals( 2, count( $data ) );
     122                $this->assertEqualSets( array( $this->editor_id, $this->author_id ), wp_list_pluck( $data, 'author' ) );
     123                // 1 of 3 posts
     124                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     125                $request->set_param( 'author', $this->editor_id );
     126                $response = $this->server->dispatch( $request );
     127                $this->assertEquals( 200, $response->get_status() );
     128                $data = $response->get_data();
     129                $this->assertEquals( 1, count( $data ) );
     130                $this->assertEquals( $this->editor_id, $data[0]['author'] );
     131        }
     132
     133        public function test_get_items_author_exclude_query() {
     134                $this->factory->post->create( array( 'post_author' => $this->editor_id ) );
     135                $this->factory->post->create( array( 'post_author' => $this->author_id ) );
     136                // All 3 posts
     137                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     138                $response = $this->server->dispatch( $request );
     139                $this->assertEquals( 200, $response->get_status() );
     140                $this->assertEquals( 3, count( $response->get_data() ) );
     141                // 1 of 3 posts
     142                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     143                $request->set_param( 'author_exclude', array( $this->editor_id, $this->author_id ) );
     144                $response = $this->server->dispatch( $request );
     145                $this->assertEquals( 200, $response->get_status() );
     146                $data = $response->get_data();
     147                $this->assertEquals( 1, count( $data ) );
     148                $this->assertNotEquals( $this->editor_id, $data[0]['author'] );
     149                $this->assertNotEquals( $this->author_id, $data[0]['author'] );
     150                // 2 of 3 posts
     151                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     152                $request->set_param( 'author_exclude', $this->editor_id );
     153                $response = $this->server->dispatch( $request );
     154                $this->assertEquals( 200, $response->get_status() );
     155                $data = $response->get_data();
     156                $this->assertEquals( 2, count( $data ) );
     157                $this->assertNotEquals( $this->editor_id, $data[0]['author'] );
     158                $this->assertNotEquals( $this->editor_id, $data[1]['author'] );
     159        }
     160
     161        public function test_get_items_include_query() {
     162                $id1 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     163                $this->factory->post->create( array( 'post_status' => 'publish' ) );
     164                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     165                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     166                // Orderby=>desc
     167                $request->set_param( 'include', array( $id1, $id3 ) );
     168                $response = $this->server->dispatch( $request );
     169                $data = $response->get_data();
     170                $this->assertEquals( 2, count( $data ) );
     171                $this->assertEquals( $id3, $data[0]['id'] );
     172                // Orderby=>include
     173                $request->set_param( 'orderby', 'include' );
     174                $response = $this->server->dispatch( $request );
     175                $data = $response->get_data();
     176                $this->assertEquals( 2, count( $data ) );
     177                $this->assertEquals( $id1, $data[0]['id'] );
     178        }
     179
     180        public function test_get_items_exclude_query() {
     181                $id1 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     182                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     183                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     184                $response = $this->server->dispatch( $request );
     185                $data = $response->get_data();
     186                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     187                $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     188                $request->set_param( 'exclude', array( $id2 ) );
     189                $response = $this->server->dispatch( $request );
     190                $data = $response->get_data();
     191                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     192                $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     193        }
     194
     195        public function test_get_items_search_query() {
     196                for ( $i = 0;  $i < 5;  $i++ ) {
     197                        $this->factory->post->create( array( 'post_status' => 'publish' ) );
     198                }
     199                $this->factory->post->create( array( 'post_title' => 'Search Result', 'post_status' => 'publish' ) );
     200                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     201                $response = $this->server->dispatch( $request );
     202                $this->assertEquals( 7, count( $response->get_data() ) );
     203                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     204                $request->set_param( 'search', 'Search Result' );
     205                $response = $this->server->dispatch( $request );
     206                $data = $response->get_data();
     207                $this->assertEquals( 1, count( $data ) );
     208                $this->assertEquals( 'Search Result', $data[0]['title']['rendered'] );
     209        }
     210
     211        public function test_get_items_slug_query() {
     212                $this->factory->post->create( array( 'post_title' => 'Apple', 'post_status' => 'publish' ) );
     213                $this->factory->post->create( array( 'post_title' => 'Banana', 'post_status' => 'publish' ) );
     214                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     215                $request->set_param( 'slug', 'apple' );
     216                $response = $this->server->dispatch( $request );
     217                $this->assertEquals( 200, $response->get_status() );
     218                $data = $response->get_data();
     219                $this->assertEquals( 1, count( $data ) );
     220                $this->assertEquals( 'Apple', $data[0]['title']['rendered'] );
     221        }
     222
     223        public function test_get_items_status_query() {
     224                wp_set_current_user( 0 );
     225                $this->factory->post->create( array( 'post_status' => 'draft' ) );
     226                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     227                $request->set_param( 'status', 'publish' );
     228                $response = $this->server->dispatch( $request );
     229                $this->assertEquals( 200, $response->get_status() );
     230                $this->assertEquals( 1, count( $response->get_data() ) );
     231                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     232                $request->set_param( 'status', 'draft' );
     233                $response = $this->server->dispatch( $request );
     234                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     235                wp_set_current_user( $this->editor_id );
     236                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     237                $request->set_param( 'status', 'draft' );
     238                $response = $this->server->dispatch( $request );
     239                $this->assertEquals( 200, $response->get_status() );
     240                $this->assertEquals( 1, count( $response->get_data() ) );
     241        }
     242
     243        public function test_get_items_status_without_permissions() {
     244                $draft_id = $this->factory->post->create( array(
     245                        'post_status' => 'draft',
     246                ) );
     247                wp_set_current_user( 0 );
     248
     249                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     250                $response = $this->server->dispatch( $request );
     251
     252                $this->assertEquals( 200, $response->get_status() );
     253
     254                $all_data = $response->get_data();
     255                foreach ( $all_data as $post ) {
     256                        $this->assertNotEquals( $draft_id, $post['id'] );
     257                }
     258        }
     259
     260        public function test_get_items_order_and_orderby() {
     261                $this->factory->post->create( array( 'post_title' => 'Apple Pie', 'post_status' => 'publish' ) );
     262                $this->factory->post->create( array( 'post_title' => 'Apple Sauce', 'post_status' => 'publish' ) );
     263                $this->factory->post->create( array( 'post_title' => 'Apple Cobbler', 'post_status' => 'publish' ) );
     264                $this->factory->post->create( array( 'post_title' => 'Apple Coffee Cake', 'post_status' => 'publish' ) );
     265                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     266                $request->set_param( 'search', 'Apple' );
     267                // order defaults to 'desc'
     268                $request->set_param( 'orderby', 'title' );
     269                $response = $this->server->dispatch( $request );
     270                $data = $response->get_data();
     271                $this->assertEquals( 'Apple Sauce', $data[0]['title']['rendered'] );
     272                // order=>asc
     273                $request->set_param( 'order', 'asc' );
     274                $response = $this->server->dispatch( $request );
     275                $data = $response->get_data();
     276                $this->assertEquals( 'Apple Cobbler', $data[0]['title']['rendered'] );
     277        }
     278
     279        public function test_get_items_with_orderby_relevance() {
     280                $this->factory->post->create( array( 'post_title' => 'Title is more relevant', 'post_content' => 'Content is', 'post_status' => 'publish' ) );
     281                $this->factory->post->create( array( 'post_title' => 'Title is', 'post_content' => 'Content is less relevant', 'post_status' => 'publish' ) );
     282
     283                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     284                $request->set_param( 'orderby', 'relevance' );
     285                $response = $this->server->dispatch( $request );
     286                $this->assertErrorResponse( 'rest_no_search_term_defined', $response, 400 );
     287        }
     288
     289        public function test_get_items_ignore_sticky_posts_by_default() {
     290                $this->markTestSkipped( 'Broken, see https://github.com/WP-API/WP-API/issues/2210' );
     291                $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' ) );
     292                $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' ) );
     293                $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' ) );
     294                stick_post( $post_id2 );
     295
     296                // No stickies by default
     297                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     298                $response = $this->server->dispatch( $request );
     299                $data = $response->get_data();
     300                $this->assertEquals( array( $this->post_id, $post_id3, $post_id2, $post_id1 ), wp_list_pluck( $data, 'id' ) );
     301
     302                // Permit stickies
     303                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     304                $request->set_param( 'filter', array( 'ignore_sticky_posts' => false ) );
     305                $response = $this->server->dispatch( $request );
     306                $data = $response->get_data();
     307                $this->assertEquals( array( $post_id2, $this->post_id, $post_id3, $post_id1 ), wp_list_pluck( $data, 'id' ) );
     308        }
     309
     310        public function test_get_items_offset_query() {
     311                $id1 = $this->post_id;
     312                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     313                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     314                $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     315                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     316                $request->set_param( 'offset', 1 );
     317                $response = $this->server->dispatch( $request );
     318                $this->assertCount( 3, $response->get_data() );
     319                // 'offset' works with 'per_page'
     320                $request->set_param( 'per_page', 2 );
     321                $response = $this->server->dispatch( $request );
     322                $this->assertCount( 2, $response->get_data() );
     323                // 'offset' takes priority over 'page'
     324                $request->set_param( 'page', 3 );
     325                $response = $this->server->dispatch( $request );
     326                $this->assertCount( 2, $response->get_data() );
     327        }
     328
     329        public function test_get_items_tags_query() {
     330                $id1 = $this->post_id;
     331                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     332                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     333                $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     334                $tag = wp_insert_term( 'My Tag', 'post_tag' );
     335
     336                wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
     337                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     338                $request->set_param( 'tags', array( $tag['term_id'] ) );
     339
     340                $response = $this->server->dispatch( $request );
     341                $data = $response->get_data();
     342                $this->assertCount( 1, $data );
     343                $this->assertEquals( $id1, $data[0]['id'] );
     344        }
     345
     346        public function test_get_items_tags_exclude_query() {
     347                $id1 = $this->post_id;
     348                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     349                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     350                $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     351                $tag = wp_insert_term( 'My Tag', 'post_tag' );
     352
     353                wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
     354                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     355                $request->set_param( 'tags_exclude', array( $tag['term_id'] ) );
     356
     357                $response = $this->server->dispatch( $request );
     358                $data = $response->get_data();
     359                $this->assertCount( 3, $data );
     360                $this->assertEquals( $id4, $data[0]['id'] );
     361                $this->assertEquals( $id3, $data[1]['id'] );
     362                $this->assertEquals( $id2, $data[2]['id'] );
     363        }
     364
     365        public function test_get_items_tags_and_categories_query() {
     366                $id1 = $this->post_id;
     367                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     368                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     369                $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     370                $tag = wp_insert_term( 'My Tag', 'post_tag' );
     371                $category = wp_insert_term( 'My Category', 'category' );
     372
     373                wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
     374                wp_set_object_terms( $id2, array( $tag['term_id'] ), 'post_tag' );
     375                wp_set_object_terms( $id1, array( $category['term_id'] ), 'category' );
     376
     377                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     378                $request->set_param( 'tags', array( $tag['term_id'] ) );
     379                $request->set_param( 'categories', array( $category['term_id'] ) );
     380
     381                $response = $this->server->dispatch( $request );
     382                $this->assertCount( 1, $response->get_data() );
     383        }
     384
     385        public function test_get_items_tags_and_categories_exclude_query() {
     386                $id1 = $this->post_id;
     387                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     388                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     389                $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     390                $tag = wp_insert_term( 'My Tag', 'post_tag' );
     391                $category = wp_insert_term( 'My Category', 'category' );
     392
     393                wp_set_object_terms( $id1, array( $tag['term_id'] ), 'post_tag' );
     394                wp_set_object_terms( $id2, array( $tag['term_id'] ), 'post_tag' );
     395                wp_set_object_terms( $id1, array( $category['term_id'] ), 'category' );
     396
     397                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     398                $request->set_param( 'tags', array( $tag['term_id'] ) );
     399                $request->set_param( 'categories_exclude', array( $category['term_id'] ) );
     400
     401                $response = $this->server->dispatch( $request );
     402                $data = $response->get_data();
     403                $this->assertCount( 1, $data );
     404                $this->assertEquals( $id2, $data[0]['id'] );
     405        }
     406
     407        public function test_get_items_sticky_query() {
     408                $id1 = $this->post_id;
     409                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     410
     411                update_option( 'sticky_posts', array( $id2 ) );
     412
     413                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     414                $request->set_param( 'sticky', true );
     415
     416                $response = $this->server->dispatch( $request );
     417                $this->assertCount( 1, $response->get_data() );
     418
     419                $posts = $response->get_data();
     420                $post = $posts[0];
     421                $this->assertEquals( $id2, $post['id'] );
     422        }
     423
     424        public function test_get_items_sticky_with_post__in_query() {
     425                $id1 = $this->post_id;
     426                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     427                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     428
     429                update_option( 'sticky_posts', array( $id2 ) );
     430
     431                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     432                $request->set_param( 'sticky', true );
     433                $request->set_param( 'include', array( $id1 ) );
     434
     435                $response = $this->server->dispatch( $request );
     436                $this->assertCount( 0, $response->get_data() );
     437
     438                update_option( 'sticky_posts', array( $id1, $id2 ) );
     439
     440                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     441                $request->set_param( 'sticky', true );
     442                $request->set_param( 'include', array( $id1 ) );
     443
     444                $response = $this->server->dispatch( $request );
     445
     446                $this->assertCount( 1, $response->get_data() );
     447
     448                $posts = $response->get_data();
     449                $post = $posts[0];
     450                $this->assertEquals( $id1, $post['id'] );
     451        }
     452
     453        public function test_get_items_not_sticky_query() {
     454                $id1 = $this->post_id;
     455                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     456
     457                update_option( 'sticky_posts', array( $id2 ) );
     458
     459                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     460                $request->set_param( 'sticky', false );
     461
     462                $response = $this->server->dispatch( $request );
     463                $this->assertCount( 1, $response->get_data() );
     464
     465                $posts = $response->get_data();
     466                $post = $posts[0];
     467                $this->assertEquals( $id1, $post['id'] );
     468        }
     469
     470        public function test_get_items_sticky_with_post__not_in_query() {
     471                $id1 = $this->post_id;
     472                $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     473                $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) );
     474
     475                update_option( 'sticky_posts', array( $id2 ) );
     476
     477                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     478                $request->set_param( 'sticky', false );
     479                $request->set_param( 'exclude', array( $id3 ) );
     480
     481                $response = $this->server->dispatch( $request );
     482                $this->assertCount( 1, $response->get_data() );
     483
     484                $posts = $response->get_data();
     485                $post = $posts[0];
     486                $this->assertEquals( $id1, $post['id'] );
     487        }
     488
     489        /**
     490         * @group test
     491         */
     492        public function test_get_items_pagination_headers() {
     493                // Start of the index
     494                for ( $i = 0; $i < 49; $i++ ) {
     495                        $this->factory->post->create( array(
     496                                'post_title'   => "Post {$i}",
     497                                ) );
     498                }
     499                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     500                $response = $this->server->dispatch( $request );
     501                $headers = $response->get_headers();
     502                $this->assertEquals( 50, $headers['X-WP-Total'] );
     503                $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
     504                $next_link = add_query_arg( array(
     505                        'page'    => 2,
     506                        ), rest_url( '/wp/v2/posts' ) );
     507                $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
     508                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     509                // 3rd page
     510                $this->factory->post->create( array(
     511                                'post_title'   => 'Post 51',
     512                                ) );
     513                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     514                $request->set_param( 'page', 3 );
     515                $response = $this->server->dispatch( $request );
     516                $headers = $response->get_headers();
     517                $this->assertEquals( 51, $headers['X-WP-Total'] );
     518                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     519                $prev_link = add_query_arg( array(
     520                        'page'    => 2,
     521                        ), rest_url( '/wp/v2/posts' ) );
     522                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     523                $next_link = add_query_arg( array(
     524                        'page'    => 4,
     525                        ), rest_url( '/wp/v2/posts' ) );
     526                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     527                // Last page
     528                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     529                $request->set_param( 'page', 6 );
     530                $response = $this->server->dispatch( $request );
     531                $headers = $response->get_headers();
     532                $this->assertEquals( 51, $headers['X-WP-Total'] );
     533                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     534                $prev_link = add_query_arg( array(
     535                        'page'    => 5,
     536                        ), rest_url( '/wp/v2/posts' ) );
     537                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     538                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     539                // Out of bounds
     540                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     541                $request->set_param( 'page', 8 );
     542                $response = $this->server->dispatch( $request );
     543                $headers = $response->get_headers();
     544                $this->assertEquals( 51, $headers['X-WP-Total'] );
     545                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     546                $prev_link = add_query_arg( array(
     547                        'page'    => 6,
     548                        ), rest_url( '/wp/v2/posts' ) );
     549                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     550                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     551
     552                // With query params.
     553                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     554                $request->set_query_params( array( 'per_page' => 5, 'page' => 2 ) );
     555                $response = $this->server->dispatch( $request );
     556                $headers = $response->get_headers();
     557                $this->assertEquals( 51, $headers['X-WP-Total'] );
     558                $this->assertEquals( 11, $headers['X-WP-TotalPages'] );
     559                $prev_link = add_query_arg( array(
     560                        'per_page' => 5,
     561                        'page'     => 1,
     562                        ), rest_url( '/wp/v2/posts' ) );
     563                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     564                $next_link = add_query_arg( array(
     565                        'per_page' => 5,
     566                        'page'     => 3,
     567                        ), rest_url( '/wp/v2/posts' ) );
     568                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     569        }
     570
     571        public function test_get_items_private_filter_query_var() {
     572                // Private query vars inaccessible to unauthorized users
     573                wp_set_current_user( 0 );
     574                $draft_id = $this->factory->post->create( array( 'post_status' => 'draft' ) );
     575                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     576                $request->set_param( 'filter', array( 'post_status' => 'draft' ) );
     577                $response = $this->server->dispatch( $request );
     578                $data = $response->get_data();
     579                $this->assertCount( 1, $data );
     580                $this->assertEquals( $this->post_id, $data[0]['id'] );
     581                // But they are accessible to authorized users
     582                wp_set_current_user( $this->editor_id );
     583                $response = $this->server->dispatch( $request );
     584                $data = $response->get_data();
     585                $this->assertCount( 1, $data );
     586                $this->assertEquals( $draft_id, $data[0]['id'] );
     587        }
     588
     589        public function test_get_items_invalid_per_page() {
     590                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     591                $request->set_query_params( array( 'per_page' => -1 ) );
     592                $response = $this->server->dispatch( $request );
     593                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     594        }
     595
     596        public function test_get_items_invalid_posts_per_page_ignored() {
     597                // This test ensures that filter[posts_per_page] is ignored, and that -1
     598                // cannot be used to sidestep per_page's valid range to retrieve all posts
     599                for ( $i = 0; $i < 20; $i++ ) {
     600                        $this->factory->post->create( array( 'post_status' => 'publish' ) );
     601                }
     602                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     603                $request->set_query_params( array( 'filter' => array( 'posts_per_page' => -1 ) ) );
     604                $response = $this->server->dispatch( $request );
     605                $this->assertCount( 10, $response->get_data() );
     606        }
     607
     608        public function test_get_items_invalid_context() {
     609                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     610                $request->set_param( 'context', 'banana' );
     611                $response = $this->server->dispatch( $request );
     612                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     613        }
     614
     615        public function test_get_items_invalid_date() {
     616                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     617                $request->set_param( 'after', rand_str() );
     618                $request->set_param( 'before', rand_str() );
     619                $response = $this->server->dispatch( $request );
     620                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     621        }
     622
     623        public function test_get_items_valid_date() {
     624                $post1 = $this->factory->post->create( array( 'post_date' => '2016-01-15T00:00:00Z' ) );
     625                $post2 = $this->factory->post->create( array( 'post_date' => '2016-01-16T00:00:00Z' ) );
     626                $post3 = $this->factory->post->create( array( 'post_date' => '2016-01-17T00:00:00Z' ) );
     627
     628                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     629                $request->set_param( 'after', '2016-01-15T00:00:00Z' );
     630                $request->set_param( 'before', '2016-01-17T00:00:00Z' );
     631                $response = $this->server->dispatch( $request );
     632                $data = $response->get_data();
     633                $this->assertCount( 1, $data );
     634                $this->assertEquals( $post2, $data[0]['id'] );
     635        }
     636
     637        public function test_get_item() {
     638                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     639                $response = $this->server->dispatch( $request );
     640
     641                $this->check_get_post_response( $response, 'view' );
     642        }
     643
     644        public function test_get_item_links() {
     645                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     646                $response = $this->server->dispatch( $request );
     647
     648                $links = $response->get_links();
     649
     650                $this->assertEquals( rest_url( '/wp/v2/posts/' . $this->post_id ), $links['self'][0]['href'] );
     651                $this->assertEquals( rest_url( '/wp/v2/posts' ), $links['collection'][0]['href'] );
     652
     653                $this->assertEquals( rest_url( '/wp/v2/types/' . get_post_type( $this->post_id ) ), $links['about'][0]['href'] );
     654
     655                $replies_url = rest_url( '/wp/v2/comments' );
     656                $replies_url = add_query_arg( 'post', $this->post_id, $replies_url );
     657                $this->assertEquals( $replies_url, $links['replies'][0]['href'] );
     658
     659                $this->assertEquals( rest_url( '/wp/v2/posts/' . $this->post_id . '/revisions' ), $links['version-history'][0]['href'] );
     660
     661                $attachments_url = rest_url( '/wp/v2/media' );
     662                $attachments_url = add_query_arg( 'parent', $this->post_id, $attachments_url );
     663                $this->assertEquals( $attachments_url, $links['https://api.w.org/attachment'][0]['href'] );
     664
     665                $term_links = $links['https://api.w.org/term'];
     666                $tag_link = $cat_link = $format_link = null;
     667                foreach ( $term_links as $link ) {
     668                        if ( 'post_tag' === $link['attributes']['taxonomy'] ) {
     669                                $tag_link = $link;
     670                        } elseif ( 'category' === $link['attributes']['taxonomy'] ) {
     671                                $cat_link = $link;
     672                        } elseif ( 'post_format' === $link['attributes']['taxonomy'] ) {
     673                                $format_link = $link;
     674                        }
     675                }
     676                $this->assertNotEmpty( $tag_link );
     677                $this->assertNotEmpty( $cat_link );
     678                $this->assertNull( $format_link );
     679
     680                $tags_url = add_query_arg( 'post', $this->post_id, rest_url( '/wp/v2/tags' ) );
     681                $this->assertEquals( $tags_url, $tag_link['href'] );
     682
     683                $category_url = add_query_arg( 'post', $this->post_id, rest_url( '/wp/v2/categories' ) );
     684                $this->assertEquals( $category_url, $cat_link['href'] );
     685        }
     686
     687        public function test_get_item_links_no_author() {
     688                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     689                $response = $this->server->dispatch( $request );
     690                $links = $response->get_links();
     691                $this->assertFalse( isset( $links['author'] ) );
     692                wp_update_post( array( 'ID' => $this->post_id, 'post_author' => $this->author_id ) );
     693                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     694                $response = $this->server->dispatch( $request );
     695                $links = $response->get_links();
     696                $this->assertEquals( rest_url( '/wp/v2/users/' . $this->author_id ), $links['author'][0]['href'] );
     697        }
     698
     699        public function test_get_post_without_permission() {
     700                $draft_id = $this->factory->post->create( array(
     701                        'post_status' => 'draft',
     702                ) );
     703                wp_set_current_user( 0 );
     704
     705                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $draft_id ) );
     706                $response = $this->server->dispatch( $request );
     707
     708                $this->assertErrorResponse( 'rest_forbidden', $response, 403 );
     709        }
     710
     711        public function test_get_post_invalid_id() {
     712                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     713                $response = $this->server->dispatch( $request );
     714
     715                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     716        }
     717
     718        public function test_get_post_list_context_with_permission() {
     719                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     720                $request->set_query_params( array(
     721                        'context' => 'edit',
     722                ) );
     723
     724                wp_set_current_user( $this->editor_id );
     725
     726                $response = $this->server->dispatch( $request );
     727
     728                $this->check_get_posts_response( $response, 'edit' );
     729        }
     730
     731        public function test_get_post_list_context_without_permission() {
     732                wp_set_current_user( 0 );
     733                $request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
     734                $request->set_query_params( array(
     735                        'context' => 'edit',
     736                ) );
     737                $response = $this->server->dispatch( $request );
     738
     739                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     740        }
     741
     742        public function test_get_post_context_without_permission() {
     743                wp_set_current_user( 0 );
     744                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     745                $request->set_query_params( array(
     746                        'context' => 'edit',
     747                ) );
     748                $response = $this->server->dispatch( $request );
     749
     750                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     751        }
     752
     753        public function test_get_post_with_password() {
     754                $post_id = $this->factory->post->create( array(
     755                        'post_password' => '$inthebananastand',
     756                ) );
     757
     758                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
     759                $response = $this->server->dispatch( $request );
     760
     761                $this->check_get_post_response( $response, 'view' );
     762
     763                $data = $response->get_data();
     764                $this->assertTrue( $data['content']['protected'] );
     765                $this->assertTrue( $data['excerpt']['protected'] );
     766        }
     767
     768        public function test_get_post_with_password_using_password() {
     769                global $wp_version;
     770                if ( version_compare( $wp_version, '4.7-alpha', '<' ) ) {
     771                        return $this->markTestSkipped( 'WordPress < 4.6 does not support filtering passwords for posts.' );
     772                }
     773
     774                $post_id = $this->factory->post->create( array(
     775                        'post_password' => '$inthebananastand',
     776                        'post_content'  => 'Some secret content.',
     777                        'post_excerpt'  => 'Some secret excerpt.',
     778                ) );
     779
     780                $post = get_post( $post_id );
     781                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
     782                $request->set_param( 'password', '$inthebananastand' );
     783                $response = $this->server->dispatch( $request );
     784
     785                $this->check_get_post_response( $response, 'view' );
     786
     787                $data = $response->get_data();
     788                $this->assertEquals( wpautop( $post->post_content ), $data['content']['rendered'] );
     789                $this->assertEquals( wpautop( $post->post_excerpt ), $data['excerpt']['rendered'] );
     790        }
     791
     792        public function test_get_post_with_password_using_incorrect_password() {
     793                $post_id = $this->factory->post->create( array(
     794                        'post_password' => '$inthebananastand',
     795                ) );
     796
     797                $post = get_post( $post_id );
     798                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
     799                $request->set_param( 'password', 'wrongpassword' );
     800                $response = $this->server->dispatch( $request );
     801
     802                $this->assertErrorResponse( 'rest_post_incorrect_password', $response, 403 );
     803        }
     804
     805        public function test_get_post_with_password_without_permission() {
     806                $post_id = $this->factory->post->create( array(
     807                        'post_password' => '$inthebananastand',
     808                        'post_content'  => 'Some secret content.',
     809                        'post_excerpt'  => 'Some secret excerpt.',
     810                ) );
     811                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) );
     812                $response = $this->server->dispatch( $request );
     813                $data = $response->get_data();
     814                $this->check_get_post_response( $response, 'view' );
     815                $this->assertEquals( '', $data['content']['rendered'] );
     816                $this->assertEquals( '', $data['excerpt']['rendered'] );
     817
     818        }
     819
     820        public function test_get_item_read_permission_custom_post_status() {
     821                register_post_status( 'testpubstatus', array( 'public' => true ) );
     822                register_post_status( 'testprivtatus', array( 'public' => false ) );
     823                // Public status
     824                wp_update_post( array( 'ID' => $this->post_id, 'post_status' => 'testpubstatus' ) );
     825                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     826                $response = $this->server->dispatch( $request );
     827                $this->assertEquals( 200, $response->get_status() );
     828                // Private status
     829                wp_update_post( array( 'ID' => $this->post_id, 'post_status' => 'testprivtatus' ) );
     830                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     831                $response = $this->server->dispatch( $request );
     832                $this->assertEquals( 403, $response->get_status() );
     833        }
     834
     835        public function test_prepare_item() {
     836                wp_set_current_user( $this->editor_id );
     837
     838                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     839                $request->set_query_params( array( 'context' => 'edit' ) );
     840                $response = $this->server->dispatch( $request );
     841
     842                $this->check_get_post_response( $response, 'edit' );
     843        }
     844
     845        public function test_create_item() {
     846                wp_set_current_user( $this->editor_id );
     847
     848                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     849                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     850                $params = $this->set_post_data();
     851                $request->set_body_params( $params );
     852                $response = $this->server->dispatch( $request );
     853
     854                $this->check_create_post_response( $response );
     855        }
     856
     857        public function test_rest_create_item() {
     858                wp_set_current_user( $this->editor_id );
     859
     860                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     861                $request->add_header( 'content-type', 'application/json' );
     862                $params = $this->set_post_data();
     863                $request->set_body( wp_json_encode( $params ) );
     864                $response = $this->server->dispatch( $request );
     865
     866                $this->check_create_post_response( $response );
     867        }
     868
     869        public function test_create_post_invalid_id() {
     870                wp_set_current_user( $this->editor_id );
     871
     872                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     873                $params = $this->set_post_data( array(
     874                        'id' => '3',
     875                ) );
     876                $request->set_body_params( $params );
     877                $response = $this->server->dispatch( $request );
     878
     879                $this->assertErrorResponse( 'rest_post_exists', $response, 400 );
     880        }
     881
     882        public function test_create_post_as_contributor() {
     883                wp_set_current_user( $this->contributor_id );
     884
     885                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     886                $params = $this->set_post_data(array(
     887                        'status' => 'pending',
     888                ));
     889
     890                $request->set_body_params( $params );
     891                $response = $this->server->dispatch( $request );
     892                $this->check_create_post_response( $response );
     893        }
     894
     895        public function test_create_post_sticky() {
     896                wp_set_current_user( $this->editor_id );
     897
     898                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     899                $params = $this->set_post_data( array(
     900                        'sticky' => true,
     901                ) );
     902                $request->set_body_params( $params );
     903                $response = $this->server->dispatch( $request );
     904
     905                $new_data = $response->get_data();
     906                $this->assertEquals( true, $new_data['sticky'] );
     907                $post = get_post( $new_data['id'] );
     908                $this->assertEquals( true, is_sticky( $post->ID ) );
     909        }
     910
     911        public function test_create_post_sticky_as_contributor() {
     912                wp_set_current_user( $this->contributor_id );
     913
     914                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     915                $params = $this->set_post_data( array(
     916                        'sticky' => true,
     917                        'status' => 'pending',
     918                ) );
     919                $request->set_body_params( $params );
     920                $response = $this->server->dispatch( $request );
     921
     922                $this->assertErrorResponse( 'rest_cannot_assign_sticky', $response, 403 );
     923        }
     924
     925        public function test_create_post_other_author_without_permission() {
     926                wp_set_current_user( $this->author_id );
     927
     928                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     929                $params = $this->set_post_data(array(
     930                        'author' => $this->editor_id,
     931                ));
     932                $request->set_body_params( $params );
     933                $response = $this->server->dispatch( $request );
     934
     935                $this->assertErrorResponse( 'rest_cannot_edit_others', $response, 403 );
     936        }
     937
     938        public function test_create_post_without_permission() {
     939                wp_set_current_user( 0 );
     940
     941                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     942                $params = $this->set_post_data( array(
     943                        'status' => 'draft',
     944                ) );
     945                $request->set_body_params( $params );
     946                $response = $this->server->dispatch( $request );
     947
     948                $this->assertErrorResponse( 'rest_cannot_create', $response, 401 );
     949        }
     950
     951        public function test_create_post_draft() {
     952                wp_set_current_user( $this->editor_id );
     953
     954                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     955                $params = $this->set_post_data( array(
     956                        'status' => 'draft',
     957                ) );
     958                $request->set_body_params( $params );
     959                $response = $this->server->dispatch( $request );
     960
     961                $data = $response->get_data();
     962                $new_post = get_post( $data['id'] );
     963                $this->assertEquals( 'draft', $data['status'] );
     964                $this->assertEquals( 'draft', $new_post->post_status );
     965                // Confirm dates are null
     966                $this->assertNull( $data['date_gmt'] );
     967                $this->assertNull( $data['modified_gmt'] );
     968        }
     969
     970        public function test_create_post_private() {
     971                wp_set_current_user( $this->editor_id );
     972
     973                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     974                $params = $this->set_post_data( array(
     975                        'status' => 'private',
     976                ) );
     977                $request->set_body_params( $params );
     978                $response = $this->server->dispatch( $request );
     979
     980                $data = $response->get_data();
     981                $new_post = get_post( $data['id'] );
     982                $this->assertEquals( 'private', $data['status'] );
     983                $this->assertEquals( 'private', $new_post->post_status );
     984        }
     985
     986        public function test_create_post_private_without_permission() {
     987                wp_set_current_user( $this->author_id );
     988                $user = wp_get_current_user();
     989                $user->add_cap( 'publish_posts', false );
     990                // Flush capabilities, https://core.trac.wordpress.org/ticket/28374
     991                $user->get_role_caps();
     992                $user->update_user_level_from_caps();
     993
     994                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     995                $params = $this->set_post_data( array(
     996                        'status' => 'private',
     997                        'author' => $this->author_id,
     998                ) );
     999                $request->set_body_params( $params );
     1000                $response = $this->server->dispatch( $request );
     1001
     1002                $this->assertErrorResponse( 'rest_cannot_publish', $response, 403 );
     1003        }
     1004
     1005        public function test_create_post_publish_without_permission() {
     1006                wp_set_current_user( $this->author_id );
     1007                $user = wp_get_current_user();
     1008                $user->add_cap( 'publish_posts', false );
     1009                // Flush capabilities, https://core.trac.wordpress.org/ticket/28374
     1010                $user->get_role_caps();
     1011                $user->update_user_level_from_caps();
     1012
     1013                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1014                $params = $this->set_post_data( array(
     1015                        'status' => 'publish',
     1016                ) );
     1017                $request->set_body_params( $params );
     1018                $response = $this->server->dispatch( $request );
     1019
     1020                $this->assertErrorResponse( 'rest_cannot_publish', $response, 403 );
     1021        }
     1022
     1023        public function test_create_post_invalid_status() {
     1024                wp_set_current_user( $this->editor_id );
     1025
     1026                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1027                $params = $this->set_post_data( array(
     1028                        'status' => 'teststatus',
     1029                ) );
     1030                $request->set_body_params( $params );
     1031                $response = $this->server->dispatch( $request );
     1032
     1033                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1034        }
     1035
     1036        public function test_create_post_with_format() {
     1037                wp_set_current_user( $this->editor_id );
     1038
     1039                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1040                $params = $this->set_post_data( array(
     1041                        'format' => 'gallery',
     1042                ) );
     1043                $request->set_body_params( $params );
     1044                $response = $this->server->dispatch( $request );
     1045
     1046                $data = $response->get_data();
     1047                $new_post = get_post( $data['id'] );
     1048                $this->assertEquals( 'gallery', $data['format'] );
     1049                $this->assertEquals( 'gallery', get_post_format( $new_post->ID ) );
     1050        }
     1051
     1052        public function test_create_post_with_invalid_format() {
     1053                wp_set_current_user( $this->editor_id );
     1054
     1055                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1056                $params = $this->set_post_data( array(
     1057                        'format' => 'testformat',
     1058                ) );
     1059                $request->set_body_params( $params );
     1060                $response = $this->server->dispatch( $request );
     1061
     1062                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1063        }
     1064
     1065        public function test_create_update_post_with_featured_media() {
     1066
     1067                $file = DIR_TESTDATA . '/images/canola.jpg';
     1068                $this->attachment_id = $this->factory->attachment->create_object( $file, 0, array(
     1069                        'post_mime_type' => 'image/jpeg',
     1070                        'menu_order' => rand( 1, 100 ),
     1071                ) );
     1072
     1073                wp_set_current_user( $this->editor_id );
     1074
     1075                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1076                $params = $this->set_post_data( array(
     1077                        'featured_media' => $this->attachment_id,
     1078                ) );
     1079                $request->set_body_params( $params );
     1080                $response = $this->server->dispatch( $request );
     1081                $data = $response->get_data();
     1082                $new_post = get_post( $data['id'] );
     1083                $this->assertEquals( $this->attachment_id, $data['featured_media'] );
     1084                $this->assertEquals( $this->attachment_id, (int) get_post_thumbnail_id( $new_post->ID ) );
     1085
     1086                $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $new_post->ID );
     1087                $params = $this->set_post_data( array(
     1088                        'featured_media' => 0,
     1089                ) );
     1090                $request->set_body_params( $params );
     1091                $response = $this->server->dispatch( $request );
     1092                $data = $response->get_data();
     1093                $this->assertEquals( 0, $data['featured_media'] );
     1094                $this->assertEquals( 0, (int) get_post_thumbnail_id( $new_post->ID ) );
     1095        }
     1096
     1097        public function test_create_post_invalid_author() {
     1098                wp_set_current_user( $this->editor_id );
     1099
     1100                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1101                $params = $this->set_post_data( array(
     1102                        'author' => -1,
     1103                ) );
     1104                $request->set_body_params( $params );
     1105                $response = $this->server->dispatch( $request );
     1106
     1107                $this->assertErrorResponse( 'rest_invalid_author', $response, 400 );
     1108        }
     1109
     1110        public function test_create_post_invalid_author_without_permission() {
     1111                wp_set_current_user( $this->author_id );
     1112
     1113                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1114                $params = $this->set_post_data( array(
     1115                        'author' => $this->editor_id,
     1116                ) );
     1117                $request->set_body_params( $params );
     1118                $response = $this->server->dispatch( $request );
     1119
     1120                $this->assertErrorResponse( 'rest_cannot_edit_others', $response, 403 );
     1121        }
     1122
     1123        public function test_create_post_with_password() {
     1124                wp_set_current_user( $this->editor_id );
     1125
     1126                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1127                $params = $this->set_post_data( array(
     1128                        'password' => 'testing',
     1129                ) );
     1130                $request->set_body_params( $params );
     1131                $response = $this->server->dispatch( $request );
     1132
     1133                $data = $response->get_data();
     1134                $this->assertEquals( 'testing', $data['password'] );
     1135        }
     1136
     1137        public function test_create_post_with_falsy_password() {
     1138                wp_set_current_user( $this->editor_id );
     1139
     1140                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1141                $params = $this->set_post_data( array(
     1142                        'password' => '0',
     1143                ) );
     1144                $request->set_body_params( $params );
     1145                $response = $this->server->dispatch( $request );
     1146
     1147                $data = $response->get_data();
     1148
     1149                $this->assertEquals( '0', $data['password'] );
     1150        }
     1151
     1152        public function test_create_post_with_empty_string_password_and_sticky() {
     1153                wp_set_current_user( $this->editor_id );
     1154
     1155                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1156                $params = $this->set_post_data( array(
     1157                        'password' => '',
     1158                        'sticky'   => true,
     1159                ) );
     1160                $request->set_body_params( $params );
     1161                $response = $this->server->dispatch( $request );
     1162
     1163                $this->assertEquals( 201, $response->get_status() );
     1164                $data = $response->get_data();
     1165                $this->assertEquals( '', $data['password'] );
     1166        }
     1167
     1168        public function test_create_post_with_password_and_sticky_fails() {
     1169                wp_set_current_user( $this->editor_id );
     1170
     1171                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1172                $params = $this->set_post_data( array(
     1173                        'password' => '123',
     1174                        'sticky'   => true,
     1175                ) );
     1176                $request->set_body_params( $params );
     1177                $response = $this->server->dispatch( $request );
     1178
     1179                $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
     1180        }
     1181
     1182        public function test_create_post_custom_date() {
     1183                wp_set_current_user( $this->editor_id );
     1184
     1185                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1186                $params = $this->set_post_data( array(
     1187                        'date' => '2010-01-01T02:00:00Z',
     1188                ) );
     1189                $request->set_body_params( $params );
     1190                $response = $this->server->dispatch( $request );
     1191
     1192                $data = $response->get_data();
     1193                $new_post = get_post( $data['id'] );
     1194                $time = gmmktime( 2, 0, 0, 1, 1, 2010 );
     1195                $this->assertEquals( '2010-01-01T02:00:00', $data['date'] );
     1196                $this->assertEquals( $time, strtotime( $new_post->post_date ) );
     1197        }
     1198
     1199        public function test_create_post_custom_date_with_timezone() {
     1200                wp_set_current_user( $this->editor_id );
     1201
     1202                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1203                $params = $this->set_post_data( array(
     1204                        'date' => '2010-01-01T02:00:00-10:00',
     1205                ) );
     1206                $request->set_body_params( $params );
     1207                $response = $this->server->dispatch( $request );
     1208
     1209                $data = $response->get_data();
     1210                $new_post = get_post( $data['id'] );
     1211                $time = gmmktime( 12, 0, 0, 1, 1, 2010 );
     1212
     1213                $this->assertEquals( '2010-01-01T12:00:00', $data['date'] );
     1214                $this->assertEquals( '2010-01-01T12:00:00', $data['modified'] );
     1215
     1216                $this->assertEquals( $time, strtotime( $new_post->post_date ) );
     1217                $this->assertEquals( $time, strtotime( $new_post->post_modified ) );
     1218        }
     1219
     1220        public function test_create_post_with_db_error() {
     1221                wp_set_current_user( $this->editor_id );
     1222
     1223                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1224                $params  = $this->set_post_data( array() );
     1225                $request->set_body_params( $params );
     1226
     1227                /**
     1228                 * Disable showing error as the below is going to intentionally
     1229                 * trigger a DB error.
     1230                 */
     1231                global $wpdb;
     1232                $wpdb->suppress_errors = true;
     1233                add_filter( 'query', array( $this, 'error_insert_query' ) );
     1234
     1235                $response = $this->server->dispatch( $request );
     1236                remove_filter( 'query', array( $this, 'error_insert_query' ) );
     1237                $wpdb->show_errors = true;
     1238
     1239                $this->assertErrorResponse( 'db_insert_error', $response, 500 );
     1240        }
     1241
     1242        public function test_create_post_with_invalid_date() {
     1243                wp_set_current_user( $this->editor_id );
     1244
     1245                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1246                $params = $this->set_post_data( array(
     1247                        'date' => '2010-60-01T02:00:00Z',
     1248                ) );
     1249                $request->set_body_params( $params );
     1250                $response = $this->server->dispatch( $request );
     1251
     1252                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1253        }
     1254
     1255        public function test_create_post_with_invalid_date_gmt() {
     1256                wp_set_current_user( $this->editor_id );
     1257
     1258                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1259                $params = $this->set_post_data( array(
     1260                        'date_gmt' => '2010-60-01T02:00:00',
     1261                ) );
     1262                $request->set_body_params( $params );
     1263                $response = $this->server->dispatch( $request );
     1264
     1265                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1266        }
     1267
     1268        public function test_create_post_with_quotes_in_title() {
     1269                wp_set_current_user( $this->editor_id );
     1270
     1271                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1272                $params = $this->set_post_data( array(
     1273                        'title' => "Rob O'Rourke's Diary",
     1274                ) );
     1275                $request->set_body_params( $params );
     1276                $response = $this->server->dispatch( $request );
     1277                $new_data = $response->get_data();
     1278                $this->assertEquals( "Rob O'Rourke's Diary", $new_data['title']['raw'] );
     1279        }
     1280
     1281        public function test_create_post_with_categories() {
     1282                wp_set_current_user( $this->editor_id );
     1283                $category = wp_insert_term( 'Test Category', 'category' );
     1284                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1285                $params = $this->set_post_data( array(
     1286                        'password'   => 'testing',
     1287                        'categories' => array(
     1288                                $category['term_id']
     1289                        ),
     1290                ) );
     1291                $request->set_body_params( $params );
     1292                $response = $this->server->dispatch( $request );
     1293
     1294                $data = $response->get_data();
     1295                $this->assertEquals( array( $category['term_id'] ), $data['categories'] );
     1296        }
     1297
     1298        public function test_create_post_with_invalid_categories() {
     1299                wp_set_current_user( $this->editor_id );
     1300                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1301                $params = $this->set_post_data( array(
     1302                        'password'   => 'testing',
     1303                        'categories' => array(
     1304                                REST_TESTS_IMPOSSIBLY_HIGH_NUMBER
     1305                        ),
     1306                ) );
     1307                $request->set_body_params( $params );
     1308                $response = $this->server->dispatch( $request );
     1309
     1310                $data = $response->get_data();
     1311                $this->assertEquals( array(), $data['categories'] );
     1312        }
     1313
     1314        public function test_update_item() {
     1315                wp_set_current_user( $this->editor_id );
     1316
     1317                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1318                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     1319                $params = $this->set_post_data();
     1320                $request->set_body_params( $params );
     1321                $response = $this->server->dispatch( $request );
     1322
     1323                $this->check_update_post_response( $response );
     1324                $new_data = $response->get_data();
     1325                $this->assertEquals( $this->post_id, $new_data['id'] );
     1326                $this->assertEquals( $params['title'], $new_data['title']['raw'] );
     1327                $this->assertEquals( $params['content'], $new_data['content']['raw'] );
     1328                $this->assertEquals( $params['excerpt'], $new_data['excerpt']['raw'] );
     1329                $post = get_post( $this->post_id );
     1330                $this->assertEquals( $params['title'], $post->post_title );
     1331                $this->assertEquals( $params['content'], $post->post_content );
     1332                $this->assertEquals( $params['excerpt'], $post->post_excerpt );
     1333        }
     1334
     1335        public function test_rest_update_post() {
     1336                wp_set_current_user( $this->editor_id );
     1337
     1338                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1339                $request->add_header( 'content-type', 'application/json' );
     1340                $params = $this->set_post_data();
     1341                $request->set_body( wp_json_encode( $params ) );
     1342                $response = $this->server->dispatch( $request );
     1343
     1344                $this->check_update_post_response( $response );
     1345                $new_data = $response->get_data();
     1346                $this->assertEquals( $this->post_id, $new_data['id'] );
     1347                $this->assertEquals( $params['title'], $new_data['title']['raw'] );
     1348                $this->assertEquals( $params['content'], $new_data['content']['raw'] );
     1349                $this->assertEquals( $params['excerpt'], $new_data['excerpt']['raw'] );
     1350                $post = get_post( $this->post_id );
     1351                $this->assertEquals( $params['title'], $post->post_title );
     1352                $this->assertEquals( $params['content'], $post->post_content );
     1353                $this->assertEquals( $params['excerpt'], $post->post_excerpt );
     1354        }
     1355
     1356        public function test_rest_update_post_raw() {
     1357                wp_set_current_user( $this->editor_id );
     1358
     1359                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1360                $request->add_header( 'content-type', 'application/json' );
     1361                $params = $this->set_raw_post_data();
     1362                $request->set_body( wp_json_encode( $params ) );
     1363                $response = $this->server->dispatch( $request );
     1364
     1365                $this->check_update_post_response( $response );
     1366                $new_data = $response->get_data();
     1367                $this->assertEquals( $this->post_id, $new_data['id'] );
     1368                $this->assertEquals( $params['title']['raw'], $new_data['title']['raw'] );
     1369                $this->assertEquals( $params['content']['raw'], $new_data['content']['raw'] );
     1370                $this->assertEquals( $params['excerpt']['raw'], $new_data['excerpt']['raw'] );
     1371                $post = get_post( $this->post_id );
     1372                $this->assertEquals( $params['title']['raw'], $post->post_title );
     1373                $this->assertEquals( $params['content']['raw'], $post->post_content );
     1374                $this->assertEquals( $params['excerpt']['raw'], $post->post_excerpt );
     1375        }
     1376
     1377        public function test_update_post_without_extra_params() {
     1378                wp_set_current_user( $this->editor_id );
     1379
     1380                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1381                $params = $this->set_post_data();
     1382                unset( $params['type'] );
     1383                unset( $params['name'] );
     1384                unset( $params['author'] );
     1385                unset( $params['status'] );
     1386                $request->set_body_params( $params );
     1387                $response = $this->server->dispatch( $request );
     1388
     1389                $this->check_update_post_response( $response );
     1390        }
     1391
     1392        public function test_update_post_without_permission() {
     1393                wp_set_current_user( $this->editor_id );
     1394                $user = wp_get_current_user();
     1395                $user->add_cap( 'edit_published_posts', false );
     1396                // Flush capabilities, https://core.trac.wordpress.org/ticket/28374
     1397                $user->get_role_caps();
     1398                $user->update_user_level_from_caps();
     1399
     1400                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1401                $params = $this->set_post_data();
     1402                $request->set_body_params( $params );
     1403                $response = $this->server->dispatch( $request );
     1404
     1405                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     1406        }
     1407
     1408        public function test_update_post_sticky_as_contributor() {
     1409                wp_set_current_user( $this->contributor_id );
     1410
     1411                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1412                $params = $this->set_post_data( array(
     1413                        'sticky' => true,
     1414                        'status' => 'pending',
     1415                ) );
     1416                $request->set_body_params( $params );
     1417                $response = $this->server->dispatch( $request );
     1418
     1419                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     1420        }
     1421
     1422        public function test_update_post_invalid_id() {
     1423                wp_set_current_user( $this->editor_id );
     1424
     1425                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ) );
     1426                $response = $this->server->dispatch( $request );
     1427
     1428                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     1429        }
     1430
     1431        public function test_update_post_invalid_route() {
     1432                wp_set_current_user( $this->editor_id );
     1433
     1434                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/pages/%d', $this->post_id ) );
     1435                $response = $this->server->dispatch( $request );
     1436
     1437                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     1438        }
     1439
     1440        public function test_update_post_with_format() {
     1441                wp_set_current_user( $this->editor_id );
     1442
     1443                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1444                $params = $this->set_post_data( array(
     1445                        'format' => 'gallery',
     1446                ) );
     1447                $request->set_body_params( $params );
     1448                $response = $this->server->dispatch( $request );
     1449
     1450                $data = $response->get_data();
     1451                $new_post = get_post( $data['id'] );
     1452                $this->assertEquals( 'gallery', $data['format'] );
     1453                $this->assertEquals( 'gallery', get_post_format( $new_post->ID ) );
     1454        }
     1455
     1456        public function test_update_post_with_invalid_format() {
     1457                wp_set_current_user( $this->editor_id );
     1458
     1459                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1460                $params = $this->set_post_data( array(
     1461                        'format' => 'testformat',
     1462                ) );
     1463                $request->set_body_params( $params );
     1464                $response = $this->server->dispatch( $request );
     1465
     1466                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1467        }
     1468
     1469        public function test_update_post_ignore_readonly() {
     1470                wp_set_current_user( $this->editor_id );
     1471
     1472                $new_content = rand_str();
     1473                $expected_modified = current_time( 'mysql' );
     1474
     1475                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1476                $params = $this->set_post_data( array(
     1477                        'modified' => '2010-06-01T02:00:00Z',
     1478                        'content'  => $new_content,
     1479                ) );
     1480                $request->set_body_params( $params );
     1481                $response = $this->server->dispatch( $request );
     1482
     1483                // The readonly modified param should be ignored, request should be a success.
     1484                $data = $response->get_data();
     1485                $new_post = get_post( $data['id'] );
     1486
     1487                $this->assertEquals( $new_content, $data['content']['raw'] );
     1488                $this->assertEquals( $new_content, $new_post->post_content );
     1489
     1490                // The modified date should equal the current time.
     1491                $this->assertEquals( date( 'Y-m-d', strtotime( mysql_to_rfc3339( $expected_modified ) ) ), date( 'Y-m-d', strtotime( $data['modified'] ) ) );
     1492                $this->assertEquals( date( 'Y-m-d', strtotime( $expected_modified ) ), date( 'Y-m-d', strtotime( $new_post->post_modified ) ) );
     1493        }
     1494
     1495        public function test_update_post_with_invalid_date() {
     1496                wp_set_current_user( $this->editor_id );
     1497
     1498                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1499                $params = $this->set_post_data( array(
     1500                        'date' => rand_str(),
     1501                ) );
     1502                $request->set_body_params( $params );
     1503                $response = $this->server->dispatch( $request );
     1504
     1505                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1506        }
     1507
     1508        public function test_update_post_with_invalid_date_gmt() {
     1509                wp_set_current_user( $this->editor_id );
     1510
     1511                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1512                $params = $this->set_post_data( array(
     1513                        'date_gmt' => rand_str(),
     1514                ) );
     1515                $request->set_body_params( $params );
     1516                $response = $this->server->dispatch( $request );
     1517
     1518                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1519        }
     1520
     1521        public function test_update_post_slug() {
     1522                wp_set_current_user( $this->editor_id );
     1523
     1524                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1525                $params = $this->set_post_data( array(
     1526                        'slug' => 'sample-slug',
     1527                ) );
     1528                $request->set_body_params( $params );
     1529                $response = $this->server->dispatch( $request );
     1530
     1531                $new_data = $response->get_data();
     1532                $this->assertEquals( 'sample-slug', $new_data['slug'] );
     1533                $post = get_post( $new_data['id'] );
     1534                $this->assertEquals( 'sample-slug', $post->post_name );
     1535        }
     1536
     1537        public function test_update_post_slug_accented_chars() {
     1538                wp_set_current_user( $this->editor_id );
     1539
     1540                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1541                $params = $this->set_post_data( array(
     1542                        'slug' => 'tęst-acceńted-chäræcters',
     1543                ) );
     1544                $request->set_body_params( $params );
     1545                $response = $this->server->dispatch( $request );
     1546
     1547                $new_data = $response->get_data();
     1548                $this->assertEquals( 'test-accented-charaecters', $new_data['slug'] );
     1549                $post = get_post( $new_data['id'] );
     1550                $this->assertEquals( 'test-accented-charaecters', $post->post_name );
     1551        }
     1552
     1553        public function test_update_post_sticky() {
     1554                wp_set_current_user( $this->editor_id );
     1555
     1556                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1557                $params = $this->set_post_data( array(
     1558                        'sticky' => true,
     1559                ) );
     1560                $request->set_body_params( $params );
     1561                $response = $this->server->dispatch( $request );
     1562
     1563                $new_data = $response->get_data();
     1564                $this->assertEquals( true, $new_data['sticky'] );
     1565                $post = get_post( $new_data['id'] );
     1566                $this->assertEquals( true, is_sticky( $post->ID ) );
     1567
     1568                // Updating another field shouldn't change sticky status
     1569                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1570                $params = $this->set_post_data( array(
     1571                        'title'       => 'This should not reset sticky',
     1572                ) );
     1573                $request->set_body_params( $params );
     1574                $response = $this->server->dispatch( $request );
     1575
     1576                $new_data = $response->get_data();
     1577                $this->assertEquals( true, $new_data['sticky'] );
     1578                $post = get_post( $new_data['id'] );
     1579                $this->assertEquals( true, is_sticky( $post->ID ) );
     1580        }
     1581
     1582        public function test_update_post_excerpt() {
     1583                wp_set_current_user( $this->editor_id );
     1584
     1585                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1586                $request->set_body_params( array(
     1587                        'excerpt' => 'An Excerpt',
     1588                ) );
     1589
     1590                $response = $this->server->dispatch( $request );
     1591                $new_data = $response->get_data();
     1592                $this->assertEquals( 'An Excerpt', $new_data['excerpt']['raw'] );
     1593        }
     1594
     1595        public function test_update_post_empty_excerpt() {
     1596                wp_set_current_user( $this->editor_id );
     1597
     1598                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1599                $request->set_body_params( array(
     1600                        'excerpt' => '',
     1601                ) );
     1602
     1603                $response = $this->server->dispatch( $request );
     1604                $new_data = $response->get_data();
     1605                $this->assertEquals( '', $new_data['excerpt']['raw'] );
     1606        }
     1607
     1608        public function test_update_post_content() {
     1609                wp_set_current_user( $this->editor_id );
     1610
     1611                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1612                $request->set_body_params( array(
     1613                        'content' => 'Some Content',
     1614                ) );
     1615
     1616                $response = $this->server->dispatch( $request );
     1617                $new_data = $response->get_data();
     1618                $this->assertEquals( 'Some Content', $new_data['content']['raw'] );
     1619        }
     1620
     1621        public function test_update_post_empty_content() {
     1622                wp_set_current_user( $this->editor_id );
     1623
     1624                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1625                $request->set_body_params( array(
     1626                        'content' => '',
     1627                ) );
     1628
     1629                $response = $this->server->dispatch( $request );
     1630                $new_data = $response->get_data();
     1631                $this->assertEquals( '', $new_data['content']['raw'] );
     1632        }
     1633
     1634        public function test_update_post_with_password_and_sticky_fails() {
     1635                wp_set_current_user( $this->editor_id );
     1636
     1637                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1638                $params = $this->set_post_data( array(
     1639                        'password' => '123',
     1640                        'sticky'   => true,
     1641                ) );
     1642                $request->set_body_params( $params );
     1643                $response = $this->server->dispatch( $request );
     1644
     1645                $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
     1646        }
     1647
     1648        public function test_update_stick_post_with_password_fails() {
     1649                wp_set_current_user( $this->editor_id );
     1650
     1651                stick_post( $this->post_id );
     1652
     1653                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1654                $params = $this->set_post_data( array(
     1655                        'password' => '123',
     1656                ) );
     1657                $request->set_body_params( $params );
     1658                $response = $this->server->dispatch( $request );
     1659
     1660                $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
     1661        }
     1662
     1663        public function test_update_password_protected_post_with_sticky_fails() {
     1664                wp_set_current_user( $this->editor_id );
     1665
     1666                wp_update_post( array( 'ID' => $this->post_id, 'post_password' => '123' ) );
     1667
     1668                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1669                $params = $this->set_post_data( array(
     1670                        'sticky' => true,
     1671                ) );
     1672                $request->set_body_params( $params );
     1673                $response = $this->server->dispatch( $request );
     1674
     1675                $this->assertErrorResponse( 'rest_invalid_field', $response, 400 );
     1676        }
     1677
     1678        public function test_update_post_with_quotes_in_title() {
     1679                wp_set_current_user( $this->editor_id );
     1680
     1681                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1682                $params = $this->set_post_data( array(
     1683                        'title' => "Rob O'Rourke's Diary",
     1684                ) );
     1685                $request->set_body_params( $params );
     1686                $response = $this->server->dispatch( $request );
     1687                $new_data = $response->get_data();
     1688                $this->assertEquals( "Rob O'Rourke's Diary", $new_data['title']['raw'] );
     1689        }
     1690
     1691        public function test_update_post_with_categories() {
     1692
     1693                wp_set_current_user( $this->editor_id );
     1694                $category = wp_insert_term( 'Test Category', 'category' );
     1695
     1696                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1697                $params = $this->set_post_data( array(
     1698                        'title' => 'Tester',
     1699                        'categories' => array(
     1700                                $category['term_id'],
     1701                        ),
     1702                ) );
     1703                $request->set_body_params( $params );
     1704                $response = $this->server->dispatch( $request );
     1705                $new_data = $response->get_data();
     1706                $this->assertEquals( array( $category['term_id'] ), $new_data['categories'] );
     1707                $categories_path = '';
     1708                $links = $response->get_links();
     1709                foreach ( $links['https://api.w.org/term'] as $link ) {
     1710                        if ( 'category' === $link['attributes']['taxonomy'] ) {
     1711                                $categories_path = $link['href'];
     1712                        }
     1713                }
     1714                $query = parse_url( $categories_path, PHP_URL_QUERY );
     1715                parse_str( $query, $args );
     1716                $request = new WP_REST_Request( 'GET', $args['rest_route'] );
     1717                unset( $args['rest_route'] );
     1718                $request->set_query_params( $args );
     1719                $response = $this->server->dispatch( $request );
     1720                $data = $response->get_data();
     1721                $this->assertCount( 1, $data );
     1722                $this->assertEquals( 'Test Category', $data[0]['name'] );
     1723        }
     1724
     1725        public function test_update_post_with_empty_categories() {
     1726
     1727                wp_set_current_user( $this->editor_id );
     1728                $category = wp_insert_term( 'Test Category', 'category' );
     1729                wp_set_object_terms( $this->post_id, $category['term_id'], 'category' );
     1730
     1731                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1732                $params = $this->set_post_data( array(
     1733                        'title' => 'Tester',
     1734                        'categories' => array(),
     1735                ) );
     1736                $request->set_body_params( $params );
     1737                $response = $this->server->dispatch( $request );
     1738                $new_data = $response->get_data();
     1739                $this->assertEquals( array(), $new_data['categories'] );
     1740        }
     1741
     1742        public function test_delete_item() {
     1743                $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) );
     1744                wp_set_current_user( $this->editor_id );
     1745
     1746                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $post_id ) );
     1747                $response = $this->server->dispatch( $request );
     1748
     1749                $this->assertNotInstanceOf( 'WP_Error', $response );
     1750                $this->assertEquals( 200, $response->get_status() );
     1751                $data = $response->get_data();
     1752                $this->assertEquals( 'Deleted post', $data['title']['raw'] );
     1753        }
     1754
     1755        public function test_delete_item_skip_trash() {
     1756                $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) );
     1757                wp_set_current_user( $this->editor_id );
     1758
     1759                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $post_id ) );
     1760                $request['force'] = true;
     1761                $response = $this->server->dispatch( $request );
     1762
     1763                $this->assertNotInstanceOf( 'WP_Error', $response );
     1764                $this->assertEquals( 200, $response->get_status() );
     1765                $data = $response->get_data();
     1766                $this->assertEquals( 'Deleted post', $data['title']['raw'] );
     1767        }
     1768
     1769        public function test_delete_item_already_trashed() {
     1770                $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) );
     1771                wp_set_current_user( $this->editor_id );
     1772                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $post_id ) );
     1773                $response = $this->server->dispatch( $request );
     1774                $this->assertEquals( 200, $response->get_status() );
     1775                $response = $this->server->dispatch( $request );
     1776                $this->assertErrorResponse( 'rest_already_trashed', $response, 410 );
     1777        }
     1778
     1779        public function test_delete_post_invalid_id() {
     1780                wp_set_current_user( $this->editor_id );
     1781
     1782                $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     1783                $response = $this->server->dispatch( $request );
     1784
     1785                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     1786        }
     1787
     1788        public function test_delete_post_invalid_post_type() {
     1789                $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );
     1790                wp_set_current_user( $this->editor_id );
     1791
     1792                $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $page_id );
     1793                $response = $this->server->dispatch( $request );
     1794
     1795                $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );
     1796        }
     1797
     1798        public function test_delete_post_without_permission() {
     1799                wp_set_current_user( $this->author_id );
     1800
     1801                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1802                $response = $this->server->dispatch( $request );
     1803
     1804                $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
     1805        }
     1806
     1807        public function test_register_post_type_invalid_controller() {
     1808
     1809                register_post_type( 'invalid-controller', array( 'show_in_rest' => true, 'rest_controller_class' => 'Fake_Class_Baba' ) );
     1810                create_initial_rest_routes();
     1811                $routes = $this->server->get_routes();
     1812                $this->assertFalse( isset( $routes['/wp/v2/invalid-controller'] ) );
     1813                _unregister_post_type( 'invalid-controller' );
     1814
     1815        }
     1816
     1817        public function test_get_item_schema() {
     1818                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
     1819                $response = $this->server->dispatch( $request );
     1820                $data = $response->get_data();
     1821                $properties = $data['schema']['properties'];
     1822                $this->assertEquals( 25, count( $properties ) );
     1823                $this->assertArrayHasKey( 'author', $properties );
     1824                $this->assertArrayHasKey( 'comment_status', $properties );
     1825                $this->assertArrayHasKey( 'content', $properties );
     1826                $this->assertArrayHasKey( 'date', $properties );
     1827                $this->assertArrayHasKey( 'date_gmt', $properties );
     1828                $this->assertArrayHasKey( 'excerpt', $properties );
     1829                $this->assertArrayHasKey( 'featured_media', $properties );
     1830                $this->assertArrayHasKey( 'guid', $properties );
     1831                $this->assertArrayHasKey( 'format', $properties );
     1832                $this->assertArrayHasKey( 'id', $properties );
     1833                $this->assertArrayHasKey( 'link', $properties );
     1834                $this->assertArrayHasKey( 'meta', $properties );
     1835                $this->assertArrayHasKey( 'modified', $properties );
     1836                $this->assertArrayHasKey( 'modified_gmt', $properties );
     1837                $this->assertArrayHasKey( 'password', $properties );
     1838                $this->assertArrayHasKey( 'ping_status', $properties );
     1839                $this->assertArrayHasKey( 'slug', $properties );
     1840                $this->assertArrayHasKey( 'status', $properties );
     1841                $this->assertArrayHasKey( 'sticky', $properties );
     1842                $this->assertArrayHasKey( 'title', $properties );
     1843                $this->assertArrayHasKey( 'type', $properties );
     1844                $this->assertArrayHasKey( 'tags', $properties );
     1845                $this->assertArrayHasKey( 'tags_exclude', $properties );
     1846                $this->assertArrayHasKey( 'categories', $properties );
     1847                $this->assertArrayHasKey( 'categories_exclude', $properties );
     1848        }
     1849
     1850        public function test_get_additional_field_registration() {
     1851
     1852                $schema = array(
     1853                        'type'        => 'integer',
     1854                        'description' => 'Some integer of mine',
     1855                        'enum'        => array( 1, 2, 3, 4 ),
     1856                        'context'     => array( 'view', 'edit' ),
     1857                );
     1858
     1859                register_rest_field( 'post', 'my_custom_int', array(
     1860                        'schema'          => $schema,
     1861                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     1862                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     1863                ) );
     1864
     1865                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' );
     1866
     1867                $response = $this->server->dispatch( $request );
     1868                $data = $response->get_data();
     1869
     1870                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     1871                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     1872
     1873                wp_set_current_user( 1 );
     1874
     1875                $post_id = $this->factory->post->create();
     1876
     1877                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id );
     1878
     1879                $response = $this->server->dispatch( $request );
     1880                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     1881
     1882                $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $post_id );
     1883                $request->set_body_params(array(
     1884                        'my_custom_int' => 123,
     1885                ));
     1886
     1887                $response = $this->server->dispatch( $request );
     1888                $this->assertEquals( 123, get_post_meta( $post_id, 'my_custom_int', true ) );
     1889
     1890                $request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
     1891                $request->set_body_params(array(
     1892                        'my_custom_int' => 123,
     1893                        'title' => 'hello',
     1894                ));
     1895
     1896                $response = $this->server->dispatch( $request );
     1897
     1898                $this->assertEquals( 123, $response->data['my_custom_int'] );
     1899
     1900                global $wp_rest_additional_fields;
     1901                $wp_rest_additional_fields = array();
     1902        }
     1903
     1904        public function test_additional_field_update_errors() {
     1905                $schema = array(
     1906                        'type'        => 'integer',
     1907                        'description' => 'Some integer of mine',
     1908                        'enum'        => array( 1, 2, 3, 4 ),
     1909                        'context'     => array( 'view', 'edit' ),
     1910                );
     1911
     1912                register_rest_field( 'post', 'my_custom_int', array(
     1913                        'schema'          => $schema,
     1914                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     1915                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     1916                ) );
     1917
     1918                wp_set_current_user( $this->editor_id );
     1919                // Check for error on update.
     1920                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', $this->post_id ) );
     1921                $request->set_body_params( array(
     1922                        'my_custom_int' => 'returnError',
     1923                ) );
     1924
     1925                $response = $this->server->dispatch( $request );
     1926
     1927                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1928
     1929                global $wp_rest_additional_fields;
     1930                $wp_rest_additional_fields = array();
     1931        }
     1932
     1933        public function additional_field_get_callback( $object ) {
     1934                return get_post_meta( $object['id'], 'my_custom_int', true );
     1935        }
     1936
     1937        public function additional_field_update_callback( $value, $post ) {
     1938                if ( 'returnError' === $value ) {
     1939                        return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
     1940                }
     1941                update_post_meta( $post->ID, 'my_custom_int', $value );
     1942        }
     1943
     1944        public function tearDown() {
     1945                _unregister_post_type( 'youseeeme' );
     1946                if ( isset( $this->attachment_id ) ) {
     1947                        $this->remove_added_uploads();
     1948                }
     1949                parent::tearDown();
     1950        }
     1951
     1952        /**
     1953         * Internal function used to disable an insert query which
     1954         * will trigger a wpdb error for testing purposes.
     1955         */
     1956        public function error_insert_query( $query ) {
     1957                if ( strpos( $query, 'INSERT' ) === 0 ) {
     1958                        $query = '],';
     1959                }
     1960                return $query;
     1961        }
     1962
     1963}
  • tests/phpunit/tests/rest-api/rest-request-validation.php

     
     1<?php
     2
     3class WP_Test_REST_Request_Validation extends WP_Test_REST_TestCase {
     4
     5        public function test_validate_within_min_max_range_inclusive() {
     6                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     7                        'args' => array(
     8                                'minmaxrange' => array(
     9                                        'type'    => 'integer',
     10                                        'minimum' => 2,
     11                                        'maximum' => 10,
     12                                ),
     13                        ),
     14                ) );
     15                $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
     16                $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (inclusive)', $ret->get_error_message() );
     17                $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
     18                $this->assertTrue( $ret );
     19                $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
     20                $this->assertTrue( $ret );
     21                $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
     22                $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (inclusive)', $ret->get_error_message() );
     23        }
     24
     25        public function test_validate_within_min_max_range_min_exclusive() {
     26                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     27                        'args' => array(
     28                                'minmaxrange' => array(
     29                                        'type'             => 'integer',
     30                                        'minimum'          => 2,
     31                                        'maximum'          => 10,
     32                                        'exclusiveMinimum' => true,
     33                                ),
     34                        ),
     35                ) );
     36                $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
     37                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (inclusive)', $ret->get_error_message() );
     38                $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
     39                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (inclusive)', $ret->get_error_message() );
     40                $ret = rest_validate_request_arg( 3, $request, 'minmaxrange' );
     41                $this->assertTrue( $ret );
     42                $ret = rest_validate_request_arg( 9, $request, 'minmaxrange' );
     43                $this->assertTrue( $ret );
     44                $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
     45                $this->assertTrue( $ret );
     46                $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
     47                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (inclusive)', $ret->get_error_message() );
     48        }
     49
     50        public function test_validate_within_min_max_range_max_exclusive() {
     51                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     52                        'args' => array(
     53                                'minmaxrange' => array(
     54                                        'type'             => 'integer',
     55                                        'minimum'          => 2,
     56                                        'maximum'          => 10,
     57                                        'exclusiveMaximum' => true,
     58                                ),
     59                        ),
     60                ) );
     61                $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
     62                $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (exclusive)', $ret->get_error_message() );
     63                $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
     64                $this->assertTrue( $ret );
     65                $ret = rest_validate_request_arg( 3, $request, 'minmaxrange' );
     66                $this->assertTrue( $ret );
     67                $ret = rest_validate_request_arg( 9, $request, 'minmaxrange' );
     68                $this->assertTrue( $ret );
     69                $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
     70                $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (exclusive)', $ret->get_error_message() );
     71                $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
     72                $this->assertEquals( 'minmaxrange must be between 2 (inclusive) and 10 (exclusive)', $ret->get_error_message() );
     73        }
     74
     75        public function test_validate_within_min_max_range_both_exclusive() {
     76                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     77                        'args' => array(
     78                                'minmaxrange' => array(
     79                                        'type'             => 'integer',
     80                                        'minimum'          => 2,
     81                                        'maximum'          => 10,
     82                                        'exclusiveMinimum' => true,
     83                                        'exclusiveMaximum' => true,
     84                                ),
     85                        ),
     86                ) );
     87                $ret = rest_validate_request_arg( 1, $request, 'minmaxrange' );
     88                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
     89                $ret = rest_validate_request_arg( 2, $request, 'minmaxrange' );
     90                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
     91                $ret = rest_validate_request_arg( 3, $request, 'minmaxrange' );
     92                $this->assertTrue( $ret );
     93                $ret = rest_validate_request_arg( 9, $request, 'minmaxrange' );
     94                $this->assertTrue( $ret );
     95                $ret = rest_validate_request_arg( 10, $request, 'minmaxrange' );
     96                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
     97                $ret = rest_validate_request_arg( 11, $request, 'minmaxrange' );
     98                $this->assertEquals( 'minmaxrange must be between 2 (exclusive) and 10 (exclusive)', $ret->get_error_message() );
     99        }
     100
     101        public function test_validate_greater_than_min_inclusive() {
     102                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     103                        'args' => array(
     104                                'greaterthanmin' => array(
     105                                        'type'             => 'integer',
     106                                        'minimum'          => 2,
     107                                ),
     108                        ),
     109                ) );
     110                $ret = rest_validate_request_arg( 1, $request, 'greaterthanmin' );
     111                $this->assertEquals( 'greaterthanmin must be greater than 2 (inclusive)', $ret->get_error_message() );
     112                $ret = rest_validate_request_arg( 2, $request, 'greaterthanmin' );
     113                $this->assertTrue( $ret );
     114        }
     115
     116        public function test_validate_greater_than_min_exclusive() {
     117                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     118                        'args' => array(
     119                                'greaterthanmin' => array(
     120                                        'type'             => 'integer',
     121                                        'minimum'          => 2,
     122                                        'exclusiveMinimum' => true,
     123                                ),
     124                        ),
     125                ) );
     126                $ret = rest_validate_request_arg( 1, $request, 'greaterthanmin' );
     127                $this->assertEquals( 'greaterthanmin must be greater than 2 (exclusive)', $ret->get_error_message() );
     128                $ret = rest_validate_request_arg( 2, $request, 'greaterthanmin' );
     129                $this->assertEquals( 'greaterthanmin must be greater than 2 (exclusive)', $ret->get_error_message() );
     130                $ret = rest_validate_request_arg( 3, $request, 'greaterthanmin' );
     131                $this->assertTrue( $ret );
     132        }
     133
     134        public function test_validate_less_than_max_inclusive() {
     135                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     136                        'args' => array(
     137                                'lessthanmax' => array(
     138                                        'type'             => 'integer',
     139                                        'maximum'          => 10,
     140                                ),
     141                        ),
     142                ) );
     143                $ret = rest_validate_request_arg( 11, $request, 'lessthanmax' );
     144                $this->assertEquals( 'lessthanmax must be less than 10 (inclusive)', $ret->get_error_message() );
     145                $ret = rest_validate_request_arg( 10, $request, 'lessthanmax' );
     146                $this->assertTrue( $ret );
     147        }
     148
     149        public function test_validate_less_than_max_exclusive() {
     150                $request = new WP_REST_Request( 'GET', '/wp/v2/foo', array(
     151                        'args' => array(
     152                                'lessthanmax' => array(
     153                                        'type'             => 'integer',
     154                                        'maximum'          => 10,
     155                                        'exclusiveMaximum' => true,
     156                                ),
     157                        ),
     158                ) );
     159                $ret = rest_validate_request_arg( 11, $request, 'lessthanmax' );
     160                $this->assertEquals( 'lessthanmax must be less than 10 (exclusive)', $ret->get_error_message() );
     161                $ret = rest_validate_request_arg( 10, $request, 'lessthanmax' );
     162                $this->assertEquals( 'lessthanmax must be less than 10 (exclusive)', $ret->get_error_message() );
     163                $ret = rest_validate_request_arg( 9, $request, 'lessthanmax' );
     164                $this->assertTrue( $ret );
     165        }
     166
     167}
  • tests/phpunit/tests/rest-api/rest-revisions-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Revisions_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9 /**
     10  * @group restapi
     11  */
     12class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        public function setUp() {
     15                parent::setUp();
     16                $this->post_id = $this->factory->post->create();
     17                $this->page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );
     18
     19                $this->editor_id = $this->factory->user->create( array(
     20                        'role' => 'editor',
     21                ) );
     22                $this->contributor_id = $this->factory->user->create( array(
     23                        'role' => 'contributor',
     24                ) );
     25
     26                wp_update_post( array( 'post_content' => 'This content is better.', 'ID' => $this->post_id ) );
     27                wp_update_post( array( 'post_content' => 'This content is marvelous.', 'ID' => $this->post_id ) );
     28                $revisions = wp_get_post_revisions( $this->post_id );
     29                $this->revision_1 = array_pop( $revisions );
     30                $this->revision_id1 = $this->revision_1->ID;
     31                $this->revision_2 = array_pop( $revisions );
     32                $this->revision_id2 = $this->revision_2->ID;
     33        }
     34
     35        public function test_register_routes() {
     36                $routes = $this->server->get_routes();
     37                $this->assertArrayHasKey( '/wp/v2/posts/(?P<parent>[\d]+)/revisions', $routes );
     38                $this->assertArrayHasKey( '/wp/v2/posts/(?P<parent>[\d]+)/revisions/(?P<id>[\d]+)', $routes );
     39                $this->assertArrayHasKey( '/wp/v2/pages/(?P<parent>[\d]+)/revisions', $routes );
     40                $this->assertArrayHasKey( '/wp/v2/pages/(?P<parent>[\d]+)/revisions/(?P<id>[\d]+)', $routes );
     41        }
     42
     43        public function test_context_param() {
     44                // Collection
     45                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions' );
     46                $response = $this->server->dispatch( $request );
     47                $data = $response->get_data();
     48                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     49                $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
     50                // Single
     51                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_1->ID );
     52                $response = $this->server->dispatch( $request );
     53                $data = $response->get_data();
     54                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     55                $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
     56        }
     57
     58        public function test_get_items() {
     59                wp_set_current_user( $this->editor_id );
     60                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions' );
     61                $response = $this->server->dispatch( $request );
     62                $data = $response->get_data();
     63                $this->assertEquals( 200, $response->get_status() );
     64                $this->assertCount( 2, $data );
     65
     66                // Reverse chron
     67                $this->assertEquals( $this->revision_id2, $data[0]['id'] );
     68                $this->check_get_revision_response( $data[0], $this->revision_2 );
     69
     70                $this->assertEquals( $this->revision_id1, $data[1]['id'] );
     71                $this->check_get_revision_response( $data[1], $this->revision_1 );
     72        }
     73
     74        public function test_get_items_no_permission() {
     75                wp_set_current_user( 0 );
     76                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions' );
     77                $response = $this->server->dispatch( $request );
     78
     79                $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
     80                wp_set_current_user( $this->contributor_id );
     81                $response = $this->server->dispatch( $request );
     82                $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
     83        }
     84
     85        public function test_get_items_missing_parent() {
     86                wp_set_current_user( $this->editor_id );
     87                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' );
     88                $response = $this->server->dispatch( $request );
     89                $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
     90        }
     91
     92        public function test_get_items_invalid_parent_post_type() {
     93                wp_set_current_user( $this->editor_id );
     94                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->page_id . '/revisions' );
     95                $response = $this->server->dispatch( $request );
     96                $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
     97        }
     98
     99        public function test_get_item() {
     100                wp_set_current_user( $this->editor_id );
     101                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     102                $response = $this->server->dispatch( $request );
     103                $this->assertEquals( 200, $response->get_status() );
     104                $this->check_get_revision_response( $response, $this->revision_1 );
     105                $fields = array(
     106                        'author',
     107                        'date',
     108                        'date_gmt',
     109                        'modified',
     110                        'modified_gmt',
     111                        'guid',
     112                        'id',
     113                        'parent',
     114                        'slug',
     115                        'title',
     116                        'excerpt',
     117                        'content',
     118                );
     119                $data = $response->get_data();
     120                $this->assertEqualSets( $fields, array_keys( $data ) );
     121        }
     122
     123        public function test_get_item_embed_context() {
     124                wp_set_current_user( $this->editor_id );
     125                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     126                $request->set_param( 'context', 'embed' );
     127                $response = $this->server->dispatch( $request );
     128                $fields = array(
     129                        'author',
     130                        'date',
     131                        'id',
     132                        'parent',
     133                        'slug',
     134                        'title',
     135                        'excerpt',
     136                );
     137                $data = $response->get_data();
     138                $this->assertEqualSets( $fields, array_keys( $data ) );
     139        }
     140
     141        public function test_get_item_no_permission() {
     142                wp_set_current_user( 0 );
     143                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     144
     145                $response = $this->server->dispatch( $request );
     146                $this->assertErrorResponse( 'rest_cannot_read', $response, 401 );
     147                wp_set_current_user( $this->contributor_id );
     148                $response = $this->server->dispatch( $request );
     149                $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
     150        }
     151
     152        public function test_get_item_missing_parent() {
     153                wp_set_current_user( $this->editor_id );
     154                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 );
     155                $response = $this->server->dispatch( $request );
     156                $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
     157        }
     158
     159        public function test_get_item_invalid_parent_post_type() {
     160                wp_set_current_user( $this->editor_id );
     161                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->page_id . '/revisions/' . $this->revision_id1 );
     162                $response = $this->server->dispatch( $request );
     163                $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 );
     164        }
     165
     166        public function test_delete_item() {
     167                wp_set_current_user( $this->editor_id );
     168                $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     169                $response = $this->server->dispatch( $request );
     170                $this->assertEquals( 200, $response->get_status() );
     171                $this->assertNull( get_post( $this->revision_id1 ) );
     172        }
     173
     174        public function test_delete_item_no_permission() {
     175                wp_set_current_user( $this->contributor_id );
     176                $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     177                $response = $this->server->dispatch( $request );
     178                $this->assertErrorResponse( 'rest_cannot_read', $response, 403 );
     179        }
     180
     181        public function test_prepare_item() {
     182                wp_set_current_user( $this->editor_id );
     183                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     184                $response = $this->server->dispatch( $request );
     185                $this->assertEquals( 200, $response->get_status() );
     186                $this->check_get_revision_response( $response, $this->revision_1 );
     187        }
     188
     189        public function test_get_item_schema() {
     190                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions' );
     191                $response = $this->server->dispatch( $request );
     192                $data = $response->get_data();
     193                $properties = $data['schema']['properties'];
     194                $this->assertEquals( 12, count( $properties ) );
     195                $this->assertArrayHasKey( 'author', $properties );
     196                $this->assertArrayHasKey( 'content', $properties );
     197                $this->assertArrayHasKey( 'date', $properties );
     198                $this->assertArrayHasKey( 'date_gmt', $properties );
     199                $this->assertArrayHasKey( 'excerpt', $properties );
     200                $this->assertArrayHasKey( 'guid', $properties );
     201                $this->assertArrayHasKey( 'id', $properties );
     202                $this->assertArrayHasKey( 'modified', $properties );
     203                $this->assertArrayHasKey( 'modified_gmt', $properties );
     204                $this->assertArrayHasKey( 'parent', $properties );
     205                $this->assertArrayHasKey( 'slug', $properties );
     206                $this->assertArrayHasKey( 'title', $properties );
     207        }
     208
     209        public function test_create_item() {
     210                $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $this->post_id . '/revisions' );
     211                $response = $this->server->dispatch( $request );
     212                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     213        }
     214
     215        public function test_update_item() {
     216                $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     217                $response = $this->server->dispatch( $request );
     218                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     219        }
     220
     221        public function test_get_additional_field_registration() {
     222
     223                $schema = array(
     224                        'type'        => 'integer',
     225                        'description' => 'Some integer of mine',
     226                        'enum'        => array( 1, 2, 3, 4 ),
     227                        'context'     => array( 'view', 'edit' ),
     228                );
     229
     230                register_rest_field( 'post-revision', 'my_custom_int', array(
     231                        'schema'          => $schema,
     232                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     233                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     234                ) );
     235
     236                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . $this->post_id . '/revisions' );
     237
     238                $response = $this->server->dispatch( $request );
     239                $data = $response->get_data();
     240
     241                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     242                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     243
     244                wp_set_current_user( 1 );
     245
     246                $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->post_id . '/revisions/' . $this->revision_id1 );
     247
     248                $response = $this->server->dispatch( $request );
     249                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     250
     251                global $wp_rest_additional_fields;
     252                $wp_rest_additional_fields = array();
     253        }
     254
     255        public function additional_field_get_callback( $object ) {
     256                return get_post_meta( $object['id'], 'my_custom_int', true );
     257        }
     258
     259        public function additional_field_update_callback( $value, $post ) {
     260                update_post_meta( $post->ID, 'my_custom_int', $value );
     261        }
     262
     263        protected function check_get_revision_response( $response, $revision ) {
     264                if ( $response instanceof WP_REST_Response ) {
     265                        $links = $response->get_links();
     266                        $response = $response->get_data();
     267                } else {
     268                        $this->assertArrayHasKey( '_links', $response );
     269                        $links = $response['_links'];
     270                }
     271
     272                $this->assertEquals( $revision->post_author, $response['author'] );
     273
     274                $rendered_content = apply_filters( 'the_content', $revision->post_content );
     275                $this->assertEquals( $rendered_content, $response['content']['rendered'] );
     276
     277                $this->assertEquals( mysql_to_rfc3339( $revision->post_date ), $response['date'] );
     278                $this->assertEquals( mysql_to_rfc3339( $revision->post_date_gmt ), $response['date_gmt'] );
     279
     280                $rendered_excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $revision->post_excerpt, $revision ) );
     281                $this->assertEquals( $rendered_excerpt, $response['excerpt']['rendered'] );
     282
     283                $rendered_guid = apply_filters( 'get_the_guid', $revision->guid );
     284                $this->assertEquals( $rendered_guid, $response['guid']['rendered'] );
     285
     286                $this->assertEquals( $revision->ID, $response['id'] );
     287                $this->assertEquals( mysql_to_rfc3339( $revision->post_modified ), $response['modified'] );
     288                $this->assertEquals( mysql_to_rfc3339( $revision->post_modified_gmt ), $response['modified_gmt'] );
     289                $this->assertEquals( $revision->post_name, $response['slug'] );
     290
     291                $rendered_title = get_the_title( $revision->ID );
     292                $this->assertEquals( $rendered_title, $response['title']['rendered'] );
     293
     294                $parent = get_post( $revision->post_parent );
     295                $parent_controller = new WP_REST_Posts_Controller( $parent->post_type );
     296                $parent_object = get_post_type_object( $parent->post_type );
     297                $parent_base = ! empty( $parent_object->rest_base ) ? $parent_object->rest_base : $parent_object->name;
     298                $this->assertEquals( rest_url( '/wp/v2/' . $parent_base . '/' . $revision->post_parent ), $links['parent'][0]['href'] );
     299        }
     300
     301}
  • tests/phpunit/tests/rest-api/rest-settings-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_Test_REST_Settings_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        public function setUp() {
     15                global $wp_version;
     16                if ( version_compare( $wp_version, '4.7-alpha', '<' ) ) {
     17                        return $this->markTestSkipped( 'WordPress version not supported.' );
     18                }
     19                parent::setUp();
     20                $this->administrator = $this->factory->user->create( array(
     21                        'role' => 'administrator',
     22                ) );
     23                $this->endpoint = new WP_REST_Settings_Controller();
     24        }
     25
     26        public function test_register_routes() {
     27                $routes = $this->server->get_routes();
     28                $this->assertArrayHasKey( '/wp/v2/settings', $routes );
     29        }
     30
     31        public function test_get_items() {
     32        }
     33
     34        public function test_context_param() {
     35        }
     36
     37        public function test_get_item_is_not_public() {
     38                $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     39                $response = $this->server->dispatch( $request );
     40                $this->assertEquals( 403, $response->get_status() );
     41        }
     42
     43        public function test_get_item() {
     44                wp_set_current_user( $this->administrator );
     45                $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     46                $response = $this->server->dispatch( $request );
     47                $data = $response->get_data();
     48
     49                $this->assertEquals( 200, $response->get_status() );
     50                $this->assertEquals( array(
     51                        'title',
     52                        'description',
     53                        'url',
     54                        'email',
     55                        'timezone',
     56                        'date_format',
     57                        'time_format',
     58                        'start_of_week',
     59                        'language',
     60                        'use_smilies',
     61                        'default_category',
     62                        'default_post_format',
     63                        'posts_per_page',
     64                ), array_keys( $data ) );
     65        }
     66
     67        public function test_get_item_value_is_cast_to_type() {
     68                wp_set_current_user( $this->administrator );
     69                update_option( 'posts_per_page', 'invalid_number' ); // this is cast to (int) 1
     70                $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     71                $response = $this->server->dispatch( $request );
     72                $data = $response->get_data();
     73
     74                $this->assertEquals( 200, $response->get_status() );
     75                $this->assertEquals( 1, $data['posts_per_page'] );
     76        }
     77
     78        public function test_get_item_with_custom_setting() {
     79                wp_set_current_user( $this->administrator );
     80
     81                register_setting( 'somegroup', 'mycustomsetting', array(
     82                        'show_in_rest' => array(
     83                                'name'   => 'mycustomsettinginrest',
     84                                'schema' => array(
     85                                        'enum'    => array( 'validvalue1', 'validvalue2' ),
     86                                        'default' => 'validvalue1',
     87                                ),
     88                        ),
     89                        'type'         => 'string',
     90                ) );
     91
     92                $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     93                $response = $this->server->dispatch( $request );
     94                $data = $response->get_data();
     95
     96                $this->assertEquals( 200, $response->get_status() );
     97                $this->assertArrayHasKey( 'mycustomsettinginrest', $data );
     98                $this->assertEquals( 'validvalue1', $data['mycustomsettinginrest'] );
     99
     100                update_option( 'mycustomsetting', 'validvalue2' );
     101
     102                $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     103                $response = $this->server->dispatch( $request );
     104                $data = $response->get_data();
     105                $this->assertEquals( 'validvalue2', $data['mycustomsettinginrest'] );
     106
     107                unregister_setting( 'somegroup', 'mycustomsetting' );
     108        }
     109
     110        public function get_setting_custom_callback( $result, $name, $args ) {
     111                switch ( $name ) {
     112                        case 'mycustomsetting1':
     113                                return 'filtered1';
     114                }
     115                return $result;
     116        }
     117
     118        public function test_get_item_with_filter() {
     119                wp_set_current_user( $this->administrator );
     120
     121                add_filter( 'rest_pre_get_setting', array( $this, 'get_setting_custom_callback' ), 10, 3 );
     122
     123                register_setting( 'somegroup', 'mycustomsetting1', array(
     124                        'show_in_rest' => array(
     125                                'name'   => 'mycustomsettinginrest1',
     126                        ),
     127                        'type'         => 'string',
     128                ) );
     129
     130                register_setting( 'somegroup', 'mycustomsetting2', array(
     131                        'show_in_rest' => array(
     132                                'name'   => 'mycustomsettinginrest2',
     133                        ),
     134                        'type'         => 'string',
     135                ) );
     136
     137                update_option( 'mycustomsetting1', 'unfiltered1' );
     138                update_option( 'mycustomsetting2', 'unfiltered2' );
     139
     140                $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     141                $response = $this->server->dispatch( $request );
     142                $data = $response->get_data();
     143
     144                $this->assertEquals( 200, $response->get_status() );
     145
     146                $this->assertArrayHasKey( 'mycustomsettinginrest1', $data );
     147                $this->assertEquals( 'unfiltered1', $data['mycustomsettinginrest1'] );
     148
     149                $this->assertArrayHasKey( 'mycustomsettinginrest2', $data );
     150                $this->assertEquals( 'unfiltered2', $data['mycustomsettinginrest2'] );
     151
     152                unregister_setting( 'somegroup', 'mycustomsetting' );
     153                remove_all_filters( 'rest_pre_get_setting' );
     154        }
     155
     156        public function test_create_item() {
     157        }
     158
     159        public function test_update_item() {
     160                wp_set_current_user( $this->administrator );
     161                $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     162                $request->set_param( 'title', 'The new title!' );
     163                $response = $this->server->dispatch( $request );
     164                $data = $response->get_data();
     165
     166                $this->assertEquals( 200, $response->get_status() );
     167                $this->assertEquals( 'The new title!', $data['title'] );
     168                $this->assertEquals( get_option( 'blogname' ), $data['title'] );
     169        }
     170
     171        public function update_setting_custom_callback( $result, $name, $value, $args ) {
     172                if ( 'title' === $name && 'The new title!' === $value ) {
     173                        // Do not allow changing the title in this case
     174                        return true;
     175                }
     176
     177                return false;
     178        }
     179
     180        public function test_update_item_with_filter() {
     181                wp_set_current_user( $this->administrator );
     182
     183                $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     184                $request->set_param( 'title', 'The old title!' );
     185                $request->set_param( 'description', 'The old description!' );
     186                $response = $this->server->dispatch( $request );
     187                $data = $response->get_data();
     188                $this->assertEquals( 200, $response->get_status() );
     189                $this->assertEquals( 'The old title!', $data['title'] );
     190                $this->assertEquals( 'The old description!', $data['description'] );
     191                $this->assertEquals( get_option( 'blogname' ), $data['title'] );
     192                $this->assertEquals( get_option( 'blogdescription' ), $data['description'] );
     193
     194                add_filter( 'rest_pre_update_setting', array( $this, 'update_setting_custom_callback' ), 10, 4 );
     195
     196                $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     197                $request->set_param( 'title', 'The new title!' );
     198                $request->set_param( 'description', 'The new description!' );
     199                $response = $this->server->dispatch( $request );
     200                $data = $response->get_data();
     201
     202                $this->assertEquals( 200, $response->get_status() );
     203                $this->assertEquals( 'The old title!', $data['title'] );
     204                $this->assertEquals( 'The new description!', $data['description'] );
     205                $this->assertEquals( get_option( 'blogname' ), $data['title'] );
     206                $this->assertEquals( get_option( 'blogdescription' ), $data['description'] );
     207
     208                remove_all_filters( 'rest_pre_update_setting' );
     209        }
     210
     211        public function test_update_item_with_invalid_type() {
     212                wp_set_current_user( $this->administrator );
     213                $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     214                $request->set_param( 'title', array( 'rendered' => 'This should fail.' ) );
     215                $response = $this->server->dispatch( $request );
     216                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     217        }
     218
     219        /**
     220         * Setting an item to "null" will essentially restore it to it's default value.
     221         */
     222        public function test_update_item_with_null() {
     223                update_option( 'posts_per_page', 9 );
     224
     225                wp_set_current_user( $this->administrator );
     226                $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     227                $request->set_param( 'posts_per_page', null );
     228                $response = $this->server->dispatch( $request );
     229                $data = $response->get_data();
     230
     231                $this->assertEquals( 200, $response->get_status() );
     232                $this->assertEquals( 10, $data['posts_per_page'] );
     233        }
     234
     235        public function test_delete_item() {
     236        }
     237
     238        public function test_prepare_item() {
     239        }
     240
     241        public function test_get_item_schema() {
     242        }
     243}
  • tests/phpunit/tests/rest-api/rest-tags-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Terms_Controller functionality, used for Tags.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Tags_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        public function setUp() {
     15                parent::setUp();
     16                $this->administrator = $this->factory->user->create( array(
     17                        'role' => 'administrator',
     18                ) );
     19                $this->subscriber = $this->factory->user->create( array(
     20                        'role' => 'subscriber',
     21                ) );
     22        }
     23
     24        public function test_register_routes() {
     25                $routes = $this->server->get_routes();
     26                $this->assertArrayHasKey( '/wp/v2/tags', $routes );
     27                $this->assertArrayHasKey( '/wp/v2/tags/(?P<id>[\d]+)', $routes );
     28        }
     29
     30        public function test_context_param() {
     31                // Collection
     32                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
     33                $response = $this->server->dispatch( $request );
     34                $data = $response->get_data();
     35                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     36                $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     37                // Single
     38                $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) );
     39                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags/' . $tag1 );
     40                $response = $this->server->dispatch( $request );
     41                $data = $response->get_data();
     42                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     43                $this->assertEqualSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     44        }
     45
     46        public function test_registered_query_params() {
     47                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
     48                $response = $this->server->dispatch( $request );
     49                $data = $response->get_data();
     50                $keys = array_keys( $data['endpoints'][0]['args'] );
     51                sort( $keys );
     52                $this->assertEquals( array(
     53                        'context',
     54                        'exclude',
     55                        'hide_empty',
     56                        'include',
     57                        'offset',
     58                        'order',
     59                        'orderby',
     60                        'page',
     61                        'per_page',
     62                        'post',
     63                        'search',
     64                        'slug',
     65                        ), $keys );
     66        }
     67
     68        public function test_get_items() {
     69                $this->factory->tag->create();
     70                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     71                $response = $this->server->dispatch( $request );
     72                $this->check_get_taxonomy_terms_response( $response );
     73        }
     74
     75        public function test_get_items_invalid_permission_for_context() {
     76                wp_set_current_user( 0 );
     77                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     78                $request->set_param( 'context', 'edit' );
     79                $response = $this->server->dispatch( $request );
     80                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     81        }
     82
     83        public function test_get_items_hide_empty_arg() {
     84                $post_id = $this->factory->post->create();
     85                $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) );
     86                $tag2 = $this->factory->tag->create( array( 'name' => 'The Be Sharps' ) );
     87                wp_set_object_terms( $post_id, array( $tag1, $tag2 ), 'post_tag' );
     88                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     89                $request->set_param( 'hide_empty', true );
     90                $response = $this->server->dispatch( $request );
     91                $data = $response->get_data();
     92                $this->assertEquals( 2, count( $data ) );
     93                $this->assertEquals( 'Season 5', $data[0]['name'] );
     94                $this->assertEquals( 'The Be Sharps', $data[1]['name'] );
     95        }
     96
     97        public function test_get_items_include_query() {
     98                $id1 = $this->factory->tag->create();
     99                $id2 = $this->factory->tag->create();
     100                $id3 = $this->factory->tag->create();
     101                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     102                // Orderby=>asc
     103                $request->set_param( 'include', array( $id3, $id1 ) );
     104                $response = $this->server->dispatch( $request );
     105                $data = $response->get_data();
     106                $this->assertEquals( 2, count( $data ) );
     107                $this->assertEquals( $id1, $data[0]['id'] );
     108                // Orderby=>include
     109                $request->set_param( 'orderby', 'include' );
     110                $response = $this->server->dispatch( $request );
     111                $data = $response->get_data();
     112                $this->assertEquals( 2, count( $data ) );
     113                $this->assertEquals( $id3, $data[0]['id'] );
     114        }
     115
     116        public function test_get_items_exclude_query() {
     117                $id1 = $this->factory->tag->create();
     118                $id2 = $this->factory->tag->create();
     119                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     120                $response = $this->server->dispatch( $request );
     121                $data = $response->get_data();
     122                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     123                $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     124                $request->set_param( 'exclude', array( $id2 ) );
     125                $response = $this->server->dispatch( $request );
     126                $data = $response->get_data();
     127                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     128                $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     129        }
     130
     131        public function test_get_items_offset_query() {
     132                $id1 = $this->factory->tag->create();
     133                $id2 = $this->factory->tag->create();
     134                $id3 = $this->factory->tag->create();
     135                $id4 = $this->factory->tag->create();
     136                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     137                $request->set_param( 'offset', 1 );
     138                $response = $this->server->dispatch( $request );
     139                $this->assertCount( 3, $response->get_data() );
     140                // 'offset' works with 'per_page'
     141                $request->set_param( 'per_page', 2 );
     142                $response = $this->server->dispatch( $request );
     143                $this->assertCount( 2, $response->get_data() );
     144                // 'offset' takes priority over 'page'
     145                $request->set_param( 'page', 3 );
     146                $response = $this->server->dispatch( $request );
     147                $this->assertCount( 2, $response->get_data() );
     148        }
     149
     150
     151        public function test_get_items_orderby_args() {
     152                $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
     153                $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
     154                /*
     155                 * Tests:
     156                 * - orderby
     157                 * - order
     158                 * - per_page
     159                 */
     160                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     161                $request->set_param( 'orderby', 'name' );
     162                $request->set_param( 'order', 'desc' );
     163                $request->set_param( 'per_page', 1 );
     164                $response = $this->server->dispatch( $request );
     165                $this->assertEquals( 200, $response->get_status() );
     166                $data = $response->get_data();
     167                $this->assertEquals( 1, count( $data ) );
     168                $this->assertEquals( 'Banana', $data[0]['name'] );
     169                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     170                $request->set_param( 'orderby', 'name' );
     171                $request->set_param( 'order', 'asc' );
     172                $request->set_param( 'per_page', 2 );
     173                $response = $this->server->dispatch( $request );
     174                $this->assertEquals( 200, $response->get_status() );
     175                $data = $response->get_data();
     176                $this->assertEquals( 2, count( $data ) );
     177                $this->assertEquals( 'Apple', $data[0]['name'] );
     178        }
     179
     180        public function test_get_items_orderby_id() {
     181                $tag0 = $this->factory->tag->create( array( 'name' => 'Cantaloupe' ) );
     182                $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
     183                $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
     184                // defaults to orderby=name, order=asc
     185                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     186                $response = $this->server->dispatch( $request );
     187                $this->assertEquals( 200, $response->get_status() );
     188                $data = $response->get_data();
     189                $this->assertEquals( 'Apple', $data[0]['name'] );
     190                $this->assertEquals( 'Banana', $data[1]['name'] );
     191                $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
     192                // orderby=id, with default order=asc
     193                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     194                $request->set_param( 'orderby', 'id' );
     195                $response = $this->server->dispatch( $request );
     196                $this->assertEquals( 200, $response->get_status() );
     197                $data = $response->get_data();
     198                $this->assertEquals( 'Cantaloupe', $data[0]['name'] );
     199                $this->assertEquals( 'Apple', $data[1]['name'] );
     200                $this->assertEquals( 'Banana', $data[2]['name'] );
     201                // orderby=id, order=desc
     202                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     203                $request->set_param( 'orderby', 'id' );
     204                $request->set_param( 'order', 'desc' );
     205                $response = $this->server->dispatch( $request );
     206                $data = $response->get_data();
     207                $this->assertEquals( 200, $response->get_status() );
     208                $this->assertEquals( 'Banana', $data[0]['name'] );
     209                $this->assertEquals( 'Apple', $data[1]['name'] );
     210                $this->assertEquals( 'Cantaloupe', $data[2]['name'] );
     211        }
     212
     213        public function test_get_items_post_args() {
     214                $post_id = $this->factory->post->create();
     215                $tag1 = $this->factory->tag->create( array( 'name' => 'DC' ) );
     216                $tag2 = $this->factory->tag->create( array( 'name' => 'Marvel' ) );
     217                $this->factory->tag->create( array( 'name' => 'Dark Horse' ) );
     218                wp_set_object_terms( $post_id, array( $tag1, $tag2 ), 'post_tag' );
     219
     220                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     221                $request->set_param( 'post', $post_id );
     222                $response = $this->server->dispatch( $request );
     223                $this->assertEquals( 200, $response->get_status() );
     224
     225                $data = $response->get_data();
     226                $this->assertEquals( 2, count( $data ) );
     227                $this->assertEquals( 'DC', $data[0]['name'] );
     228        }
     229
     230        public function test_get_terms_post_args_paging() {
     231                $post_id = $this->factory->post->create();
     232                $tag_ids = array();
     233
     234                for ( $i = 0; $i < 30; $i++ ) {
     235                        $tag_ids[] = $this->factory->tag->create( array(
     236                                'name'   => "Tag {$i}",
     237                        ) );
     238                }
     239                wp_set_object_terms( $post_id, $tag_ids, 'post_tag' );
     240
     241                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     242                $request->set_param( 'post', $post_id );
     243                $request->set_param( 'page', 1 );
     244                $request->set_param( 'per_page', 15 );
     245                $request->set_param( 'orderby', 'id' );
     246                $response = $this->server->dispatch( $request );
     247                $tags = $response->get_data();
     248
     249                $i = 0;
     250                foreach ( $tags as $tag ) {
     251                        $this->assertEquals( $tag['name'], "Tag {$i}" );
     252                        $i++;
     253                }
     254
     255                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     256                $request->set_param( 'post', $post_id );
     257                $request->set_param( 'page', 2 );
     258                $request->set_param( 'per_page', 15 );
     259                $request->set_param( 'orderby', 'id' );
     260                $response = $this->server->dispatch( $request );
     261                $tags = $response->get_data();
     262
     263                foreach ( $tags as $tag ) {
     264                        $this->assertEquals( $tag['name'], "Tag {$i}" );
     265                        $i++;
     266                }
     267        }
     268
     269        public function test_get_items_post_empty() {
     270                $post_id = $this->factory->post->create();
     271
     272                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     273                $request->set_param( 'post', $post_id );
     274                $response = $this->server->dispatch( $request );
     275                $this->assertEquals( 200, $response->get_status() );
     276
     277                $data = $response->get_data();
     278                $this->assertCount( 0, $data );
     279        }
     280
     281        public function test_get_items_custom_tax_post_args() {
     282                register_taxonomy( 'batman', 'post', array( 'show_in_rest' => true ) );
     283                $controller = new WP_REST_Terms_Controller( 'batman' );
     284                $controller->register_routes();
     285                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'batman' ) );
     286                $term2 = $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'batman' ) );
     287                $this->factory->term->create( array( 'name' => 'Car', 'taxonomy' => 'batman' ) );
     288                $post_id = $this->factory->post->create();
     289                wp_set_object_terms( $post_id, array( $term1, $term2 ), 'batman' );
     290
     291                $request = new WP_REST_Request( 'GET', '/wp/v2/batman' );
     292                $request->set_param( 'post', $post_id );
     293                $response = $this->server->dispatch( $request );
     294                $this->assertEquals( 200, $response->get_status() );
     295
     296                $data = $response->get_data();
     297                $this->assertEquals( 2, count( $data ) );
     298                $this->assertEquals( 'Cape', $data[0]['name'] );
     299        }
     300
     301        public function test_get_items_search_args() {
     302                $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
     303                $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
     304                /*
     305                 * Tests:
     306                 * - search
     307                 */
     308                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     309                $request->set_param( 'search', 'App' );
     310                $response = $this->server->dispatch( $request );
     311                $this->assertEquals( 200, $response->get_status() );
     312                $data = $response->get_data();
     313                $this->assertEquals( 1, count( $data ) );
     314                $this->assertEquals( 'Apple', $data[0]['name'] );
     315                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     316                $request->set_param( 'search', 'Garbage' );
     317                $response = $this->server->dispatch( $request );
     318                $this->assertEquals( 200, $response->get_status() );
     319                $data = $response->get_data();
     320                $this->assertEquals( 0, count( $data ) );
     321        }
     322
     323        public function test_get_items_slug_arg() {
     324                $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) );
     325                $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) );
     326                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     327                $request->set_param( 'slug', 'apple' );
     328                $response = $this->server->dispatch( $request );
     329                $this->assertEquals( 200, $response->get_status() );
     330                $data = $response->get_data();
     331                $this->assertEquals( 1, count( $data ) );
     332                $this->assertEquals( 'Apple', $data[0]['name'] );
     333        }
     334
     335        public function test_get_terms_private_taxonomy() {
     336                register_taxonomy( 'robin', 'post', array( 'public' => false ) );
     337                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
     338                $term2 = $this->factory->term->create( array( 'name' => 'Mask', 'taxonomy' => 'robin' ) );
     339
     340                $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin' );
     341                $response = $this->server->dispatch( $request );
     342                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     343        }
     344
     345        public function test_get_terms_pagination_headers() {
     346                // Start of the index
     347                for ( $i = 0; $i < 50; $i++ ) {
     348                        $this->factory->tag->create( array(
     349                                'name'   => "Tag {$i}",
     350                                ) );
     351                }
     352                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     353                $response = $this->server->dispatch( $request );
     354                $headers = $response->get_headers();
     355                $this->assertEquals( 50, $headers['X-WP-Total'] );
     356                $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
     357                $next_link = add_query_arg( array(
     358                        'page'    => 2,
     359                        ), rest_url( 'wp/v2/tags' ) );
     360                $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
     361                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     362                // 3rd page
     363                $this->factory->tag->create( array(
     364                                'name'   => 'Tag 51',
     365                                ) );
     366                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     367                $request->set_param( 'page', 3 );
     368                $response = $this->server->dispatch( $request );
     369                $headers = $response->get_headers();
     370                $this->assertEquals( 51, $headers['X-WP-Total'] );
     371                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     372                $prev_link = add_query_arg( array(
     373                        'page'    => 2,
     374                        ), rest_url( 'wp/v2/tags' ) );
     375                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     376                $next_link = add_query_arg( array(
     377                        'page'    => 4,
     378                        ), rest_url( 'wp/v2/tags' ) );
     379                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     380                // Last page
     381                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     382                $request->set_param( 'page', 6 );
     383                $response = $this->server->dispatch( $request );
     384                $headers = $response->get_headers();
     385                $this->assertEquals( 51, $headers['X-WP-Total'] );
     386                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     387                $prev_link = add_query_arg( array(
     388                        'page'    => 5,
     389                        ), rest_url( 'wp/v2/tags' ) );
     390                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     391                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     392                // Out of bounds
     393                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     394                $request->set_param( 'page', 8 );
     395                $response = $this->server->dispatch( $request );
     396                $headers = $response->get_headers();
     397                $this->assertEquals( 51, $headers['X-WP-Total'] );
     398                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     399                $prev_link = add_query_arg( array(
     400                        'page'    => 6,
     401                        ), rest_url( 'wp/v2/tags' ) );
     402                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     403                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     404        }
     405
     406        public function test_get_items_invalid_context() {
     407                $request = new WP_REST_Request( 'GET', '/wp/v2/tags' );
     408                $request->set_param( 'context', 'banana' );
     409                $response = $this->server->dispatch( $request );
     410                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     411        }
     412
     413        public function test_get_item() {
     414                $id = $this->factory->tag->create();
     415                $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id );
     416                $response = $this->server->dispatch( $request );
     417                $this->check_get_taxonomy_term_response( $response, $id );
     418        }
     419
     420        public function test_get_term_invalid_term() {
     421                $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     422                $response = $this->server->dispatch( $request );
     423                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     424        }
     425
     426        public function test_get_item_invalid_permission_for_context() {
     427                $id = $this->factory->tag->create();
     428                wp_set_current_user( 0 );
     429                $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id );
     430                $request->set_param( 'context', 'edit' );
     431                $response = $this->server->dispatch( $request );
     432                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     433        }
     434
     435        public function test_get_term_private_taxonomy() {
     436                register_taxonomy( 'robin', 'post', array( 'public' => false ) );
     437                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
     438
     439                $request = new WP_REST_Request( 'GET', '/wp/v2/terms/robin/' . $term1 );
     440                $response = $this->server->dispatch( $request );
     441                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     442        }
     443
     444        public function test_get_item_incorrect_taxonomy() {
     445                register_taxonomy( 'robin', 'post' );
     446                $term1 = $this->factory->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin' ) );
     447                $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $term1 );
     448                $response = $this->server->dispatch( $request );
     449                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     450        }
     451
     452        public function test_create_item() {
     453                wp_set_current_user( $this->administrator );
     454                $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
     455                $request->set_param( 'name', 'My Awesome Term' );
     456                $request->set_param( 'description', 'This term is so awesome.' );
     457                $request->set_param( 'slug', 'so-awesome' );
     458                $response = $this->server->dispatch( $request );
     459                $this->assertEquals( 201, $response->get_status() );
     460                $headers = $response->get_headers();
     461                $data = $response->get_data();
     462                $this->assertContains( '/wp/v2/tags/' . $data['id'], $headers['Location'] );
     463                $this->assertEquals( 'My Awesome Term', $data['name'] );
     464                $this->assertEquals( 'This term is so awesome.', $data['description'] );
     465                $this->assertEquals( 'so-awesome', $data['slug'] );
     466        }
     467
     468        public function test_create_item_incorrect_permissions() {
     469                wp_set_current_user( $this->subscriber );
     470                $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
     471                $request->set_param( 'name', 'Incorrect permissions' );
     472                $response = $this->server->dispatch( $request );
     473                $this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
     474        }
     475
     476        public function test_create_item_missing_arguments() {
     477                wp_set_current_user( $this->administrator );
     478                $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
     479                $response = $this->server->dispatch( $request );
     480                $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 );
     481        }
     482
     483        public function test_create_item_parent_non_hierarchical_taxonomy() {
     484                wp_set_current_user( $this->administrator );
     485
     486                $request = new WP_REST_Request( 'POST', '/wp/v2/tags' );
     487                $request->set_param( 'name', 'My Awesome Term' );
     488                $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     489                $response = $this->server->dispatch( $request );
     490                $this->assertErrorResponse( 'rest_taxonomy_not_hierarchical', $response, 400 );
     491        }
     492
     493        public function test_update_item() {
     494                wp_set_current_user( $this->administrator );
     495                $orig_args = array(
     496                        'name'        => 'Original Name',
     497                        'description' => 'Original Description',
     498                        'slug'        => 'original-slug',
     499                        );
     500                $term = get_term_by( 'id', $this->factory->tag->create( $orig_args ), 'post_tag' );
     501                $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id );
     502                $request->set_param( 'name', 'New Name' );
     503                $request->set_param( 'description', 'New Description' );
     504                $request->set_param( 'slug', 'new-slug' );
     505                $response = $this->server->dispatch( $request );
     506                $this->assertEquals( 200, $response->get_status() );
     507                $data = $response->get_data();
     508                $this->assertEquals( 'New Name', $data['name'] );
     509                $this->assertEquals( 'New Description', $data['description'] );
     510                $this->assertEquals( 'new-slug', $data['slug'] );
     511        }
     512
     513        public function test_update_item_invalid_term() {
     514                wp_set_current_user( $this->administrator );
     515                $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     516                $request->set_param( 'name', 'Invalid Term' );
     517                $response = $this->server->dispatch( $request );
     518                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     519        }
     520
     521        public function test_update_item_incorrect_permissions() {
     522                wp_set_current_user( $this->subscriber );
     523                $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
     524                $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id );
     525                $request->set_param( 'name', 'Incorrect permissions' );
     526                $response = $this->server->dispatch( $request );
     527                $this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
     528        }
     529
     530        public function test_update_item_parent_non_hierarchical_taxonomy() {
     531                wp_set_current_user( $this->administrator );
     532                $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
     533
     534                $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_taxonomy_id );
     535                $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     536                $response = $this->server->dispatch( $request );
     537                $this->assertErrorResponse( 'rest_taxonomy_not_hierarchical', $response, 400 );
     538        }
     539
     540        public function test_delete_item() {
     541                wp_set_current_user( $this->administrator );
     542                $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' );
     543                $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id );
     544                $request->set_param( 'force', true );
     545                $response = $this->server->dispatch( $request );
     546                $this->assertEquals( 200, $response->get_status() );
     547                $data = $response->get_data();
     548                $this->assertEquals( 'Deleted Tag', $data['name'] );
     549        }
     550
     551        public function test_delete_item_force_false() {
     552                wp_set_current_user( $this->administrator );
     553                $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' );
     554                $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id );
     555                // force defaults to false
     556                $response = $this->server->dispatch( $request );
     557                $this->assertEquals( 501, $response->get_status() );
     558        }
     559
     560        public function test_delete_item_invalid_term() {
     561                wp_set_current_user( $this->administrator );
     562                $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER );
     563                $response = $this->server->dispatch( $request );
     564                $this->assertErrorResponse( 'rest_term_invalid', $response, 404 );
     565        }
     566
     567        public function test_delete_item_incorrect_permissions() {
     568                wp_set_current_user( $this->subscriber );
     569                $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
     570                $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id );
     571                $response = $this->server->dispatch( $request );
     572                $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
     573        }
     574
     575        public function test_prepare_item() {
     576                $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' );
     577                $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $term->term_id );
     578                $response = $this->server->dispatch( $request );
     579                $data = $response->get_data();
     580
     581                $this->check_taxonomy_term( $term, $data, $response->get_links() );
     582        }
     583
     584        public function test_get_item_schema() {
     585                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
     586                $response = $this->server->dispatch( $request );
     587                $data = $response->get_data();
     588                $properties = $data['schema']['properties'];
     589                $this->assertEquals( 8, count( $properties ) );
     590                $this->assertArrayHasKey( 'id', $properties );
     591                $this->assertArrayHasKey( 'count', $properties );
     592                $this->assertArrayHasKey( 'description', $properties );
     593                $this->assertArrayHasKey( 'link', $properties );
     594                $this->assertArrayHasKey( 'meta', $properties );
     595                $this->assertArrayHasKey( 'name', $properties );
     596                $this->assertArrayHasKey( 'slug', $properties );
     597                $this->assertArrayHasKey( 'taxonomy', $properties );
     598                $this->assertEquals( array_keys( get_taxonomies() ), $properties['taxonomy']['enum'] );
     599        }
     600
     601        public function test_get_item_schema_non_hierarchical() {
     602                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
     603                $response = $this->server->dispatch( $request );
     604                $data = $response->get_data();
     605                $properties = $data['schema']['properties'];
     606                $this->assertArrayHasKey( 'id', $properties );
     607                $this->assertFalse( isset( $properties['parent'] ) );
     608        }
     609
     610        public function test_get_additional_field_registration() {
     611
     612                $schema = array(
     613                        'type'        => 'integer',
     614                        'description' => 'Some integer of mine',
     615                        'enum'        => array( 1, 2, 3, 4 ),
     616                        'context'     => array( 'view', 'edit' ),
     617                );
     618
     619                register_rest_field( 'tag', 'my_custom_int', array(
     620                        'schema'          => $schema,
     621                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     622                ) );
     623
     624                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' );
     625
     626                $response = $this->server->dispatch( $request );
     627                $data = $response->get_data();
     628                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     629                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     630
     631                $tag_id = $this->factory->tag->create();
     632                $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $tag_id );
     633
     634                $response = $this->server->dispatch( $request );
     635                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     636
     637                global $wp_rest_additional_fields;
     638                $wp_rest_additional_fields = array();
     639        }
     640
     641        public function test_additional_field_update_errors() {
     642                $schema = array(
     643                        'type'        => 'integer',
     644                        'description' => 'Some integer of mine',
     645                        'enum'        => array( 1, 2, 3, 4 ),
     646                        'context'     => array( 'view', 'edit' ),
     647                );
     648
     649                register_rest_field( 'tag', 'my_custom_int', array(
     650                        'schema'          => $schema,
     651                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     652                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     653                ) );
     654
     655                wp_set_current_user( $this->administrator );
     656                $tag_id = $this->factory->tag->create();
     657                // Check for error on update.
     658                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/tags/%d', $tag_id ) );
     659                $request->set_body_params( array(
     660                        'my_custom_int' => 'returnError',
     661                ) );
     662
     663                $response = $this->server->dispatch( $request );
     664
     665                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     666
     667                global $wp_rest_additional_fields;
     668                $wp_rest_additional_fields = array();
     669        }
     670
     671        public function additional_field_get_callback( $object, $request ) {
     672                return 123;
     673        }
     674
     675        public function additional_field_update_callback( $value, $tag ) {
     676                if ( 'returnError' === $value ) {
     677                        return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
     678                }
     679        }
     680
     681        public function tearDown() {
     682                _unregister_taxonomy( 'batman' );
     683                _unregister_taxonomy( 'robin' );
     684                parent::tearDown();
     685        }
     686
     687        protected function check_get_taxonomy_terms_response( $response ) {
     688                $this->assertEquals( 200, $response->get_status() );
     689                $data = $response->get_data();
     690                $args = array(
     691                        'hide_empty' => false,
     692                );
     693                $tags = get_terms( 'post_tag', $args );
     694                $this->assertEquals( count( $tags ), count( $data ) );
     695                $this->assertEquals( $tags[0]->term_id, $data[0]['id'] );
     696                $this->assertEquals( $tags[0]->name, $data[0]['name'] );
     697                $this->assertEquals( $tags[0]->slug, $data[0]['slug'] );
     698                $this->assertEquals( $tags[0]->taxonomy, $data[0]['taxonomy'] );
     699                $this->assertEquals( $tags[0]->description, $data[0]['description'] );
     700                $this->assertEquals( $tags[0]->count, $data[0]['count'] );
     701        }
     702
     703        protected function check_taxonomy_term( $term, $data, $links ) {
     704                $this->assertEquals( $term->term_id, $data['id'] );
     705                $this->assertEquals( $term->name, $data['name'] );
     706                $this->assertEquals( $term->slug, $data['slug'] );
     707                $this->assertEquals( $term->description, $data['description'] );
     708                $this->assertEquals( get_term_link( $term ),  $data['link'] );
     709                $this->assertEquals( $term->count, $data['count'] );
     710                $taxonomy = get_taxonomy( $term->taxonomy );
     711                if ( $taxonomy->hierarchical ) {
     712                        $this->assertEquals( $term->parent, $data['parent'] );
     713                } else {
     714                        $this->assertFalse( isset( $data['parent'] ) );
     715                }
     716                $expected_links = array(
     717                        'self',
     718                        'collection',
     719                        'about',
     720                        'https://api.w.org/post_type',
     721                );
     722                if ( $taxonomy->hierarchical && $term->parent ) {
     723                        $expected_links[] = 'up';
     724                }
     725                $this->assertEqualSets( $expected_links, array_keys( $links ) );
     726                $this->assertContains( 'wp/v2/taxonomies/' . $term->taxonomy, $links['about'][0]['href'] );
     727                $this->assertEquals( add_query_arg( 'tags', $term->term_id, rest_url( 'wp/v2/posts' ) ), $links['https://api.w.org/post_type'][0]['href'] );
     728        }
     729
     730        protected function check_get_taxonomy_term_response( $response, $id ) {
     731
     732                $this->assertEquals( 200, $response->get_status() );
     733
     734                $data = $response->get_data();
     735                $tag = get_term( $id, 'post_tag' );
     736                $this->check_taxonomy_term( $tag, $data, $response->get_links() );
     737        }
     738}
  • tests/phpunit/tests/rest-api/rest-taxonomies-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Taxonomies_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcase {
     13
     14        public function test_register_routes() {
     15                $routes = $this->server->get_routes();
     16
     17                $this->assertArrayHasKey( '/wp/v2/taxonomies', $routes );
     18                $this->assertArrayHasKey( '/wp/v2/taxonomies/(?P<taxonomy>[\w-]+)', $routes );
     19        }
     20
     21        public function test_context_param() {
     22                // Collection
     23                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/taxonomies' );
     24                $response = $this->server->dispatch( $request );
     25                $data = $response->get_data();
     26                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     27                $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
     28                // Single
     29                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/taxonomies/post_tag' );
     30                $response = $this->server->dispatch( $request );
     31                $data = $response->get_data();
     32                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     33                $this->assertEqualSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] );
     34        }
     35
     36        public function test_get_items() {
     37                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
     38                $response = $this->server->dispatch( $request );
     39                $data = $response->get_data();
     40                $taxonomies = $this->get_public_taxonomies( get_taxonomies( '', 'objects' ) );
     41                $this->assertEquals( count( $taxonomies ), count( $data ) );
     42                $this->assertEquals( 'Categories', $data['category']['name'] );
     43                $this->assertEquals( 'category', $data['category']['slug'] );
     44                $this->assertEquals( true, $data['category']['hierarchical'] );
     45                $this->assertEquals( 'Tags', $data['post_tag']['name'] );
     46                $this->assertEquals( 'post_tag', $data['post_tag']['slug'] );
     47                $this->assertEquals( false, $data['post_tag']['hierarchical'] );
     48        }
     49
     50        public function test_get_items_invalid_permission_for_context() {
     51                wp_set_current_user( 0 );
     52                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
     53                $request->set_param( 'context', 'edit' );
     54                $response = $this->server->dispatch( $request );
     55                $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
     56        }
     57
     58        public function test_get_taxonomies_for_type() {
     59                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
     60                $request->set_param( 'type', 'post' );
     61                $response = $this->server->dispatch( $request );
     62                $this->check_taxonomies_for_type_response( 'post', $response );
     63        }
     64
     65        public function test_get_taxonomies_for_invalid_type() {
     66                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' );
     67                $request->set_param( 'type', 'wingding' );
     68                $response = $this->server->dispatch( $request );
     69                $this->assertEquals( 200, $response->get_status() );
     70                $data = $response->get_data();
     71                $this->assertEquals( '{}', json_encode( $data ) );
     72        }
     73
     74        public function test_get_item() {
     75                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' );
     76                $response = $this->server->dispatch( $request );
     77                $this->check_taxonomy_object_response( 'view', $response );
     78        }
     79
     80        public function test_get_item_edit_context() {
     81                $editor_id = $this->factory->user->create( array( 'role' => 'editor' ) );
     82                wp_set_current_user( $editor_id );
     83                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' );
     84                $request->set_param( 'context', 'edit' );
     85                $response = $this->server->dispatch( $request );
     86                $this->check_taxonomy_object_response( 'edit', $response );
     87        }
     88
     89        public function test_get_item_invalid_permission_for_context() {
     90                wp_set_current_user( 0 );
     91                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' );
     92                $request->set_param( 'context', 'edit' );
     93                $response = $this->server->dispatch( $request );
     94                $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     95        }
     96
     97        public function test_get_invalid_taxonomy() {
     98                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/invalid' );
     99                $response = $this->server->dispatch( $request );
     100                $this->assertErrorResponse( 'rest_taxonomy_invalid', $response, 404 );
     101        }
     102
     103        public function test_get_non_public_taxonomy() {
     104                register_taxonomy( 'api-private', 'post', array( 'public' => false ) );
     105
     106                $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/api-private' );
     107                $response = $this->server->dispatch( $request );
     108                $this->assertErrorResponse( 'rest_forbidden', $response, 403 );
     109        }
     110
     111        public function test_create_item() {
     112                /** Taxonomies can't be created **/
     113        }
     114
     115        public function test_update_item() {
     116                /** Taxonomies can't be updated **/
     117        }
     118
     119        public function test_delete_item() {
     120                /** Taxonomies can't be deleted **/
     121        }
     122
     123        public function test_prepare_item() {
     124                $tax = get_taxonomy( 'category' );
     125                $endpoint = new WP_REST_Taxonomies_Controller;
     126                $request = new WP_REST_Request;
     127                $request->set_param( 'context', 'edit' );
     128                $response = $endpoint->prepare_item_for_response( $tax, $request );
     129                $this->check_taxonomy_object( 'edit', $tax, $response->get_data(), $response->get_links() );
     130        }
     131
     132        public function test_get_item_schema() {
     133                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/taxonomies' );
     134                $response = $this->server->dispatch( $request );
     135                $data = $response->get_data();
     136                $properties = $data['schema']['properties'];
     137                $this->assertEquals( 8, count( $properties ) );
     138                $this->assertArrayHasKey( 'capabilities', $properties );
     139                $this->assertArrayHasKey( 'description', $properties );
     140                $this->assertArrayHasKey( 'hierarchical', $properties );
     141                $this->assertArrayHasKey( 'labels', $properties );
     142                $this->assertArrayHasKey( 'name', $properties );
     143                $this->assertArrayHasKey( 'slug', $properties );
     144                $this->assertArrayHasKey( 'show_cloud', $properties );
     145                $this->assertArrayHasKey( 'types', $properties );
     146        }
     147
     148        public function tearDown() {
     149                parent::tearDown();
     150        }
     151
     152        /**
     153         * Utility function for use in get_public_taxonomies
     154         */
     155        private function is_public( $taxonomy ) {
     156                return ! empty( $taxonomy->show_in_rest );
     157        }
     158        /**
     159         * Utility function to filter down to only public taxonomies
     160         */
     161        private function get_public_taxonomies( $taxonomies ) {
     162                // Pass through array_values to re-index after filtering
     163                return array_values( array_filter( $taxonomies, array( $this, 'is_public' ) ) );
     164        }
     165
     166        protected function check_taxonomy_object( $context, $tax_obj, $data, $links ) {
     167                $this->assertEquals( $tax_obj->label, $data['name'] );
     168                $this->assertEquals( $tax_obj->name, $data['slug'] );
     169                $this->assertEquals( $tax_obj->description, $data['description'] );
     170                $this->assertEquals( $tax_obj->hierarchical, $data['hierarchical'] );
     171                $this->assertEquals( rest_url( 'wp/v2/taxonomies' ), $links['collection'][0]['href'] );
     172                $this->assertArrayHasKey( 'https://api.w.org/items', $links );
     173                if ( 'edit' === $context ) {
     174                        $this->assertEquals( $tax_obj->cap, $data['capabilities'] );
     175                        $this->assertEquals( $tax_obj->labels, $data['labels'] );
     176                        $this->assertEquals( $tax_obj->show_tagcloud, $data['show_cloud'] );
     177                } else {
     178                        $this->assertFalse( isset( $data['capabilities'] ) );
     179                        $this->assertFalse( isset( $data['labels'] ) );
     180                        $this->assertFalse( isset( $data['show_cloud'] ) );
     181                }
     182        }
     183
     184        protected function check_taxonomy_object_response( $context, $response ) {
     185                $this->assertEquals( 200, $response->get_status() );
     186                $data = $response->get_data();
     187                $category = get_taxonomy( 'category' );
     188                $this->check_taxonomy_object( $context, $category, $data, $response->get_links() );
     189        }
     190
     191        protected function check_taxonomies_for_type_response( $type, $response ) {
     192                $this->assertEquals( 200, $response->get_status() );
     193                $data = $response->get_data();
     194                $taxonomies = $this->get_public_taxonomies( get_object_taxonomies( $type, 'objects' ) );
     195                $this->assertEquals( count( $taxonomies ), count( $data ) );
     196        }
     197
     198}
  • tests/phpunit/tests/rest-api/rest-test-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Controller functionality
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_REST_Test_Controller extends WP_REST_Controller {
     13
     14        /**
     15         * Get the Post type's schema, conforming to JSON Schema
     16         *
     17         * @return array
     18         */
     19        public function get_item_schema() {
     20                $schema = array(
     21                        '$schema'              => 'http://json-schema.org/draft-04/schema#',
     22                        'title'                => 'type',
     23                        'type'                 => 'object',
     24                        'properties'           => array(
     25                                'somestring'       => array(
     26                                        'type'         => 'string',
     27                                        'description'  => 'A pretty string.',
     28                                        'context'      => array( 'view' ),
     29                                ),
     30                                'someinteger'      => array(
     31                                        'type'         => 'integer',
     32                                        'context'      => array( 'view' ),
     33                                ),
     34                                'someboolean'      => array(
     35                                        'type'         => 'boolean',
     36                                        'context'      => array( 'view' ),
     37                                ),
     38                                'someurl'          => array(
     39                                        'type'         => 'string',
     40                                        'format'       => 'uri',
     41                                        'context'      => array( 'view' ),
     42                                ),
     43                                'somedate'             => array(
     44                                        'type'         => 'string',
     45                                        'format'       => 'date-time',
     46                                        'context'      => array( 'view' ),
     47                                ),
     48                                'someemail'             => array(
     49                                        'type'         => 'string',
     50                                        'format'       => 'email',
     51                                        'context'      => array( 'view' ),
     52                                ),
     53                                'someenum'         => array(
     54                                        'type'         => 'string',
     55                                        'enum'         => array( 'a', 'b', 'c' ),
     56                                        'context'      => array( 'view' ),
     57                                ),
     58                                'someargoptions'   => array(
     59                                        'type'         => 'integer',
     60                                        'required'     => true,
     61                                        'arg_options'  => array(
     62                                                'required'          => false,
     63                                                'sanitize_callback' => '__return_true',
     64                                        ),
     65                                ),
     66                                'somedefault'      => array(
     67                                        'type'         => 'string',
     68                                        'enum'         => array( 'a', 'b', 'c' ),
     69                                        'context'      => array( 'view' ),
     70                                        'default'      => 'a',
     71                                ),
     72                        ),
     73                );
     74
     75                return $schema;
     76        }
     77
     78}
  • tests/phpunit/tests/rest-api/rest-users-controller.php

     
     1<?php
     2/**
     3 * Unit tests covering WP_REST_Users_Controller functionality.
     4 *
     5 * @package WordPress
     6 * @subpackage REST API
     7 */
     8
     9/**
     10 * @group restapi
     11 */
     12class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase {
     13        /**
     14         * This function is run before each method
     15         */
     16        public function setUp() {
     17                parent::setUp();
     18
     19                $this->user = $this->factory->user->create( array(
     20                        'role' => 'administrator',
     21                ) );
     22
     23                $this->editor = $this->factory->user->create( array(
     24                        'role'       => 'editor',
     25                        'user_email' => 'editor@example.com',
     26                ) );
     27
     28                $this->endpoint = new WP_REST_Users_Controller();
     29        }
     30
     31        public function test_register_routes() {
     32                $routes = $this->server->get_routes();
     33
     34                $this->assertArrayHasKey( '/wp/v2/users', $routes );
     35                $this->assertCount( 2, $routes['/wp/v2/users'] );
     36                $this->assertArrayHasKey( '/wp/v2/users/(?P<id>[\d]+)', $routes );
     37                $this->assertCount( 3, $routes['/wp/v2/users/(?P<id>[\d]+)'] );
     38                $this->assertArrayHasKey( '/wp/v2/users/me', $routes );
     39        }
     40
     41        public function test_context_param() {
     42                // Collection
     43                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
     44                $response = $this->server->dispatch( $request );
     45                $data = $response->get_data();
     46                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     47                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     48                // Single
     49                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users/' . $this->user );
     50                $response = $this->server->dispatch( $request );
     51                $data = $response->get_data();
     52                $this->assertEquals( 'view', $data['endpoints'][0]['args']['context']['default'] );
     53                $this->assertEquals( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
     54        }
     55
     56        public function test_registered_query_params() {
     57                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
     58                $response = $this->server->dispatch( $request );
     59                $data = $response->get_data();
     60                $keys = array_keys( $data['endpoints'][0]['args'] );
     61                sort( $keys );
     62                $this->assertEquals( array(
     63                        'context',
     64                        'exclude',
     65                        'include',
     66                        'offset',
     67                        'order',
     68                        'orderby',
     69                        'page',
     70                        'per_page',
     71                        'roles',
     72                        'search',
     73                        'slug',
     74                        ), $keys );
     75        }
     76
     77        public function test_get_items() {
     78                wp_set_current_user( $this->user );
     79
     80                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     81                $request->set_param( 'context', 'view' );
     82                $response = $this->server->dispatch( $request );
     83
     84                $this->assertEquals( 200, $response->get_status() );
     85
     86                $all_data = $response->get_data();
     87                $data = $all_data[0];
     88                $userdata = get_userdata( $data['id'] );
     89                $this->check_user_data( $userdata, $data, 'view', $data['_links'] );
     90        }
     91
     92        public function test_get_items_with_edit_context() {
     93                wp_set_current_user( $this->user );
     94
     95                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     96                $request->set_param( 'context', 'edit' );
     97                $response = $this->server->dispatch( $request );
     98
     99                $this->assertEquals( 200, $response->get_status() );
     100
     101                $all_data = $response->get_data();
     102                $data = $all_data[0];
     103                $userdata = get_userdata( $data['id'] );
     104                $this->check_user_data( $userdata, $data, 'edit', $data['_links'] );
     105        }
     106
     107        public function test_get_items_with_edit_context_without_permission() {
     108                //test with a user not logged in
     109                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     110                $request->set_param( 'context', 'edit' );
     111                $response = $this->server->dispatch( $request );
     112
     113                $this->assertEquals( 401, $response->get_status() );
     114
     115                //test with a user logged in but without sufficient capabilities; capability in question: 'list_users'
     116                wp_set_current_user( $this->editor );
     117                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     118                $request->set_param( 'context', 'edit' );
     119                $response = $this->server->dispatch( $request );
     120
     121                $this->assertEquals( 403, $response->get_status() );
     122        }
     123
     124        public function test_get_items_unauthenticated_only_shows_public_users() {
     125                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     126                $response = $this->server->dispatch( $request );
     127
     128                $this->assertEquals( array(), $response->get_data() );
     129
     130                $this->factory->post->create( array( 'post_author' => $this->editor ) );
     131                $this->factory->post->create( array( 'post_author' => $this->user, 'post_status' => 'draft' ) );
     132
     133                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     134                $response = $this->server->dispatch( $request );
     135                $users = $response->get_data();
     136
     137                foreach ( $users as $user ) {
     138                        $this->assertTrue( count_user_posts( $user['id'] ) > 0 );
     139
     140                        // Ensure we don't expose non-public data
     141                        $this->assertArrayNotHasKey( 'capabilities', $user );
     142                        $this->assertArrayNotHasKey( 'email', $user );
     143                        $this->assertArrayNotHasKey( 'roles', $user );
     144                }
     145        }
     146
     147        /**
     148         * @group test
     149         */
     150        public function test_get_items_pagination_headers() {
     151                wp_set_current_user( $this->user );
     152                // Start of the index, including the three existing users
     153                for ( $i = 0; $i < 47; $i++ ) {
     154                        $this->factory->user->create( array(
     155                                'name'   => "User {$i}",
     156                                ) );
     157                }
     158                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     159                $response = $this->server->dispatch( $request );
     160                $headers = $response->get_headers();
     161                $this->assertEquals( 50, $headers['X-WP-Total'] );
     162                $this->assertEquals( 5, $headers['X-WP-TotalPages'] );
     163                $next_link = add_query_arg( array(
     164                        'page'    => 2,
     165                        ), rest_url( 'wp/v2/users' ) );
     166                $this->assertFalse( stripos( $headers['Link'], 'rel="prev"' ) );
     167                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     168                // 3rd page
     169                $this->factory->user->create( array(
     170                                'name'   => 'User 51',
     171                                ) );
     172                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     173                $request->set_param( 'page', 3 );
     174                $response = $this->server->dispatch( $request );
     175                $headers = $response->get_headers();
     176                $this->assertEquals( 51, $headers['X-WP-Total'] );
     177                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     178                $prev_link = add_query_arg( array(
     179                        'page'    => 2,
     180                        ), rest_url( 'wp/v2/users' ) );
     181                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     182                $next_link = add_query_arg( array(
     183                        'page'    => 4,
     184                        ), rest_url( 'wp/v2/users' ) );
     185                $this->assertContains( '<' . $next_link . '>; rel="next"', $headers['Link'] );
     186                // Last page
     187                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     188                $request->set_param( 'page', 6 );
     189                $response = $this->server->dispatch( $request );
     190                $headers = $response->get_headers();
     191                $this->assertEquals( 51, $headers['X-WP-Total'] );
     192                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     193                $prev_link = add_query_arg( array(
     194                        'page'    => 5,
     195                        ), rest_url( 'wp/v2/users' ) );
     196                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     197                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     198                // Out of bounds
     199                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     200                $request->set_param( 'page', 8 );
     201                $response = $this->server->dispatch( $request );
     202                $headers = $response->get_headers();
     203                $this->assertEquals( 51, $headers['X-WP-Total'] );
     204                $this->assertEquals( 6, $headers['X-WP-TotalPages'] );
     205                $prev_link = add_query_arg( array(
     206                        'page'    => 6,
     207                        ), rest_url( 'wp/v2/users' ) );
     208                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     209                $this->assertFalse( stripos( $headers['Link'], 'rel="next"' ) );
     210        }
     211
     212        public function test_get_items_per_page() {
     213                wp_set_current_user( $this->user );
     214                for ( $i = 0; $i < 20; $i++ ) {
     215                        $this->factory->user->create( array( 'display_name' => "User {$i}" ) );
     216                }
     217                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     218                $response = $this->server->dispatch( $request );
     219                $this->assertEquals( 10, count( $response->get_data() ) );
     220                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     221                $request->set_param( 'per_page', 5 );
     222                $response = $this->server->dispatch( $request );
     223                $this->assertEquals( 5, count( $response->get_data() ) );
     224        }
     225
     226        public function test_get_items_page() {
     227                wp_set_current_user( $this->user );
     228                for ( $i = 0; $i < 20; $i++ ) {
     229                        $this->factory->user->create( array( 'display_name' => "User {$i}" ) );
     230                }
     231                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     232                $request->set_param( 'per_page', 5 );
     233                $request->set_param( 'page', 2 );
     234                $response = $this->server->dispatch( $request );
     235                $this->assertEquals( 5, count( $response->get_data() ) );
     236                $prev_link = add_query_arg( array(
     237                        'per_page'  => 5,
     238                        'page'      => 1,
     239                        ), rest_url( 'wp/v2/users' ) );
     240                $headers = $response->get_headers();
     241                $this->assertContains( '<' . $prev_link . '>; rel="prev"', $headers['Link'] );
     242        }
     243
     244        public function test_get_items_orderby_name() {
     245                wp_set_current_user( $this->user );
     246                $low_id = $this->factory->user->create( array( 'display_name' => 'AAAAA' ) );
     247                $mid_id = $this->factory->user->create( array( 'display_name' => 'NNNNN' ) );
     248                $high_id = $this->factory->user->create( array( 'display_name' => 'ZZZZ' ) );
     249                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     250                $request->set_param( 'orderby', 'name' );
     251                $request->set_param( 'order', 'desc' );
     252                $request->set_param( 'per_page', 1 );
     253                $response = $this->server->dispatch( $request );
     254                $data = $response->get_data();
     255                $this->assertEquals( $high_id, $data[0]['id'] );
     256                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     257                $request->set_param( 'orderby', 'name' );
     258                $request->set_param( 'order', 'asc' );
     259                $request->set_param( 'per_page', 1 );
     260                $response = $this->server->dispatch( $request );
     261                $data = $response->get_data();
     262                $this->assertEquals( $low_id, $data[0]['id'] );
     263        }
     264
     265        public function test_get_items_orderby_url() {
     266                wp_set_current_user( $this->user );
     267
     268                $low_id = $this->factory->user->create( array( 'user_url' => 'http://a.com' ) );
     269                $high_id = $this->factory->user->create( array( 'user_url' => 'http://b.com' ) );
     270
     271                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     272                $request->set_param( 'orderby', 'url' );
     273                $request->set_param( 'order', 'desc' );
     274                $request->set_param( 'per_page', 1 );
     275                $request->set_param( 'include', array( $low_id, $high_id ) );
     276                $response = $this->server->dispatch( $request );
     277                $data = $response->get_data();
     278
     279                $this->assertEquals( $high_id, $data[0]['id'] );
     280
     281                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     282                $request->set_param( 'orderby', 'url' );
     283                $request->set_param( 'order', 'asc' );
     284                $request->set_param( 'per_page', 1 );
     285                $request->set_param( 'include', array( $low_id, $high_id ) );
     286                $response = $this->server->dispatch( $request );
     287                $data = $response->get_data();
     288                $this->assertEquals( $low_id, $data[0]['id'] );
     289        }
     290
     291        public function test_get_items_orderby_slug() {
     292                wp_set_current_user( $this->user );
     293
     294                $high_id = $this->factory->user->create( array( 'user_nicename' => 'blogin' ) );
     295                $low_id = $this->factory->user->create( array( 'user_nicename' => 'alogin' ) );
     296
     297                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     298                $request->set_param( 'orderby', 'slug' );
     299                $request->set_param( 'order', 'desc' );
     300                $request->set_param( 'per_page', 1 );
     301                $request->set_param( 'include', array( $low_id, $high_id ) );
     302                $response = $this->server->dispatch( $request );
     303                $data = $response->get_data();
     304
     305                $this->assertEquals( $high_id, $data[0]['id'] );
     306
     307                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     308                $request->set_param( 'orderby', 'slug' );
     309                $request->set_param( 'order', 'asc' );
     310                $request->set_param( 'per_page', 1 );
     311                $request->set_param( 'include', array( $low_id, $high_id ) );
     312                $response = $this->server->dispatch( $request );
     313                $data = $response->get_data();
     314                $this->assertEquals( $low_id, $data[0]['id'] );
     315        }
     316
     317        public function test_get_items_orderby_email() {
     318                wp_set_current_user( $this->user );
     319
     320                $high_id = $this->factory->user->create( array( 'user_email' => 'bemail@gmail.com' ) );
     321                $low_id = $this->factory->user->create( array( 'user_email' => 'aemail@gmail.com' ) );
     322
     323                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     324                $request->set_param( 'orderby', 'email' );
     325                $request->set_param( 'order', 'desc' );
     326                $request->set_param( 'per_page', 1 );
     327                $request->set_param( 'include', array( $low_id, $high_id ) );
     328                $response = $this->server->dispatch( $request );
     329                $data = $response->get_data();
     330                $this->assertEquals( $high_id, $data[0]['id'] );
     331
     332                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     333                $request->set_param( 'orderby', 'email' );
     334                $request->set_param( 'order', 'asc' );
     335                $request->set_param( 'per_page', 1 );
     336                $request->set_param( 'include', array( $low_id, $high_id ) );
     337                $response = $this->server->dispatch( $request );
     338                $data = $response->get_data();
     339                $this->assertEquals( $low_id, $data[0]['id'] );
     340        }
     341
     342        public function test_get_items_orderby_email_unauthenticated() {
     343                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     344                $request->set_param( 'orderby', 'email' );
     345                $request->set_param( 'order', 'desc' );
     346                $response = $this->server->dispatch( $request );
     347                $this->assertErrorResponse( 'rest_forbidden_orderby', $response, 401 );
     348        }
     349
     350        public function test_get_items_orderby_registered_date_unauthenticated() {
     351                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     352                $request->set_param( 'orderby', 'registered_date' );
     353                $request->set_param( 'order', 'desc' );
     354                $response = $this->server->dispatch( $request );
     355                $this->assertErrorResponse( 'rest_forbidden_orderby', $response, 401 );
     356        }
     357
     358        public function test_get_items_offset() {
     359                wp_set_current_user( $this->user );
     360                // 2 users created in __construct(), plus default user
     361                $this->factory->user->create();
     362                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     363                $request->set_param( 'offset', 1 );
     364                $response = $this->server->dispatch( $request );
     365                $this->assertCount( 3, $response->get_data() );
     366                // 'offset' works with 'per_page'
     367                $request->set_param( 'per_page', 2 );
     368                $response = $this->server->dispatch( $request );
     369                $this->assertCount( 2, $response->get_data() );
     370                // 'offset' takes priority over 'page'
     371                $request->set_param( 'page', 3 );
     372                $response = $this->server->dispatch( $request );
     373                $this->assertCount( 2, $response->get_data() );
     374        }
     375
     376        public function test_get_items_include_query() {
     377                wp_set_current_user( $this->user );
     378                $id1 = $this->factory->user->create();
     379                $id2 = $this->factory->user->create();
     380                $id3 = $this->factory->user->create();
     381                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     382                // Orderby=>asc
     383                $request->set_param( 'include', array( $id3, $id1 ) );
     384                $response = $this->server->dispatch( $request );
     385                $data = $response->get_data();
     386                $this->assertEquals( 2, count( $data ) );
     387                $this->assertEquals( $id1, $data[0]['id'] );
     388                // Orderby=>include
     389                $request->set_param( 'orderby', 'include' );
     390                $response = $this->server->dispatch( $request );
     391                $data = $response->get_data();
     392                $this->assertEquals( 2, count( $data ) );
     393                $this->assertEquals( $id3, $data[0]['id'] );
     394                // No privileges
     395                wp_set_current_user( 0 );
     396                $response = $this->server->dispatch( $request );
     397                $data = $response->get_data();
     398                $this->assertEquals( 0, count( $data ) );
     399
     400        }
     401
     402        public function test_get_items_exclude_query() {
     403                wp_set_current_user( $this->user );
     404                $id1 = $this->factory->user->create();
     405                $id2 = $this->factory->user->create();
     406                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     407                $response = $this->server->dispatch( $request );
     408                $data = $response->get_data();
     409                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     410                $this->assertTrue( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     411                $request->set_param( 'exclude', array( $id2 ) );
     412                $response = $this->server->dispatch( $request );
     413                $data = $response->get_data();
     414                $this->assertTrue( in_array( $id1, wp_list_pluck( $data, 'id' ), true ) );
     415                $this->assertFalse( in_array( $id2, wp_list_pluck( $data, 'id' ), true ) );
     416        }
     417
     418        public function test_get_items_search() {
     419                wp_set_current_user( $this->user );
     420                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     421                $request->set_param( 'search', 'yololololo' );
     422                $response = $this->server->dispatch( $request );
     423                $this->assertEquals( 0, count( $response->get_data() ) );
     424                $yolo_id = $this->factory->user->create( array( 'display_name' => 'yololololo' ) );
     425                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     426                $request->set_param( 'search', (string) $yolo_id );
     427                $response = $this->server->dispatch( $request );
     428                $this->assertEquals( 1, count( $response->get_data() ) );
     429                // default to wildcard search
     430                $adam_id = $this->factory->user->create( array(
     431                        'role'          => 'author',
     432                        'user_nicename' => 'adam',
     433                ) );
     434                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     435                $request->set_param( 'search', 'ada' );
     436                $response = $this->server->dispatch( $request );
     437                $data = $response->get_data();
     438                $this->assertEquals( 1, count( $data ) );
     439                $this->assertEquals( $adam_id, $data[0]['id'] );
     440        }
     441
     442        public function test_get_items_slug_query() {
     443                wp_set_current_user( $this->user );
     444                $this->factory->user->create( array( 'display_name' => 'foo', 'user_login' => 'bar' ) );
     445                $id2 = $this->factory->user->create( array( 'display_name' => 'Moo', 'user_login' => 'foo' ) );
     446                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     447                $request->set_param( 'slug', 'foo' );
     448                $response = $this->server->dispatch( $request );
     449                $data = $response->get_data();
     450                $this->assertEquals( 1, count( $data ) );
     451                $this->assertEquals( $id2, $data[0]['id'] );
     452        }
     453
     454        // 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.
     455        public function test_get_items_roles() {
     456                wp_set_current_user( $this->user );
     457                $tango = $this->factory->user->create( array( 'display_name' => 'tango', 'role' => 'subscriber' ) );
     458                $yolo  = $this->factory->user->create( array( 'display_name' => 'yolo', 'role' => 'author' ) );
     459                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     460                $request->set_param( 'roles', 'author,subscriber' );
     461                $response = $this->server->dispatch( $request );
     462                $data = $response->get_data();
     463                $this->assertEquals( 2, count( $data ) );
     464                $this->assertEquals( $tango, $data[0]['id'] );
     465                $this->assertEquals( $yolo, $data[1]['id'] );
     466                $request->set_param( 'roles', 'author' );
     467                $response = $this->server->dispatch( $request );
     468                $data = $response->get_data();
     469                $this->assertEquals( 1, count( $data ) );
     470                $this->assertEquals( $yolo, $data[0]['id'] );
     471                wp_set_current_user( 0 );
     472                $request->set_param( 'roles', 'author' );
     473                $response = $this->server->dispatch( $request );
     474                $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 );
     475                wp_set_current_user( $this->editor );
     476                $request->set_param( 'roles', 'author' );
     477                $response = $this->server->dispatch( $request );
     478                $this->assertErrorResponse( 'rest_user_cannot_view', $response, 403 );
     479        }
     480
     481        public function test_get_items_invalid_roles() {
     482                wp_set_current_user( $this->user );
     483                $lolz = $this->factory->user->create( array( 'display_name' => 'lolz', 'role' => 'author' ) );
     484                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     485                $request->set_param( 'roles', 'ilovesteak,author' );
     486                $response = $this->server->dispatch( $request );
     487                $data = $response->get_data();
     488                $this->assertEquals( 1, count( $data ) );
     489                $this->assertEquals( $lolz, $data[0]['id'] );
     490                $request = new WP_REST_Request( 'GET', '/wp/v2/users' );
     491                $request->set_param( 'roles', 'steakisgood' );
     492                $response = $this->server->dispatch( $request );
     493                $data = $response->get_data();
     494                $this->assertEquals( 0, count( $data ) );
     495                $this->assertEquals( array(), $data );
     496        }
     497
     498        public function test_get_item() {
     499                $user_id = $this->factory->user->create();
     500                wp_set_current_user( $this->user );
     501
     502                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $user_id ) );
     503
     504                $response = $this->server->dispatch( $request );
     505                $this->check_get_user_response( $response, 'embed' );
     506        }
     507
     508        public function test_prepare_item() {
     509                wp_set_current_user( $this->user );
     510                $request = new WP_REST_Request;
     511                $request->set_param( 'context', 'edit' );
     512                $user = get_user_by( 'id', get_current_user_id() );
     513                $data = $this->endpoint->prepare_item_for_response( $user, $request );
     514                $this->check_get_user_response( $data, 'edit' );
     515        }
     516
     517        public function test_get_user_avatar_urls() {
     518                wp_set_current_user( $this->user );
     519
     520                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->editor ) );
     521
     522                $response = $this->server->dispatch( $request );
     523
     524                $data = $response->get_data();
     525                $this->assertArrayHasKey( 24,  $data['avatar_urls'] );
     526                $this->assertArrayHasKey( 48,  $data['avatar_urls'] );
     527                $this->assertArrayHasKey( 96,  $data['avatar_urls'] );
     528
     529                $user = get_user_by( 'id', $this->editor );
     530                /**
     531                 * Ignore the subdomain, since 'get_avatar_url randomly sets the Gravatar
     532                 * server when building the url string.
     533                 */
     534                $this->assertEquals( substr( get_avatar_url( $user->user_email ), 9 ), substr( $data['avatar_urls'][96], 9 ) );
     535        }
     536
     537        public function test_get_user_invalid_id() {
     538                wp_set_current_user( $this->user );
     539                $request = new WP_REST_Request( 'GET', '/wp/v2/users/100' );
     540                $response = $this->server->dispatch( $request );
     541
     542                $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
     543        }
     544
     545        public function test_get_user_empty_capabilities() {
     546                wp_set_current_user( $this->user );
     547                $this->allow_user_to_manage_multisite();
     548
     549                $lolz = $this->factory->user->create( array( 'display_name' => 'lolz', 'roles' => '' ) );
     550                delete_user_option( $lolz, 'capabilities' );
     551                delete_user_option( $lolz, 'user_level' );
     552                $request = new WP_REST_Request( 'GET', '/wp/v2/users/' . $lolz );
     553                $request->set_param( 'context', 'edit' );
     554                $response = $this->server->dispatch( $request );
     555                $data = $response->get_data();
     556
     557                $this->assertEquals( $data['capabilities'], new stdClass() );
     558                $this->assertEquals( $data['extra_capabilities'], new stdClass() );
     559        }
     560
     561        public function test_get_item_without_permission() {
     562                wp_set_current_user( $this->editor );
     563
     564                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->user ) );
     565                $response = $this->server->dispatch( $request );
     566
     567                $this->assertErrorResponse( 'rest_user_cannot_view', $response, 403 );
     568        }
     569
     570        public function test_get_item_published_author_post() {
     571                $this->author_id = $this->factory->user->create( array(
     572                        'role' => 'author',
     573                ) );
     574                $this->post_id = $this->factory->post->create( array(
     575                        'post_author' => $this->author_id,
     576                ));
     577                wp_set_current_user( 0 );
     578                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) );
     579                $response = $this->server->dispatch( $request );
     580                $this->check_get_user_response( $response, 'embed' );
     581        }
     582
     583        public function test_get_item_published_author_pages() {
     584                $this->author_id = $this->factory->user->create( array(
     585                        'role' => 'author',
     586                ) );
     587                wp_set_current_user( 0 );
     588                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) );
     589                $response = $this->server->dispatch( $request );
     590                $this->assertEquals( 401, $response->get_status() );
     591                $this->post_id = $this->factory->post->create( array(
     592                        'post_author' => $this->author_id,
     593                        'post_type'   => 'page',
     594                ));
     595                $response = $this->server->dispatch( $request );
     596                $this->check_get_user_response( $response, 'embed' );
     597        }
     598
     599        public function test_get_user_with_edit_context() {
     600                $user_id = $this->factory->user->create();
     601                $this->allow_user_to_manage_multisite();
     602
     603                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $user_id ) );
     604                $request->set_param( 'context', 'edit' );
     605
     606                $response = $this->server->dispatch( $request );
     607                $this->check_get_user_response( $response, 'edit' );
     608        }
     609
     610        public function test_get_item_published_author_wrong_context() {
     611                $this->author_id = $this->factory->user->create( array(
     612                        'role' => 'author',
     613                ) );
     614                $this->post_id = $this->factory->post->create( array(
     615                        'post_author' => $this->author_id,
     616                ));
     617                wp_set_current_user( 0 );
     618                $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) );
     619                $request->set_param( 'context', 'edit' );
     620                $response = $this->server->dispatch( $request );
     621                $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 );
     622        }
     623
     624        public function test_get_current_user() {
     625                wp_set_current_user( $this->user );
     626
     627                $request = new WP_REST_Request( 'GET', '/wp/v2/users/me' );
     628
     629                $response = $this->server->dispatch( $request );
     630                $this->assertEquals( 302, $response->get_status() );
     631
     632                $headers = $response->get_headers();
     633                $this->assertArrayHasKey( 'Location', $headers );
     634                $this->assertEquals( rest_url( 'wp/v2/users/' . $this->user ), $headers['Location'] );
     635        }
     636
     637        public function test_get_current_user_without_permission() {
     638                wp_set_current_user( 0 );
     639                $request = new WP_REST_Request( 'GET', '/wp/v2/users/me' );
     640                $response = $this->server->dispatch( $request );
     641
     642                $this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
     643        }
     644
     645        public function test_create_item() {
     646                $this->allow_user_to_manage_multisite();
     647                wp_set_current_user( $this->user );
     648
     649                $params = array(
     650                        'username'    => 'testuser',
     651                        'password'    => 'testpassword',
     652                        'email'       => 'test@example.com',
     653                        'name'        => 'Test User',
     654                        'nickname'    => 'testuser',
     655                        'slug'        => 'test-user',
     656                        'role'        => 'editor',
     657                        'description' => 'New API User',
     658                        'url'         => 'http://example.com',
     659                );
     660
     661                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     662                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     663                $request->set_body_params( $params );
     664
     665                $response = $this->server->dispatch( $request );
     666                $data = $response->get_data();
     667                $this->assertEquals( 'http://example.com', $data['url'] );
     668                $this->check_add_edit_user_response( $response );
     669        }
     670
     671        public function test_json_create_user() {
     672                $this->allow_user_to_manage_multisite();
     673                wp_set_current_user( $this->user );
     674
     675                $params = array(
     676                        'username' => 'testjsonuser',
     677                        'password' => 'testjsonpassword',
     678                        'email'    => 'testjson@example.com',
     679                );
     680
     681                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     682                $request->add_header( 'content-type', 'application/json' );
     683                $request->set_body( wp_json_encode( $params ) );
     684
     685                $response = $this->server->dispatch( $request );
     686                $this->check_add_edit_user_response( $response );
     687        }
     688
     689        public function test_create_user_without_permission() {
     690                wp_set_current_user( $this->editor );
     691
     692                $params = array(
     693                        'username' => 'homersimpson',
     694                        'password' => 'stupidsexyflanders',
     695                        'email'    => 'chunkylover53@aol.com',
     696                );
     697
     698                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     699                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     700                $request->set_body_params( $params );
     701                $response = $this->server->dispatch( $request );
     702
     703                $this->assertErrorResponse( 'rest_cannot_create_user', $response, 403 );
     704        }
     705
     706        public function test_create_user_invalid_id() {
     707                $this->allow_user_to_manage_multisite();
     708                wp_set_current_user( $this->user );
     709
     710                $params = array(
     711                        'id'       => '156',
     712                        'username' => 'lisasimpson',
     713                        'password' => 'DavidHasselhoff',
     714                        'email'    => 'smartgirl63_@yahoo.com',
     715                );
     716
     717                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     718                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     719                $request->set_body_params( $params );
     720                $response = $this->server->dispatch( $request );
     721
     722                $this->assertErrorResponse( 'rest_user_exists', $response, 400 );
     723        }
     724
     725        public function test_create_user_invalid_email() {
     726                $this->allow_user_to_manage_multisite();
     727                wp_set_current_user( $this->user );
     728
     729                $params = array(
     730                        'username' => 'lisasimpson',
     731                        'password' => 'DavidHasselhoff',
     732                        'email'    => 'something',
     733                );
     734
     735                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     736                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     737                $request->set_body_params( $params );
     738                $response = $this->server->dispatch( $request );
     739
     740                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     741        }
     742
     743        public function test_create_user_invalid_role() {
     744                $this->allow_user_to_manage_multisite();
     745                wp_set_current_user( $this->user );
     746
     747                $params = array(
     748                        'username' => 'maggiesimpson',
     749                        'password' => 'i_shot_mrburns',
     750                        'email'    => 'packingheat@example.com',
     751                        'roles'    => array( 'baby' ),
     752                );
     753
     754                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     755                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     756                $request->set_body_params( $params );
     757                $response = $this->server->dispatch( $request );
     758
     759                $this->assertErrorResponse( 'rest_user_invalid_role', $response, 400 );
     760        }
     761
     762        public function test_update_item() {
     763                $user_id = $this->factory->user->create( array(
     764                        'user_email' => 'test@example.com',
     765                        'user_pass' => 'sjflsfls',
     766                        'user_login' => 'test_update',
     767                        'first_name' => 'Old Name',
     768                        'user_url' => 'http://apple.com',
     769                ));
     770                $this->allow_user_to_manage_multisite();
     771                wp_set_current_user( $this->user );
     772
     773                $userdata = get_userdata( $user_id );
     774                $pw_before = $userdata->user_pass;
     775
     776                $_POST['email'] = $userdata->user_email;
     777                $_POST['username'] = $userdata->user_login;
     778                $_POST['first_name'] = 'New Name';
     779                $_POST['url'] = 'http://google.com';
     780
     781                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
     782                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     783                $request->set_body_params( $_POST );
     784
     785                $response = $this->server->dispatch( $request );
     786                $this->check_add_edit_user_response( $response, true );
     787
     788                // Check that the name has been updated correctly
     789                $new_data = $response->get_data();
     790                $this->assertEquals( 'New Name', $new_data['first_name'] );
     791                $user = get_userdata( $user_id );
     792                $this->assertEquals( 'New Name', $user->first_name );
     793
     794                $this->assertEquals( 'http://google.com', $new_data['url'] );
     795                $this->assertEquals( 'http://google.com', $user->user_url );
     796
     797                // Check that we haven't inadvertently changed the user's password,
     798                // as per https://core.trac.wordpress.org/ticket/21429
     799                $this->assertEquals( $pw_before, $user->user_pass );
     800        }
     801
     802        public function test_update_item_existing_email() {
     803                $user1 = $this->factory->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com' ) );
     804                $user2 = $this->factory->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com' ) );
     805                $this->allow_user_to_manage_multisite();
     806                wp_set_current_user( $this->user );
     807
     808                $request = new WP_REST_Request( 'PUT', '/wp/v2/users/' . $user2 );
     809                $request->set_param( 'email', 'testjson@example.com' );
     810                $response = $this->server->dispatch( $request );
     811                $this->assertInstanceOf( 'WP_Error', $response->as_error() );
     812                $this->assertEquals( 'rest_user_invalid_email', $response->as_error()->get_error_code() );
     813        }
     814
     815        public function test_update_item_username_attempt() {
     816                $user1 = $this->factory->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com' ) );
     817                $user2 = $this->factory->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com' ) );
     818                $this->allow_user_to_manage_multisite();
     819                wp_set_current_user( $this->user );
     820
     821                $request = new WP_REST_Request( 'PUT', '/wp/v2/users/' . $user2 );
     822                $request->set_param( 'username', 'test_json_user' );
     823                $response = $this->server->dispatch( $request );
     824                $this->assertInstanceOf( 'WP_Error', $response->as_error() );
     825                $this->assertEquals( 'rest_user_invalid_argument', $response->as_error()->get_error_code() );
     826        }
     827
     828        public function test_update_item_existing_nicename() {
     829                $user1 = $this->factory->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com' ) );
     830                $user2 = $this->factory->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com' ) );
     831                $this->allow_user_to_manage_multisite();
     832                wp_set_current_user( $this->user );
     833
     834                $request = new WP_REST_Request( 'PUT', '/wp/v2/users/' . $user2 );
     835                $request->set_param( 'slug', 'test_json_user' );
     836                $response = $this->server->dispatch( $request );
     837                $this->assertInstanceOf( 'WP_Error', $response->as_error() );
     838                $this->assertEquals( 'rest_user_invalid_slug', $response->as_error()->get_error_code() );
     839        }
     840
     841        public function test_json_update_user() {
     842                $user_id = $this->factory->user->create( array(
     843                        'user_email' => 'testjson2@example.com',
     844                        'user_pass'  => 'sjflsfl3sdjls',
     845                        'user_login' => 'test_json_update',
     846                        'first_name' => 'Old Name',
     847                        'last_name'  => 'Original Last',
     848                ));
     849                $this->allow_user_to_manage_multisite();
     850                wp_set_current_user( $this->user );
     851
     852                $params = array(
     853                        'username'   => 'test_json_update',
     854                        'email'      => 'testjson2@example.com',
     855                        'first_name' => 'JSON Name',
     856                        'last_name'  => 'New Last',
     857                );
     858
     859                $userdata = get_userdata( $user_id );
     860                $pw_before = $userdata->user_pass;
     861
     862                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
     863                $request->add_header( 'content-type', 'application/json' );
     864                $request->set_body( wp_json_encode( $params ) );
     865
     866                $response = $this->server->dispatch( $request );
     867                $this->check_add_edit_user_response( $response, true );
     868
     869                // Check that the name has been updated correctly
     870                $new_data = $response->get_data();
     871                $this->assertEquals( 'JSON Name', $new_data['first_name'] );
     872                $this->assertEquals( 'New Last', $new_data['last_name'] );
     873                $user = get_userdata( $user_id );
     874                $this->assertEquals( 'JSON Name', $user->first_name );
     875                $this->assertEquals( 'New Last', $user->last_name );
     876
     877                // Check that we haven't inadvertently changed the user's password,
     878                // as per https://core.trac.wordpress.org/ticket/21429
     879                $this->assertEquals( $pw_before, $user->user_pass );
     880        }
     881
     882        public function test_update_user_role() {
     883                $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
     884
     885                wp_set_current_user( $this->user );
     886                $this->allow_user_to_manage_multisite();
     887
     888                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
     889                $request->set_param( 'roles', array( 'editor' ) );
     890                $response = $this->server->dispatch( $request );
     891
     892                $new_data = $response->get_data();
     893
     894                $this->assertEquals( 'editor', $new_data['roles'][0] );
     895                $this->assertNotEquals( 'administrator', $new_data['roles'][0] );
     896
     897                $user = get_userdata( $user_id );
     898                $this->assertArrayHasKey( 'editor', $user->caps );
     899                $this->assertArrayNotHasKey( 'administrator', $user->caps );
     900        }
     901
     902        public function test_update_user_role_invalid_privilege_escalation() {
     903                wp_set_current_user( $this->editor );
     904
     905                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->editor ) );
     906                $request->set_param( 'roles', array( 'administrator' ) );
     907                $response = $this->server->dispatch( $request );
     908
     909                $this->assertErrorResponse( 'rest_cannot_edit_roles', $response, 403 );
     910                $user = get_userdata( $this->editor );
     911                $this->assertArrayHasKey( 'editor', $user->caps );
     912                $this->assertArrayNotHasKey( 'administrator', $user->caps );
     913        }
     914
     915        public function test_update_user_role_invalid_privilege_deescalation() {
     916                if ( is_multisite() ) {
     917                        return $this->markTestSkipped( 'Test only intended for single site.' );
     918                }
     919
     920                $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
     921
     922                wp_set_current_user( $user_id );
     923
     924                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
     925                $request->set_param( 'roles', array( 'editor' ) );
     926                $response = $this->server->dispatch( $request );
     927
     928                $this->assertErrorResponse( 'rest_user_invalid_role', $response, 403 );
     929
     930                $user = get_userdata( $user_id );
     931                $this->assertArrayHasKey( 'administrator', $user->caps );
     932                $this->assertArrayNotHasKey( 'editor', $user->caps );
     933        }
     934
     935        public function test_update_user_role_privilege_deescalation_multisite() {
     936                if ( ! is_multisite() ) {
     937                        return $this->markTestSkipped( 'Test only intended for multisite.' );
     938                }
     939
     940                $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
     941
     942                wp_set_current_user( $user_id );
     943                $user = wp_get_current_user();
     944                update_site_option( 'site_admins', array( $user->user_login ) );
     945
     946                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) );
     947                $request->set_param( 'roles', array( 'editor' ) );
     948                $response = $this->server->dispatch( $request );
     949
     950                $new_data = $response->get_data();
     951                $this->assertEquals( 'editor', $new_data['roles'][0] );
     952                $this->assertNotEquals( 'administrator', $new_data['roles'][0] );
     953        }
     954
     955
     956        public function test_update_user_role_invalid_role() {
     957                wp_set_current_user( $this->user );
     958                $this->allow_user_to_manage_multisite();
     959
     960                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->editor ) );
     961                $request->set_param( 'roles', array( 'BeSharp' ) );
     962                $response = $this->server->dispatch( $request );
     963
     964                $this->assertErrorResponse( 'rest_user_invalid_role', $response, 400 );
     965
     966                $user = get_userdata( $this->editor );
     967                $this->assertArrayHasKey( 'editor', $user->caps );
     968                $this->assertArrayNotHasKey( 'BeSharp', $user->caps );
     969        }
     970
     971        public function test_update_user_without_permission() {
     972                wp_set_current_user( $this->editor );
     973
     974                $params = array(
     975                        'username' => 'homersimpson',
     976                        'password' => 'stupidsexyflanders',
     977                        'email'    => 'chunkylover53@aol.com',
     978                );
     979
     980                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->user ) );
     981                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     982                $request->set_body_params( $params );
     983                $response = $this->server->dispatch( $request );
     984
     985                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     986        }
     987
     988        public function test_update_user_invalid_id() {
     989                $this->allow_user_to_manage_multisite();
     990                wp_set_current_user( $this->user );
     991
     992                $params = array(
     993                        'id'       => '156',
     994                        'username' => 'lisasimpson',
     995                        'password' => 'DavidHasselhoff',
     996                        'email'    => 'smartgirl63_@yahoo.com',
     997                );
     998
     999                $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $this->editor ) );
     1000                $request->add_header( 'content-type', 'application/x-www-form-urlencoded' );
     1001                $request->set_body_params( $params );
     1002                $response = $this->server->dispatch( $request );
     1003
     1004                $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
     1005        }
     1006
     1007        public function test_delete_item() {
     1008                $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) );
     1009
     1010                $this->allow_user_to_manage_multisite();
     1011                wp_set_current_user( $this->user );
     1012
     1013                $userdata = get_userdata( $user_id ); // cache for later
     1014                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
     1015                $request['force'] = true;
     1016                $response = $this->server->dispatch( $request );
     1017
     1018                $this->assertEquals( 200, $response->get_status() );
     1019                $data = $response->get_data();
     1020                $this->assertEquals( 'Deleted User', $data['name'] );
     1021        }
     1022
     1023        public function test_delete_item_no_trash() {
     1024                $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) );
     1025
     1026                $this->allow_user_to_manage_multisite();
     1027                wp_set_current_user( $this->user );
     1028
     1029                $userdata = get_userdata( $user_id ); // cache for later
     1030                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
     1031                $response = $this->server->dispatch( $request );
     1032                $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 );
     1033
     1034                // Ensure the user still exists
     1035                $user = get_user_by( 'id', $user_id );
     1036                $this->assertNotEmpty( $user );
     1037        }
     1038
     1039        public function test_delete_user_without_permission() {
     1040                $user_id = $this->factory->user->create();
     1041
     1042                $this->allow_user_to_manage_multisite();
     1043                wp_set_current_user( $this->editor );
     1044
     1045                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
     1046                $request['force'] = true;
     1047                $response = $this->server->dispatch( $request );
     1048
     1049                $this->assertErrorResponse( 'rest_user_cannot_delete', $response, 403 );
     1050        }
     1051
     1052        public function test_delete_user_invalid_id() {
     1053                $this->allow_user_to_manage_multisite();
     1054                wp_set_current_user( $this->user );
     1055
     1056                $request = new WP_REST_Request( 'DELETE', '/wp/v2/users/100' );
     1057                $request['force'] = true;
     1058                $response = $this->server->dispatch( $request );
     1059
     1060                $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
     1061        }
     1062
     1063        public function test_delete_user_reassign() {
     1064                $this->allow_user_to_manage_multisite();
     1065
     1066                // Test with a new user, to avoid any complications
     1067                $user_id = $this->factory->user->create();
     1068                $reassign_id = $this->factory->user->create();
     1069                $test_post = $this->factory->post->create(array(
     1070                        'post_author' => $user_id,
     1071                ));
     1072
     1073                // Sanity check to ensure the factory created the post correctly
     1074                $post = get_post( $test_post );
     1075                $this->assertEquals( $user_id, $post->post_author );
     1076
     1077                // Delete our test user, and reassign to the new author
     1078                wp_set_current_user( $this->user );
     1079                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
     1080                $request['force'] = true;
     1081                $request->set_param( 'reassign', $reassign_id );
     1082                $response = $this->server->dispatch( $request );
     1083
     1084                $this->assertEquals( 200, $response->get_status() );
     1085
     1086                // Check that the post has been updated correctly
     1087                $post = get_post( $test_post );
     1088                $this->assertEquals( $reassign_id, $post->post_author );
     1089        }
     1090
     1091        public function test_delete_user_invalid_reassign_id() {
     1092                $user_id = $this->factory->user->create();
     1093
     1094                $this->allow_user_to_manage_multisite();
     1095                wp_set_current_user( $this->user );
     1096
     1097                $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d', $user_id ) );
     1098                $request['force'] = true;
     1099                $request->set_param( 'reassign', 100 );
     1100                $response = $this->server->dispatch( $request );
     1101
     1102                $this->assertErrorResponse( 'rest_user_invalid_reassign', $response, 400 );
     1103        }
     1104
     1105        public function test_get_item_schema() {
     1106                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
     1107                $response = $this->server->dispatch( $request );
     1108                $data = $response->get_data();
     1109                $properties = $data['schema']['properties'];
     1110
     1111                $this->assertEquals( 18, count( $properties ) );
     1112                $this->assertArrayHasKey( 'avatar_urls', $properties );
     1113                $this->assertArrayHasKey( 'capabilities', $properties );
     1114                $this->assertArrayHasKey( 'description', $properties );
     1115                $this->assertArrayHasKey( 'email', $properties );
     1116                $this->assertArrayHasKey( 'extra_capabilities', $properties );
     1117                $this->assertArrayHasKey( 'first_name', $properties );
     1118                $this->assertArrayHasKey( 'id', $properties );
     1119                $this->assertArrayHasKey( 'last_name', $properties );
     1120                $this->assertArrayHasKey( 'link', $properties );
     1121                $this->assertArrayHasKey( 'meta', $properties );
     1122                $this->assertArrayHasKey( 'name', $properties );
     1123                $this->assertArrayHasKey( 'nickname', $properties );
     1124                $this->assertArrayHasKey( 'registered_date', $properties );
     1125                $this->assertArrayHasKey( 'slug', $properties );
     1126                $this->assertArrayHasKey( 'password', $properties );
     1127                $this->assertArrayHasKey( 'url', $properties );
     1128                $this->assertArrayHasKey( 'username', $properties );
     1129                $this->assertArrayHasKey( 'roles', $properties );
     1130
     1131        }
     1132
     1133        public function test_get_item_schema_show_avatar() {
     1134                update_option( 'show_avatars', false );
     1135                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
     1136                $response = $this->server->dispatch( $request );
     1137                $data = $response->get_data();
     1138                $properties = $data['schema']['properties'];
     1139
     1140                $this->assertArrayNotHasKey( 'avatar_urls', $properties );
     1141        }
     1142
     1143        public function test_get_additional_field_registration() {
     1144
     1145                $schema = array(
     1146                        'type'        => 'integer',
     1147                        'description' => 'Some integer of mine',
     1148                        'enum'        => array( 1, 2, 3, 4 ),
     1149                        'context'     => array( 'embed', 'view', 'edit' ),
     1150                );
     1151
     1152                register_rest_field( 'user', 'my_custom_int', array(
     1153                        'schema'          => $schema,
     1154                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     1155                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     1156                ) );
     1157
     1158                $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' );
     1159
     1160                $response = $this->server->dispatch( $request );
     1161                $data = $response->get_data();
     1162
     1163                $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] );
     1164                $this->assertEquals( $schema, $data['schema']['properties']['my_custom_int'] );
     1165
     1166                wp_set_current_user( 1 );
     1167                if ( is_multisite() ) {
     1168                        $current_user = wp_get_current_user( 1 );
     1169                        update_site_option( 'site_admins', array( $current_user->user_login ) );
     1170                }
     1171
     1172                $request = new WP_REST_Request( 'GET', '/wp/v2/users/1' );
     1173
     1174                $response = $this->server->dispatch( $request );
     1175                $this->assertArrayHasKey( 'my_custom_int', $response->data );
     1176
     1177                $request = new WP_REST_Request( 'POST', '/wp/v2/users/1' );
     1178                $request->set_body_params(array(
     1179                        'my_custom_int' => 123,
     1180                ));
     1181
     1182                $response = $this->server->dispatch( $request );
     1183                $this->assertEquals( 123, get_user_meta( 1, 'my_custom_int', true ) );
     1184
     1185                $request = new WP_REST_Request( 'POST', '/wp/v2/users' );
     1186                $request->set_body_params(array(
     1187                        'my_custom_int' => 123,
     1188                        'email' => 'joe@foobar.com',
     1189                        'username' => 'abc123',
     1190                        'password' => 'hello',
     1191                ));
     1192
     1193                $response = $this->server->dispatch( $request );
     1194
     1195                $this->assertEquals( 123, $response->data['my_custom_int'] );
     1196
     1197                global $wp_rest_additional_fields;
     1198                $wp_rest_additional_fields = array();
     1199        }
     1200
     1201        public function test_additional_field_update_errors() {
     1202                $schema = array(
     1203                        'type'        => 'integer',
     1204                        'description' => 'Some integer of mine',
     1205                        'enum'        => array( 1, 2, 3, 4 ),
     1206                        'context'     => array( 'view', 'edit' ),
     1207                );
     1208
     1209                register_rest_field( 'user', 'my_custom_int', array(
     1210                        'schema'          => $schema,
     1211                        'get_callback'    => array( $this, 'additional_field_get_callback' ),
     1212                        'update_callback' => array( $this, 'additional_field_update_callback' ),
     1213                ) );
     1214
     1215                wp_set_current_user( 1 );
     1216                if ( is_multisite() ) {
     1217                        $current_user = wp_get_current_user( 1 );
     1218                        update_site_option( 'site_admins', array( $current_user->user_login ) );
     1219                }
     1220
     1221                // Check for error on update.
     1222                $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', $this->user ) );
     1223                $request->set_body_params( array(
     1224                        'my_custom_int' => 'returnError',
     1225                ) );
     1226
     1227                $response = $this->server->dispatch( $request );
     1228
     1229                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1230
     1231                global $wp_rest_additional_fields;
     1232                $wp_rest_additional_fields = array();
     1233        }
     1234
     1235        public function additional_field_get_callback( $object ) {
     1236                return get_user_meta( $object['id'], 'my_custom_int', true );
     1237        }
     1238
     1239        public function additional_field_update_callback( $value, $user ) {
     1240                if ( 'returnError' === $value ) {
     1241                        return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) );
     1242                }
     1243                update_user_meta( $user->ID, 'my_custom_int', $value );
     1244        }
     1245
     1246        public function tearDown() {
     1247                parent::tearDown();
     1248        }
     1249
     1250        protected function check_user_data( $user, $data, $context, $links ) {
     1251                $this->assertEquals( $user->ID, $data['id'] );
     1252                $this->assertEquals( $user->display_name, $data['name'] );
     1253                $this->assertEquals( $user->user_url, $data['url'] );
     1254                $this->assertEquals( $user->description, $data['description'] );
     1255                $this->assertEquals( get_author_posts_url( $user->ID ), $data['link'] );
     1256                $this->assertArrayHasKey( 'avatar_urls', $data );
     1257                $this->assertEquals( $user->user_nicename, $data['slug'] );
     1258
     1259                if ( 'edit' === $context ) {
     1260                        $this->assertEquals( $user->first_name, $data['first_name'] );
     1261                        $this->assertEquals( $user->last_name, $data['last_name'] );
     1262                        $this->assertEquals( $user->nickname, $data['nickname'] );
     1263                        $this->assertEquals( $user->user_email, $data['email'] );
     1264                        $this->assertEquals( (object) $user->allcaps, $data['capabilities'] );
     1265                        $this->assertEquals( (object) $user->caps, $data['extra_capabilities'] );
     1266                        $this->assertEquals( date( 'c', strtotime( $user->user_registered ) ), $data['registered_date'] );
     1267                        $this->assertEquals( $user->user_login, $data['username'] );
     1268                        $this->assertEquals( $user->roles, $data['roles'] );
     1269                }
     1270
     1271                if ( 'edit' !== $context ) {
     1272                        $this->assertArrayNotHasKey( 'roles', $data );
     1273                        $this->assertArrayNotHasKey( 'capabilities', $data );
     1274                        $this->assertArrayNotHasKey( 'registered', $data );
     1275                        $this->assertArrayNotHasKey( 'first_name', $data );
     1276                        $this->assertArrayNotHasKey( 'last_name', $data );
     1277                        $this->assertArrayNotHasKey( 'nickname', $data );
     1278                        $this->assertArrayNotHasKey( 'extra_capabilities', $data );
     1279                        $this->assertArrayNotHasKey( 'username', $data );
     1280                }
     1281
     1282                $this->assertEqualSets( array(
     1283                        'self',
     1284                        'collection',
     1285                ), array_keys( $links ) );
     1286
     1287                $this->assertArrayNotHasKey( 'password', $data );
     1288        }
     1289
     1290        protected function check_get_user_response( $response, $context = 'view' ) {
     1291                $this->assertEquals( 200, $response->get_status() );
     1292
     1293                $data = $response->get_data();
     1294                $userdata = get_userdata( $data['id'] );
     1295                $this->check_user_data( $userdata, $data, $context, $response->get_links() );
     1296        }
     1297
     1298        protected function check_add_edit_user_response( $response, $update = false ) {
     1299                if ( $update ) {
     1300                        $this->assertEquals( 200, $response->get_status() );
     1301                } else {
     1302                        $this->assertEquals( 201, $response->get_status() );
     1303                }
     1304
     1305                $data = $response->get_data();
     1306                $userdata = get_userdata( $data['id'] );
     1307                $this->check_user_data( $userdata, $data, 'edit', $response->get_links() );
     1308        }
     1309
     1310        protected function allow_user_to_manage_multisite() {
     1311                wp_set_current_user( $this->user );
     1312                $user = wp_get_current_user();
     1313
     1314                if ( is_multisite() ) {
     1315                        update_site_option( 'site_admins', array( $user->user_login ) );
     1316                }
     1317
     1318                return;
     1319        }
     1320}
  • tests/phpunit/tests/rest-api.php

     
    2626                $this->assertTrue( class_exists( 'WP_REST_Server' ) );
    2727                $this->assertTrue( class_exists( 'WP_REST_Request' ) );
    2828                $this->assertTrue( class_exists( 'WP_REST_Response' ) );
     29                $this->assertTrue( class_exists( 'WP_REST_Posts_Controller' ) );
    2930        }
    3031
    3132        /**
     
    3637                $this->assertEquals( 10, has_action( 'init', 'rest_api_init' ) );
    3738        }
    3839
     40        public function test_add_extra_api_taxonomy_arguments() {
     41                $taxonomy = get_taxonomy( 'category' );
     42                $this->assertTrue( $taxonomy->show_in_rest );
     43                $this->assertEquals( 'categories', $taxonomy->rest_base );
     44                $this->assertEquals( 'WP_REST_Terms_Controller', $taxonomy->rest_controller_class );
     45
     46                $taxonomy = get_taxonomy( 'post_tag' );
     47                $this->assertTrue( $taxonomy->show_in_rest );
     48                $this->assertEquals( 'tags', $taxonomy->rest_base );
     49                $this->assertEquals( 'WP_REST_Terms_Controller', $taxonomy->rest_controller_class );
     50        }
     51
     52        public function test_add_extra_api_post_type_arguments() {
     53                $post_type = get_post_type_object( 'post' );
     54                $this->assertTrue( $post_type->show_in_rest );
     55                $this->assertEquals( 'posts', $post_type->rest_base );
     56                $this->assertEquals( 'WP_REST_Posts_Controller', $post_type->rest_controller_class );
     57
     58                $post_type = get_post_type_object( 'page' );
     59                $this->assertTrue( $post_type->show_in_rest );
     60                $this->assertEquals( 'pages', $post_type->rest_base );
     61                $this->assertEquals( 'WP_REST_Posts_Controller', $post_type->rest_controller_class );
     62
     63                $post_type = get_post_type_object( 'attachment' );
     64                $this->assertTrue( $post_type->show_in_rest );
     65                $this->assertEquals( 'media', $post_type->rest_base );
     66                $this->assertEquals( 'WP_REST_Attachments_Controller', $post_type->rest_controller_class );
     67        }
     68
    3969        /**
    4070         * Check that a single route is canonicalized.
    4171         *