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