WordPress.org

Make WordPress Core

Ticket #38373: 38373.2.diff

File 38373.2.diff, 780.5 KB (added by rachelbaker, 3 years ago)
  • 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', 'register_initial_settings',  10 );
     379add_action( 'rest_api_init', 'create_initial_rest_routes', 99 );
    378380add_action( 'parse_request', 'rest_api_loaded' );
    379381
    380382/**
  • 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

     
     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

     
     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

     
     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

     
     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

     
     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