]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/wp-api.js
WordPress 4.7.1
[autoinstalls/wordpress.git] / 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 default 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 default 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 valid 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 ( ! getModel.get( embedCheckField ) ) {
325                                         getModel.fetch( {
326                                                 success: function( getModel ) {
327                                                         deferred.resolve( getModel );
328                                                 },
329                                                 error: function( getModel, response ) {
330                                                         deferred.reject( response );
331                                                 }
332                                         } );
333                                 } else {
334                                         // Resolve with the embedded model.
335                                         deferred.resolve( getModel );
336                                 }
337
338                                 // Return a promise.
339                                 return deferred.promise();
340                         },
341
342                         /**
343                          * Build a helper to retrieve a collection.
344                          *
345                          * @param  {string} parentModel      The parent model.
346                          * @param  {string} collectionName   The name to use when constructing the collection.
347                          * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
348                          * @param  {string} embedIndex       An addiitonal optional index for the _embed data.
349                          *
350                          * @return {Deferred.promise}        A promise which resolves to the constructed collection.
351                          */
352                         buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) {
353                                 /**
354                                  * Returns a promise that resolves to the requested collection
355                                  *
356                                  * Uses the embedded data if available, otherwises fetches the
357                                  * data from the server.
358                                  *
359                                  * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ]
360                                  * collection.
361                                  */
362                                 var postId, embeddeds, getObjects,
363                                         classProperties = '',
364                                         properties      = '',
365                                         deferred        = jQuery.Deferred();
366
367                                 postId    = parentModel.get( 'id' );
368                                 embeddeds = parentModel.get( '_embedded' ) || {};
369
370                                 // Verify that we have a valid post id.
371                                 if ( ! _.isNumber( postId ) || 0 === postId ) {
372                                         deferred.reject();
373                                         return deferred;
374                                 }
375
376                                 // If we have embedded getObjects data, use that when constructing the getObjects.
377                                 if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) {
378
379                                         // Some embeds also include an index offset, check for that.
380                                         if ( _.isUndefined( embedIndex ) ) {
381
382                                                 // Use the embed source point directly.
383                                                 properties = embeddeds[ embedSourcePoint ];
384                                         } else {
385
386                                                 // Add the index to the embed source point.
387                                                 properties = embeddeds[ embedSourcePoint ][ embedIndex ];
388                                         }
389                                 } else {
390
391                                         // Otherwise use the postId.
392                                         classProperties = { parent: postId };
393                                 }
394
395                                 // Create the new getObjects collection.
396                                 getObjects = new wp.api.collections[ collectionName ]( properties, classProperties );
397
398                                 // If we didn’t have embedded getObjects, fetch the getObjects data.
399                                 if ( _.isUndefined( getObjects.models[0] ) ) {
400                                         getObjects.fetch( {
401                                                 success: function( getObjects ) {
402
403                                                         // Add a helper 'parent_post' attribute onto the model.
404                                                         setHelperParentPost( getObjects, postId );
405                                                         deferred.resolve( getObjects );
406                                                 },
407                                                 error: function( getModel, response ) {
408                                                         deferred.reject( response );
409                                                 }
410                                         } );
411                                 } else {
412
413                                         // Add a helper 'parent_post' attribute onto the model.
414                                         setHelperParentPost( getObjects, postId );
415                                         deferred.resolve( getObjects );
416                                 }
417
418                                 // Return a promise.
419                                 return deferred.promise();
420
421                         },
422
423                         /**
424                          * Set the model post parent.
425                          */
426                         setHelperParentPost = function( collection, postId ) {
427
428                                 // Attach post_parent id to the collection.
429                                 _.each( collection.models, function( model ) {
430                                         model.set( 'parent_post', postId );
431                                 } );
432                         },
433
434                         /**
435                          * Add a helper function to handle post Meta.
436                          */
437                         MetaMixin = {
438                                 getMeta: function() {
439                                         return buildCollectionGetter( this, 'PostMeta', 'https://api.w.org/meta' );
440                                 }
441                         },
442
443                         /**
444                          * Add a helper function to handle post Revisions.
445                          */
446                         RevisionsMixin = {
447                                 getRevisions: function() {
448                                         return buildCollectionGetter( this, 'PostRevisions' );
449                                 }
450                         },
451
452                         /**
453                          * Add a helper function to handle post Tags.
454                          */
455                         TagsMixin = {
456
457                                 /**
458                                  * Get the tags for a post.
459                                  *
460                                  * @return {Deferred.promise} promise Resolves to an array of tags.
461                                  */
462                                 getTags: function() {
463                                         var tagIds = this.get( 'tags' ),
464                                                 tags  = new wp.api.collections.Tags();
465
466                                         // Resolve with an empty array if no tags.
467                                         if ( _.isEmpty( tagIds ) ) {
468                                                 return jQuery.Deferred().resolve( [] );
469                                         }
470
471                                         return tags.fetch( { data: { include: tagIds } } );
472                                 },
473
474                                 /**
475                                  * Set the tags for a post.
476                                  *
477                                  * Accepts an array of tag slugs, or a Tags collection.
478                                  *
479                                  * @param {array|Backbone.Collection} tags The tags to set on the post.
480                                  *
481                                  */
482                                 setTags: function( tags ) {
483                                         var allTags, newTag,
484                                                 self = this,
485                                                 newTags = [];
486
487                                         if ( _.isString( tags ) ) {
488                                                 return false;
489                                         }
490
491                                         // If this is an array of slugs, build a collection.
492                                         if ( _.isArray( tags ) ) {
493
494                                                 // Get all the tags.
495                                                 allTags = new wp.api.collections.Tags();
496                                                 allTags.fetch( {
497                                                         data:    { per_page: 100 },
498                                                         success: function( alltags ) {
499
500                                                                 // Find the passed tags and set them up.
501                                                                 _.each( tags, function( tag ) {
502                                                                         newTag = new wp.api.models.Tag( alltags.findWhere( { slug: tag } ) );
503
504                                                                         // Tie the new tag to the post.
505                                                                         newTag.set( 'parent_post', self.get( 'id' ) );
506
507                                                                         // Add the new tag to the collection.
508                                                                         newTags.push( newTag );
509                                                                 } );
510                                                                 tags = new wp.api.collections.Tags( newTags );
511                                                                 self.setTagsWithCollection( tags );
512                                                         }
513                                                 } );
514
515                                         } else {
516                                                 this.setTagsWithCollection( tags );
517                                         }
518                                 },
519
520                                 /**
521                                  * Set the tags for a post.
522                                  *
523                                  * Accepts a Tags collection.
524                                  *
525                                  * @param {array|Backbone.Collection} tags The tags to set on the post.
526                                  *
527                                  */
528                                 setTagsWithCollection: function( tags ) {
529
530                                         // Pluck out the category ids.
531                                         this.set( 'tags', tags.pluck( 'id' ) );
532                                         return this.save();
533                                 }
534                         },
535
536                         /**
537                          * Add a helper function to handle post Categories.
538                          */
539                         CategoriesMixin = {
540
541                                 /**
542                                  * Get a the categories for a post.
543                                  *
544                                  * @return {Deferred.promise} promise Resolves to an array of categories.
545                                  */
546                                 getCategories: function() {
547                                         var categoryIds = this.get( 'categories' ),
548                                                 categories  = new wp.api.collections.Categories();
549
550                                         // Resolve with an empty array if no categories.
551                                         if ( _.isEmpty( categoryIds ) ) {
552                                                 return jQuery.Deferred().resolve( [] );
553                                         }
554
555                                         return categories.fetch( { data: { include: categoryIds } } );
556                                 },
557
558                                 /**
559                                  * Set the categories for a post.
560                                  *
561                                  * Accepts an array of category slugs, or a Categories collection.
562                                  *
563                                  * @param {array|Backbone.Collection} categories The categories to set on the post.
564                                  *
565                                  */
566                                 setCategories: function( categories ) {
567                                         var allCategories, newCategory,
568                                                 self = this,
569                                                 newCategories = [];
570
571                                         if ( _.isString( categories ) ) {
572                                                 return false;
573                                         }
574
575                                         // If this is an array of slugs, build a collection.
576                                         if ( _.isArray( categories ) ) {
577
578                                                 // Get all the categories.
579                                                 allCategories = new wp.api.collections.Categories();
580                                                 allCategories.fetch( {
581                                                         data:    { per_page: 100 },
582                                                         success: function( allcats ) {
583
584                                                                 // Find the passed categories and set them up.
585                                                                 _.each( categories, function( category ) {
586                                                                         newCategory = new wp.api.models.Category( allcats.findWhere( { slug: category } ) );
587
588                                                                         // Tie the new category to the post.
589                                                                         newCategory.set( 'parent_post', self.get( 'id' ) );
590
591                                                                         // Add the new category to the collection.
592                                                                         newCategories.push( newCategory );
593                                                                 } );
594                                                                 categories = new wp.api.collections.Categories( newCategories );
595                                                                 self.setCategoriesWithCollection( categories );
596                                                         }
597                                                 } );
598
599                                         } else {
600                                                 this.setCategoriesWithCollection( categories );
601                                         }
602
603                                 },
604
605                                 /**
606                                  * Set the categories for a post.
607                                  *
608                                  * Accepts Categories collection.
609                                  *
610                                  * @param {array|Backbone.Collection} categories The categories to set on the post.
611                                  *
612                                  */
613                                 setCategoriesWithCollection: function( categories ) {
614
615                                         // Pluck out the category ids.
616                                         this.set( 'categories', categories.pluck( 'id' ) );
617                                         return this.save();
618                                 }
619                         },
620
621                         /**
622                          * Add a helper function to retrieve the author user model.
623                          */
624                         AuthorMixin = {
625                                 getAuthorUser: function() {
626                                         return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' );
627                                 }
628                         },
629
630                         /**
631                          * Add a helper function to retrieve the featured media.
632                          */
633                         FeaturedMediaMixin = {
634                                 getFeaturedMedia: function() {
635                                         return buildModelGetter( this, this.get( 'featured_media' ), 'Media', 'wp:featuredmedia', 'source_url' );
636                                 }
637                         };
638
639                 // Exit if we don't have valid model defaults.
640                 if ( _.isUndefined( model.prototype.args ) ) {
641                         return model;
642                 }
643
644                 // Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin.
645                 _.each( parseableDates, function( theDateKey ) {
646                         if ( ! _.isUndefined( model.prototype.args[ theDateKey ] ) ) {
647                                 hasDate = true;
648                         }
649                 } );
650
651                 // Add the TimeStampedMixin for models that contain a date field.
652                 if ( hasDate ) {
653                         model = model.extend( TimeStampedMixin );
654                 }
655
656                 // Add the AuthorMixin for models that contain an author.
657                 if ( ! _.isUndefined( model.prototype.args.author ) ) {
658                         model = model.extend( AuthorMixin );
659                 }
660
661                 // Add the FeaturedMediaMixin for models that contain a featured_media.
662                 if ( ! _.isUndefined( model.prototype.args.featured_media ) ) {
663                         model = model.extend( FeaturedMediaMixin );
664                 }
665
666                 // Add the CategoriesMixin for models that support categories collections.
667                 if ( ! _.isUndefined( model.prototype.args.categories ) ) {
668                         model = model.extend( CategoriesMixin );
669                 }
670
671                 // Add the MetaMixin for models that support meta collections.
672                 if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Meta' ] ) ) {
673                         model = model.extend( MetaMixin );
674                 }
675
676                 // Add the TagsMixin for models that support tags collections.
677                 if ( ! _.isUndefined( model.prototype.args.tags ) ) {
678                         model = model.extend( TagsMixin );
679                 }
680
681                 // Add the RevisionsMixin for models that support revisions collections.
682                 if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) {
683                         model = model.extend( RevisionsMixin );
684                 }
685
686                 return model;
687         };
688
689 })( window );
690
691 /* global wpApiSettings:false */
692
693 // Suppress warning about parse function's unused "options" argument:
694 /* jshint unused:false */
695 (function() {
696
697         'use strict';
698
699         var wpApiSettings = window.wpApiSettings || {};
700
701         /**
702          * Backbone base model for all models.
703          */
704         wp.api.WPApiBaseModel = Backbone.Model.extend(
705                 /** @lends WPApiBaseModel.prototype  */
706                 {
707                         /**
708                          * Set nonce header before every Backbone sync.
709                          *
710                          * @param {string} method.
711                          * @param {Backbone.Model} model.
712                          * @param {{beforeSend}, *} options.
713                          * @returns {*}.
714                          */
715                         sync: function( method, model, options ) {
716                                 var beforeSend;
717
718                                 options = options || {};
719
720                                 // Remove date_gmt if null.
721                                 if ( _.isNull( model.get( 'date_gmt' ) ) ) {
722                                         model.unset( 'date_gmt' );
723                                 }
724
725                                 // Remove slug if empty.
726                                 if ( _.isEmpty( model.get( 'slug' ) ) ) {
727                                         model.unset( 'slug' );
728                                 }
729
730                                 if ( ! _.isUndefined( wpApiSettings.nonce ) && ! _.isNull( wpApiSettings.nonce ) ) {
731                                         beforeSend = options.beforeSend;
732
733                                         // @todo enable option for jsonp endpoints
734                                         // options.dataType = 'jsonp';
735
736                                         options.beforeSend = function( xhr ) {
737                                                 xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
738
739                                                 if ( beforeSend ) {
740                                                         return beforeSend.apply( this, arguments );
741                                                 }
742                                         };
743                                 }
744
745                                 // Add '?force=true' to use delete method when required.
746                                 if ( this.requireForceForDelete && 'delete' === method ) {
747                                         model.url = model.url() + '?force=true';
748                                 }
749                                 return Backbone.sync( method, model, options );
750                         },
751
752                         /**
753                          * Save is only allowed when the PUT OR POST methods are available for the endpoint.
754                          */
755                         save: function( attrs, options ) {
756
757                                 // Do we have the put method, then execute the save.
758                                 if ( _.includes( this.methods, 'PUT' ) || _.includes( this.methods, 'POST' ) ) {
759
760                                         // Proxy the call to the original save function.
761                                         return Backbone.Model.prototype.save.call( this, attrs, options );
762                                 } else {
763
764                                         // Otherwise bail, disallowing action.
765                                         return false;
766                                 }
767                         },
768
769                         /**
770                          * Delete is only allowed when the DELETE method is available for the endpoint.
771                          */
772                         destroy: function( options ) {
773
774                                 // Do we have the DELETE method, then execute the destroy.
775                                 if ( _.includes( this.methods, 'DELETE' ) ) {
776
777                                         // Proxy the call to the original save function.
778                                         return Backbone.Model.prototype.destroy.call( this, options );
779                                 } else {
780
781                                         // Otherwise bail, disallowing action.
782                                         return false;
783                                 }
784                         }
785
786                 }
787         );
788
789         /**
790          * API Schema model. Contains meta information about the API.
791          */
792         wp.api.models.Schema = wp.api.WPApiBaseModel.extend(
793                 /** @lends Schema.prototype  */
794                 {
795                         defaults: {
796                                 _links: {},
797                                 namespace: null,
798                                 routes: {}
799                         },
800
801                         initialize: function( attributes, options ) {
802                                 var model = this;
803                                 options = options || {};
804
805                                 wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options );
806
807                                 model.apiRoot = options.apiRoot || wpApiSettings.root;
808                                 model.versionString = options.versionString || wpApiSettings.versionString;
809                         },
810
811                         url: function() {
812                                 return this.apiRoot + this.versionString;
813                         }
814                 }
815         );
816 })();
817
818 ( function() {
819
820         'use strict';
821
822         var wpApiSettings = window.wpApiSettings || {};
823
824         /**
825          * Contains basic collection functionality such as pagination.
826          */
827         wp.api.WPApiBaseCollection = Backbone.Collection.extend(
828                 /** @lends BaseCollection.prototype  */
829                 {
830
831                         /**
832                          * Setup default state.
833                          */
834                         initialize: function( models, options ) {
835                                 this.state = {
836                                         data: {},
837                                         currentPage: null,
838                                         totalPages: null,
839                                         totalObjects: null
840                                 };
841                                 if ( _.isUndefined( options ) ) {
842                                         this.parent = '';
843                                 } else {
844                                         this.parent = options.parent;
845                                 }
846                         },
847
848                         /**
849                          * Extend Backbone.Collection.sync to add nince and pagination support.
850                          *
851                          * Set nonce header before every Backbone sync.
852                          *
853                          * @param {string} method.
854                          * @param {Backbone.Model} model.
855                          * @param {{success}, *} options.
856                          * @returns {*}.
857                          */
858                         sync: function( method, model, options ) {
859                                 var beforeSend, success,
860                                         self = this;
861
862                                 options    = options || {};
863                                 beforeSend = options.beforeSend;
864
865                                 // If we have a localized nonce, pass that along with each sync.
866                                 if ( 'undefined' !== typeof wpApiSettings.nonce ) {
867                                         options.beforeSend = function( xhr ) {
868                                                 xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
869
870                                                 if ( beforeSend ) {
871                                                         return beforeSend.apply( self, arguments );
872                                                 }
873                                         };
874                                 }
875
876                                 // When reading, add pagination data.
877                                 if ( 'read' === method ) {
878                                         if ( options.data ) {
879                                                 self.state.data = _.clone( options.data );
880
881                                                 delete self.state.data.page;
882                                         } else {
883                                                 self.state.data = options.data = {};
884                                         }
885
886                                         if ( 'undefined' === typeof options.data.page ) {
887                                                 self.state.currentPage  = null;
888                                                 self.state.totalPages   = null;
889                                                 self.state.totalObjects = null;
890                                         } else {
891                                                 self.state.currentPage = options.data.page - 1;
892                                         }
893
894                                         success = options.success;
895                                         options.success = function( data, textStatus, request ) {
896                                                 if ( ! _.isUndefined( request ) ) {
897                                                         self.state.totalPages   = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 );
898                                                         self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 );
899                                                 }
900
901                                                 if ( null === self.state.currentPage ) {
902                                                         self.state.currentPage = 1;
903                                                 } else {
904                                                         self.state.currentPage++;
905                                                 }
906
907                                                 if ( success ) {
908                                                         return success.apply( this, arguments );
909                                                 }
910                                         };
911                                 }
912
913                                 // Continue by calling Bacckbone's sync.
914                                 return Backbone.sync( method, model, options );
915                         },
916
917                         /**
918                          * Fetches the next page of objects if a new page exists.
919                          *
920                          * @param {data: {page}} options.
921                          * @returns {*}.
922                          */
923                         more: function( options ) {
924                                 options = options || {};
925                                 options.data = options.data || {};
926
927                                 _.extend( options.data, this.state.data );
928
929                                 if ( 'undefined' === typeof options.data.page ) {
930                                         if ( ! this.hasMore() ) {
931                                                 return false;
932                                         }
933
934                                         if ( null === this.state.currentPage || this.state.currentPage <= 1 ) {
935                                                 options.data.page = 2;
936                                         } else {
937                                                 options.data.page = this.state.currentPage + 1;
938                                         }
939                                 }
940
941                                 return this.fetch( options );
942                         },
943
944                         /**
945                          * Returns true if there are more pages of objects available.
946                          *
947                          * @returns null|boolean.
948                          */
949                         hasMore: function() {
950                                 if ( null === this.state.totalPages ||
951                                          null === this.state.totalObjects ||
952                                          null === this.state.currentPage ) {
953                                         return null;
954                                 } else {
955                                         return ( this.state.currentPage < this.state.totalPages );
956                                 }
957                         }
958                 }
959         );
960
961 } )();
962
963 ( function() {
964
965         'use strict';
966
967         var Endpoint, initializedDeferreds = {},
968                 wpApiSettings = window.wpApiSettings || {};
969         window.wp = window.wp || {};
970         wp.api    = wp.api || {};
971
972         // If wpApiSettings is unavailable, try the default.
973         if ( _.isEmpty( wpApiSettings ) ) {
974                 wpApiSettings.root = window.location.origin + '/wp-json/';
975         }
976
977         Endpoint = Backbone.Model.extend( {
978                 defaults: {
979                         apiRoot: wpApiSettings.root,
980                         versionString: wp.api.versionString,
981                         schema: null,
982                         models: {},
983                         collections: {}
984                 },
985
986                 /**
987                  * Initialize the Endpoint model.
988                  */
989                 initialize: function() {
990                         var model = this, deferred;
991
992                         Backbone.Model.prototype.initialize.apply( model, arguments );
993
994                         deferred = jQuery.Deferred();
995                         model.schemaConstructed = deferred.promise();
996
997                         model.schemaModel = new wp.api.models.Schema( null, {
998                                 apiRoot: model.get( 'apiRoot' ),
999                                 versionString: model.get( 'versionString' )
1000                         } );
1001
1002                         // When the model loads, resolve the promise.
1003                         model.schemaModel.once( 'change', function() {
1004                                 model.constructFromSchema();
1005                                 deferred.resolve( model );
1006                         } );
1007
1008                         if ( model.get( 'schema' ) ) {
1009
1010                                 // Use schema supplied as model attribute.
1011                                 model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) );
1012                         } else if (
1013                                 ! _.isUndefined( sessionStorage ) &&
1014                                 ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) &&
1015                                 sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) )
1016                         ) {
1017
1018                                 // Used a cached copy of the schema model if available.
1019                                 model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) );
1020                         } else {
1021                                 model.schemaModel.fetch( {
1022                                         /**
1023                                          * When the server returns the schema model data, store the data in a sessionCache so we don't
1024                                          * have to retrieve it again for this session. Then, construct the models and collections based
1025                                          * on the schema model data.
1026                                          */
1027                                         success: function( newSchemaModel ) {
1028
1029                                                 // Store a copy of the schema model in the session cache if available.
1030                                                 if ( ! _.isUndefined( sessionStorage ) && ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) ) {
1031                                                         try {
1032                                                                 sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) );
1033                                                         } catch ( error ) {
1034
1035                                                                 // Fail silently, fixes errors in safari private mode.
1036                                                         }
1037                                                 }
1038                                         },
1039
1040                                         // Log the error condition.
1041                                         error: function( err ) {
1042                                                 window.console.log( err );
1043                                         }
1044                                 } );
1045                         }
1046                 },
1047
1048                 constructFromSchema: function() {
1049                         var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects,
1050
1051                         /**
1052                          * Set up the model and collection name mapping options. As the schema is built, the
1053                          * model and collection names will be adjusted if they are found in the mapping object.
1054                          *
1055                          * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options.
1056                          *
1057                          */
1058                         mapping = wpApiSettings.mapping || {
1059                                 models: {
1060                                         'Categories':      'Category',
1061                                         'Comments':        'Comment',
1062                                         'Pages':           'Page',
1063                                         'PagesMeta':       'PageMeta',
1064                                         'PagesRevisions':  'PageRevision',
1065                                         'Posts':           'Post',
1066                                         'PostsCategories': 'PostCategory',
1067                                         'PostsRevisions':  'PostRevision',
1068                                         'PostsTags':       'PostTag',
1069                                         'Schema':          'Schema',
1070                                         'Statuses':        'Status',
1071                                         'Tags':            'Tag',
1072                                         'Taxonomies':      'Taxonomy',
1073                                         'Types':           'Type',
1074                                         'Users':           'User'
1075                                 },
1076                                 collections: {
1077                                         'PagesMeta':       'PageMeta',
1078                                         'PagesRevisions':  'PageRevisions',
1079                                         'PostsCategories': 'PostCategories',
1080                                         'PostsMeta':       'PostMeta',
1081                                         'PostsRevisions':  'PostRevisions',
1082                                         'PostsTags':       'PostTags'
1083                                 }
1084                         };
1085
1086                         /**
1087                          * Iterate thru the routes, picking up models and collections to build. Builds two arrays,
1088                          * one for models and one for collections.
1089                          */
1090                         modelRoutes      = [];
1091                         collectionRoutes = [];
1092                         schemaRoot       = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' );
1093                         loadingObjects   = {};
1094
1095                         /**
1096                          * Tracking objects for models and collections.
1097                          */
1098                         loadingObjects.models      = routeModel.get( 'models' );
1099                         loadingObjects.collections = routeModel.get( 'collections' );
1100
1101                         _.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) {
1102
1103                                 // Skip the schema root if included in the schema.
1104                                 if ( index !== routeModel.get( ' versionString' ) &&
1105                                                 index !== schemaRoot &&
1106                                                 index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) )
1107                                 ) {
1108
1109                                         // Single items end with a regex (or the special case 'me').
1110                                         if ( /(?:.*[+)]|\/me)$/.test( index ) ) {
1111                                                 modelRoutes.push( { index: index, route: route } );
1112                                         } else {
1113
1114                                                 // Collections end in a name.
1115                                                 collectionRoutes.push( { index: index, route: route } );
1116                                         }
1117                                 }
1118                         } );
1119
1120                         /**
1121                          * Construct the models.
1122                          *
1123                          * Base the class name on the route endpoint.
1124                          */
1125                         _.each( modelRoutes, function( modelRoute ) {
1126
1127                                 // Extract the name and any parent from the route.
1128                                 var modelClassName,
1129                                                 routeName  = wp.api.utils.extractRoutePart( modelRoute.index, 2 ),
1130                                                 parentName = wp.api.utils.extractRoutePart( modelRoute.index, 4 ),
1131                                                 routeEnd   = wp.api.utils.extractRoutePart( modelRoute.index, 1 );
1132
1133                                 // Handle the special case of the 'me' route.
1134                                 if ( 'me' === routeEnd ) {
1135                                         routeName = 'me';
1136                                 }
1137
1138                                 // If the model has a parent in its route, add that to its class name.
1139                                 if ( '' !== parentName && parentName !== routeName ) {
1140                                         modelClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName );
1141                                         modelClassName = mapping.models[ modelClassName ] || modelClassName;
1142                                         loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
1143
1144                                                 // Return a constructed url based on the parent and id.
1145                                                 url: function() {
1146                                                         var url = routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
1147                                                                         parentName +  '/' +
1148                                                                         ( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ?
1149                                                                                 this.get( 'parent_post' ) :
1150                                                                                 this.get( 'parent' ) ) + '/' +
1151                                                                         routeName;
1152                                                         if ( ! _.isUndefined( this.get( 'id' ) ) ) {
1153                                                                 url +=  '/' + this.get( 'id' );
1154                                                         }
1155                                                         return url;
1156                                                 },
1157
1158                                                 // Include a reference to the original route object.
1159                                                 route: modelRoute,
1160
1161                                                 // Include a reference to the original class name.
1162                                                 name: modelClassName,
1163
1164                                                 // Include the array of route methods for easy reference.
1165                                                 methods: modelRoute.route.methods,
1166
1167                                                 initialize: function() {
1168
1169                                                         /**
1170                                                          * Posts and pages support trashing, other types don't support a trash
1171                                                          * and require that you pass ?force=true to actually delete them.
1172                                                          *
1173                                                          * @todo we should be getting trashability from the Schema, not hard coding types here.
1174                                                          */
1175                                                         if (
1176                                                                 'Posts' !== this.name &&
1177                                                                 'Pages' !== this.name &&
1178                                                                 _.includes( this.methods, 'DELETE' )
1179                                                         ) {
1180                                                                 this.requireForceForDelete = true;
1181                                                         }
1182                                                 }
1183                                         } );
1184                                 } else {
1185
1186                                         // This is a model without a parent in its route
1187                                         modelClassName = wp.api.utils.capitalize( routeName );
1188                                         modelClassName = mapping.models[ modelClassName ] || modelClassName;
1189                                         loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
1190
1191                                                 // Function that returns a constructed url based on the id.
1192                                                 url: function() {
1193                                                         var url = routeModel.get( 'apiRoot' ) +
1194                                                                 routeModel.get( 'versionString' ) +
1195                                                                 ( ( 'me' === routeName ) ? 'users/me' : routeName );
1196
1197                                                         if ( ! _.isUndefined( this.get( 'id' ) ) ) {
1198                                                                 url +=  '/' + this.get( 'id' );
1199                                                         }
1200                                                         return url;
1201                                                 },
1202
1203                                                 // Include a reference to the original route object.
1204                                                 route: modelRoute,
1205
1206                                                 // Include a reference to the original class name.
1207                                                 name: modelClassName,
1208
1209                                                 // Include the array of route methods for easy reference.
1210                                                 methods: modelRoute.route.methods
1211                                         } );
1212                                 }
1213
1214                                 // Add defaults to the new model, pulled form the endpoint.
1215                                 wp.api.utils.decorateFromRoute( modelRoute.route.endpoints, loadingObjects.models[ modelClassName ] );
1216
1217                         } );
1218
1219                         /**
1220                          * Construct the collections.
1221                          *
1222                          * Base the class name on the route endpoint.
1223                          */
1224                         _.each( collectionRoutes, function( collectionRoute ) {
1225
1226                                 // Extract the name and any parent from the route.
1227                                 var collectionClassName, modelClassName,
1228                                                 routeName  = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ),
1229                                                 parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 3 );
1230
1231                                 // If the collection has a parent in its route, add that to its class name.
1232                                 if ( '' !== parentName && parentName !== routeName ) {
1233
1234                                         collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName );
1235                                         modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
1236                                         collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
1237                                         loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
1238
1239                                                 // Function that returns a constructed url passed on the parent.
1240                                                 url: function() {
1241                                                         return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
1242                                                                         parentName + '/' + this.parent + '/' +
1243                                                                         routeName;
1244                                                 },
1245
1246                                                 // Specify the model that this collection contains.
1247                                                 model: function( attrs, options ) {
1248                                                         return new loadingObjects.models[ modelClassName ]( attrs, options );
1249                                                 },
1250
1251                                                 // Include a reference to the original class name.
1252                                                 name: collectionClassName,
1253
1254                                                 // Include a reference to the original route object.
1255                                                 route: collectionRoute,
1256
1257                                                 // Include the array of route methods for easy reference.
1258                                                 methods: collectionRoute.route.methods
1259                                         } );
1260                                 } else {
1261
1262                                         // This is a collection without a parent in its route.
1263                                         collectionClassName = wp.api.utils.capitalize( routeName );
1264                                         modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
1265                                         collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
1266                                         loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
1267
1268                                                 // For the url of a root level collection, use a string.
1269                                                 url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName,
1270
1271                                                 // Specify the model that this collection contains.
1272                                                 model: function( attrs, options ) {
1273                                                         return new loadingObjects.models[ modelClassName ]( attrs, options );
1274                                                 },
1275
1276                                                 // Include a reference to the original class name.
1277                                                 name: collectionClassName,
1278
1279                                                 // Include a reference to the original route object.
1280                                                 route: collectionRoute,
1281
1282                                                 // Include the array of route methods for easy reference.
1283                                                 methods: collectionRoute.route.methods
1284                                         } );
1285                                 }
1286
1287                                 // Add defaults to the new model, pulled form the endpoint.
1288                                 wp.api.utils.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] );
1289                         } );
1290
1291                         // Add mixins and helpers for each of the models.
1292                         _.each( loadingObjects.models, function( model, index ) {
1293                                 loadingObjects.models[ index ] = wp.api.utils.addMixinsAndHelpers( model, index, loadingObjects );
1294                         } );
1295
1296                 }
1297
1298         } );
1299
1300         wp.api.endpoints = new Backbone.Collection( {
1301                 model: Endpoint
1302         } );
1303
1304         /**
1305          * Initialize the wp-api, optionally passing the API root.
1306          *
1307          * @param {object} [args]
1308          * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root.
1309          * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root.
1310          * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided.
1311          */
1312         wp.api.init = function( args ) {
1313                 var endpoint, attributes = {}, deferred, promise;
1314
1315                 args                     = args || {};
1316                 attributes.apiRoot       = args.apiRoot || wpApiSettings.root;
1317                 attributes.versionString = args.versionString || wpApiSettings.versionString;
1318                 attributes.schema        = args.schema || null;
1319                 if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) {
1320                         attributes.schema = wpApiSettings.schema;
1321                 }
1322
1323                 if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) {
1324                         endpoint = wp.api.endpoints.findWhere( { apiRoot: attributes.apiRoot, versionString: attributes.versionString } );
1325                         if ( ! endpoint ) {
1326                                 endpoint = new Endpoint( attributes );
1327                                 wp.api.endpoints.add( endpoint );
1328                         }
1329                         deferred = jQuery.Deferred();
1330                         promise = deferred.promise();
1331
1332                         endpoint.schemaConstructed.done( function( endpoint ) {
1333
1334                                 // Map the default endpoints, extending any already present items (including Schema model).
1335                                 wp.api.models      = _.extend( endpoint.get( 'models' ), wp.api.models );
1336                                 wp.api.collections = _.extend( endpoint.get( 'collections' ), wp.api.collections );
1337                                 deferred.resolveWith( wp.api, [ endpoint ] );
1338                         } );
1339                         initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise;
1340                 }
1341                 return initializedDeferreds[ attributes.apiRoot + attributes.versionString ];
1342         };
1343
1344         /**
1345          * Construct the default endpoints and add to an endpoints collection.
1346          */
1347
1348         // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready.
1349         wp.api.loadPromise = wp.api.init();
1350
1351 } )();