]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/wp-api.js
WordPress 4.7-scripts
[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 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 valid 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 function 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 function to handle post Revisions.
435                          */
436                         RevisionsMixin = {
437                                 getRevisions: function() {
438                                         return buildCollectionGetter( this, 'PostRevisions' );
439                                 }
440                         },
441
442                         /**
443                          * Add a helper function 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 function 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 ) && ( _.isUndefined( wpApiSettings.cacheSchema ) || 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 } )();