NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / model-sync-rest / model-sync-rest-debug.js
blobb93760dea066ba2210d65ee13532171859c5ec2c
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('model-sync-rest', function (Y, NAME) {
10 /**
11 An extension which provides a RESTful XHR sync implementation that can be mixed
12 into a Model or ModelList subclass.
14 @module app
15 @submodule model-sync-rest
16 @since 3.6.0
17 **/
19 var Lang = Y.Lang;
21 /**
22 An extension which provides a RESTful XHR sync implementation that can be mixed
23 into a Model or ModelList subclass.
25 This makes it trivial for your Model or ModelList subclasses communicate and
26 transmit their data via RESTful XHRs. In most cases you'll only need to provide
27 a value for `root` when sub-classing `Y.Model`.
29     Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
30         root: '/users'
31     });
33     Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
34         // By convention `Y.User`'s `root` will be used for the lists' URL.
35         model: Y.User
36     });
38     var users = new Y.Users();
40     // GET users list from: "/users"
41     users.load(function () {
42         var firstUser = users.item(0);
44         firstUser.get('id'); // => "1"
46         // PUT updated user data at: "/users/1"
47         firstUser.set('name', 'Eric').save();
48     });
50 @class ModelSync.REST
51 @extensionfor Model
52 @extensionfor ModelList
53 @since 3.6.0
54 **/
55 function RESTSync() {}
57 /**
58 A request authenticity token to validate HTTP requests made by this extension
59 with the server when the request results in changing persistent state. This
60 allows you to protect your server from Cross-Site Request Forgery attacks.
62 A CSRF token provided by the server can be embedded in the HTML document and
63 assigned to `YUI.Env.CSRF_TOKEN` like this:
65     <script>
66         YUI.Env.CSRF_TOKEN = {{session.authenticityToken}};
67     </script>
69 The above should come after YUI seed file so that `YUI.Env` will be defined.
71 **Note:** This can be overridden on a per-request basis. See `sync()` method.
73 When a value for the CSRF token is provided, either statically or via `options`
74 passed to the `save()` and `destroy()` methods, the applicable HTTP requests
75 will have a `X-CSRF-Token` header added with the token value.
77 @property CSRF_TOKEN
78 @type String
79 @default YUI.Env.CSRF_TOKEN
80 @static
81 @since 3.6.0
82 **/
83 RESTSync.CSRF_TOKEN = YUI.Env.CSRF_TOKEN;
85 /**
86 Static flag to use the HTTP POST method instead of PUT or DELETE.
88 If the server-side HTTP framework isn't RESTful, setting this flag to `true`
89 will cause all PUT and DELETE requests to instead use the POST HTTP method, and
90 add a `X-HTTP-Method-Override` HTTP header with the value of the method type
91 which was overridden.
93 @property EMULATE_HTTP
94 @type Boolean
95 @default false
96 @static
97 @since 3.6.0
98 **/
99 RESTSync.EMULATE_HTTP = false;
102 Default headers used with all XHRs.
104 By default the `Accept` and `Content-Type` headers are set to
105 "application/json", this signals to the HTTP server to process the request
106 bodies as JSON and send JSON responses. If you're sending and receiving content
107 other than JSON, you can override these headers and the `parse()` and
108 `serialize()` methods.
110 **Note:** These headers will be merged with any request-specific headers, and
111 the request-specific headers will take precedence.
113 @property HTTP_HEADERS
114 @type Object
115 @default
116     {
117         "Accept"      : "application/json",
118         "Content-Type": "application/json"
119     }
120 @static
121 @since 3.6.0
123 RESTSync.HTTP_HEADERS = {
124     'Accept'      : 'application/json',
125     'Content-Type': 'application/json'
129 Static mapping of RESTful HTTP methods corresponding to CRUD actions.
131 @property HTTP_METHODS
132 @type Object
133 @default
134     {
135         "create": "POST",
136         "read"  : "GET",
137         "update": "PUT",
138         "delete": "DELETE"
139     }
140 @static
141 @since 3.6.0
143 RESTSync.HTTP_METHODS = {
144     'create': 'POST',
145     'read'  : 'GET',
146     'update': 'PUT',
147     'delete': 'DELETE'
151 The number of milliseconds before the XHRs will timeout/abort. This defaults to
152 30 seconds.
154 **Note:** This can be overridden on a per-request basis. See `sync()` method.
156 @property HTTP_TIMEOUT
157 @type Number
158 @default 30000
159 @static
160 @since 3.6.0
162 RESTSync.HTTP_TIMEOUT = 30000;
165 Properties that shouldn't be turned into ad-hoc attributes when passed to a
166 Model or ModelList constructor.
168 @property _NON_ATTRS_CFG
169 @type Array
170 @default ["root", "url"]
171 @static
172 @protected
173 @since 3.6.0
175 RESTSync._NON_ATTRS_CFG = ['root', 'url'];
177 RESTSync.prototype = {
179     // -- Public Properties ----------------------------------------------------
181     /**
182     A string which represents the root or collection part of the URL which
183     relates to a Model or ModelList. Usually this value should be same for all
184     instances of a specific Model/ModelList subclass.
186     When sub-classing `Y.Model`, usually you'll only need to override this
187     property, which lets the URLs for the XHRs be generated by convention. If
188     the `root` string ends with a trailing-slash, XHR URLs will also end with a
189     "/", and if the `root` does not end with a slash, neither will the XHR URLs.
191     @example
192         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
193             root: '/users'
194         });
196         var currentUser, newUser;
198         // GET the user data from: "/users/123"
199         currentUser = new Y.User({id: '123'}).load();
201         // POST the new user data to: "/users"
202         newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
204     When sub-classing `Y.ModelList`, usually you'll want to ignore configuring
205     the `root` and simply rely on the build-in convention of the list's
206     generated URLs defaulting to the `root` specified by the list's `model`.
208     @property root
209     @type String
210     @default ""
211     @since 3.6.0
212     **/
213     root: '',
215     /**
216     A string which specifies the URL to use when making XHRs, if not value is
217     provided, the URLs used to make XHRs will be generated by convention.
219     While a `url` can be provided for each Model/ModelList instance, usually
220     you'll want to either rely on the default convention or provide a tokenized
221     string on the prototype which can be used for all instances.
223     When sub-classing `Y.Model`, you will probably be able to rely on the
224     default convention of generating URLs in conjunction with the `root`
225     property and whether the model is new or not (i.e. has an `id`). If the
226     `root` property ends with a trailing-slash, the generated URL for the
227     specific model will also end with a trailing-slash.
229     @example
230         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
231             root: '/users/'
232         });
234         var currentUser, newUser;
236         // GET the user data from: "/users/123/"
237         currentUser = new Y.User({id: '123'}).load();
239         // POST the new user data to: "/users/"
240         newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
242     If a `url` is specified, it will be processed by `Y.Lang.sub()`, which is
243     useful when the URLs for a Model/ModelList subclass match a specific pattern
244     and can use simple replacement tokens; e.g.:
246     @example
247         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
248             root: '/users',
249             url : '/users/{username}'
250         });
252     **Note:** String subsitituion of the `url` only use string an number values
253     provided by this object's attribute and/or the `options` passed to the
254     `getURL()` method. Do not expect something fancy to happen with Object,
255     Array, or Boolean values, they will simply be ignored.
257     If your URLs have plural roots or collection URLs, while the specific item
258     resources are under a singular name, e.g. "/users" (plural) and "/user/123"
259     (singular), you'll probably want to configure the `root` and `url`
260     properties like this:
262     @example
263         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
264             root: '/users',
265             url : '/user/{id}'
266         });
268         var currentUser, newUser;
270         // GET the user data from: "/user/123"
271         currentUser = new Y.User({id: '123'}).load();
273         // POST the new user data to: "/users"
274         newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
276     When sub-classing `Y.ModelList`, usually you'll be able to rely on the
277     associated `model` to supply its `root` to be used as the model list's URL.
278     If this needs to be customized, you can provide a simple string for the
279     `url` property.
281     @example
282         Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
283             // Leverages `Y.User`'s `root`, which is "/users".
284             model: Y.User
285         });
287         // Or specified explicitly...
289         Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
290             model: Y.User,
291             url  : '/users'
292         });
294     @property url
295     @type String
296     @default ""
297     @since 3.6.0
298     **/
299     url: '',
301     // -- Lifecycle Methods ----------------------------------------------------
303     initializer: function (config) {
304         config || (config = {});
306         // Overrides `root` at the instance level.
307         if ('root' in config) {
308             this.root = config.root || '';
309         }
311         // Overrides `url` at the instance level.
312         if ('url' in config) {
313             this.url = config.url || '';
314         }
315     },
317     // -- Public Methods -------------------------------------------------------
319     /**
320     Returns the URL for this model or model list for the given `action` and
321     `options`, if specified.
323     This method correctly handles the variations of `root` and `url` values and
324     is called by the `sync()` method to get the URLs used to make the XHRs.
326     You can override this method if you need to provide a specific
327     implementation for how the URLs of your Model and ModelList subclasses need
328     to be generated.
330     @method getURL
331     @param {String} [action] Optional `sync()` action for which to generate the
332         URL.
333     @param {Object} [options] Optional options which may be used to help
334         generate the URL.
335     @return {String} this model's or model list's URL for the the given
336         `action` and `options`.
337     @since 3.6.0
338     **/
339     getURL: function (action, options) {
340         var root = this.root,
341             url  = this.url;
343         // If this is a model list, use its `url` and substitute placeholders,
344         // but default to the `root` of its `model`. By convention a model's
345         // `root` is the location to a collection resource.
346         if (this._isYUIModelList) {
347             if (!url) {
348                 return this.model.prototype.root;
349             }
351             return this._substituteURL(url, Y.merge(this.getAttrs(), options));
352         }
354         // Assume `this` is a model.
356         // When a model is new, i.e. has no `id`, the `root` should be used. By
357         // convention a model's `root` is the location to a collection resource.
358         // The model's `url` will be used as a fallback if `root` isn't defined.
359         if (root && (action === 'create' || this.isNew())) {
360             return root;
361         }
363         // When a model's `url` is not provided, we'll generate a URL to use by
364         // convention. This will combine the model's `id` with its configured
365         // `root` and add a trailing-slash if the root ends with "/".
366         if (!url) {
367             return this._joinURL(this.getAsURL('id') || '');
368         }
370         // Substitute placeholders in the `url` with URL-encoded values from the
371         // model's attribute values or the specified `options`.
372         return this._substituteURL(url, Y.merge(this.getAttrs(), options));
373     },
375     /**
376     Called to parse the response object returned from `Y.io()`. This method
377     receives the full response object and is expected to "prep" a response which
378     is suitable to pass to the `parse()` method.
380     By default the response body is returned (`responseText`), because it
381     usually represents the entire entity of this model on the server.
383     If you need to parse data out of the response's headers you should do so by
384     overriding this method. If you'd like the entire response object from the
385     XHR to be passed to your `parse()` method, you can simply assign this
386     property to `false`.
388     @method parseIOResponse
389     @param {Object} response Response object from `Y.io()`.
390     @return {Any} The modified response to pass along to the `parse()` method.
391     @since 3.7.0
392     **/
393     parseIOResponse: function (response) {
394         return response.responseText;
395     },
397     /**
398     Serializes `this` model to be used as the HTTP request entity body.
400     By default this model will be serialized to a JSON string via its `toJSON()`
401     method.
403     You can override this method when the HTTP server expects a different
404     representation of this model's data that is different from the default JSON
405     serialization. If you're sending and receive content other than JSON, be
406     sure change the `Accept` and `Content-Type` `HTTP_HEADERS` as well.
408     **Note:** A model's `toJSON()` method can also be overridden. If you only
409     need to modify which attributes are serialized to JSON, that's a better
410     place to start.
412     @method serialize
413     @param {String} [action] Optional `sync()` action for which to generate the
414         the serialized representation of this model.
415     @return {String} serialized HTTP request entity body.
416     @since 3.6.0
417     **/
418     serialize: function (action) {
419         return Y.JSON.stringify(this);
420     },
422     /**
423     Communicates with a RESTful HTTP server by sending and receiving data via
424     XHRs. This method is called internally by load(), save(), and destroy().
426     The URL used for each XHR will be retrieved by calling the `getURL()` method
427     and passing it the specified `action` and `options`.
429     This method relies heavily on standard RESTful HTTP conventions
431     @method sync
432     @param {String} action Sync action to perform. May be one of the following:
434       * `create`: Store a newly-created model for the first time.
435       * `delete`: Delete an existing model.
436       * `read`  : Load an existing model.
437       * `update`: Update an existing model.
439     @param {Object} [options] Sync options:
440       @param {String} [options.csrfToken] The authenticity token used by the
441         server to verify the validity of this request and protected against CSRF
442         attacks. This overrides the default value provided by the static
443         `CSRF_TOKEN` property.
444       @param {Object} [options.headers] The HTTP headers to mix with the default
445         headers specified by the static `HTTP_HEADERS` property.
446       @param {Number} [options.timeout] The number of milliseconds before the
447         request will timeout and be aborted. This overrides the default provided
448         by the static `HTTP_TIMEOUT` property.
449     @param {Function} [callback] Called when the sync operation finishes.
450       @param {Error|null} callback.err If an error occurred, this parameter will
451         contain the error. If the sync operation succeeded, _err_ will be
452         falsy.
453       @param {Any} [callback.response] The server's response.
454     **/
455     sync: function (action, options, callback) {
456         options || (options = {});
458         var url       = this.getURL(action, options),
459             method    = RESTSync.HTTP_METHODS[action],
460             headers   = Y.merge(RESTSync.HTTP_HEADERS, options.headers),
461             timeout   = options.timeout || RESTSync.HTTP_TIMEOUT,
462             csrfToken = options.csrfToken || RESTSync.CSRF_TOKEN,
463             entity;
465         // Prepare the content if we are sending data to the server.
466         if (method === 'POST' || method === 'PUT') {
467             entity = this.serialize(action);
468         } else {
469             // Remove header, no content is being sent.
470             delete headers['Content-Type'];
471         }
473         // Setup HTTP emulation for older servers if we need it.
474         if (RESTSync.EMULATE_HTTP &&
475                 (method === 'PUT' || method === 'DELETE')) {
477             // Pass along original method type in the headers.
478             headers['X-HTTP-Method-Override'] = method;
480             // Fall-back to using POST method type.
481             method = 'POST';
482         }
484         // Add CSRF token to HTTP request headers if one is specified and the
485         // request will cause side effects on the server.
486         if (csrfToken &&
487                 (method === 'POST' || method === 'PUT' || method === 'DELETE')) {
489             headers['X-CSRF-Token'] = csrfToken;
490         }
492         this._sendSyncIORequest({
493             action  : action,
494             callback: callback,
495             entity  : entity,
496             headers : headers,
497             method  : method,
498             timeout : timeout,
499             url     : url
500         });
501     },
503     // -- Protected Methods ----------------------------------------------------
505     /**
506     Joins the `root` URL to the specified `url`, normalizing leading/trailing
507     "/" characters.
509     @example
510         model.root = '/foo'
511         model._joinURL('bar');  // => '/foo/bar'
512         model._joinURL('/bar'); // => '/foo/bar'
514         model.root = '/foo/'
515         model._joinURL('bar');  // => '/foo/bar/'
516         model._joinURL('/bar'); // => '/foo/bar/'
518     @method _joinURL
519     @param {String} url URL to append to the `root` URL.
520     @return {String} Joined URL.
521     @protected
522     @since 3.6.0
523     **/
524     _joinURL: function (url) {
525         var root = this.root;
527         if (!(root || url)) {
528             return '';
529         }
531         if (url.charAt(0) === '/') {
532             url = url.substring(1);
533         }
535         // Combines the `root` with the `url` and adds a trailing-slash if the
536         // `root` has a trailing-slash.
537         return root && root.charAt(root.length - 1) === '/' ?
538                 root + url + '/' :
539                 root + '/' + url;
540     },
543     /**
544     Calls both public, overrideable methods: `parseIOResponse()`, then `parse()`
545     and returns the result.
547     This will call into `parseIOResponse()`, if it's defined as a method,
548     passing it the full response object from the XHR and using its return value
549     to pass along to the `parse()`. This enables developers to easily parse data
550     out of the response headers which should be used by the `parse()` method.
552     @method _parse
553     @param {Object} response Response object from `Y.io()`.
554     @return {Object|Object[]} Attribute hash or Array of model attribute hashes.
555     @protected
556     @since 3.7.0
557     **/
558     _parse: function (response) {
559         // When `parseIOResponse` is defined as a method, it will be invoked and
560         // the result will become the new response object that the `parse()`
561         // will be invoked with.
562         if (typeof this.parseIOResponse === 'function') {
563             response = this.parseIOResponse(response);
564         }
566         return this.parse(response);
567     },
569     /**
570     Performs the XHR and returns the resulting `Y.io()` request object.
572     This method is called by `sync()`.
574     @method _sendSyncIORequest
575     @param {Object} config An object with the following properties:
576       @param {String} config.action The `sync()` action being performed.
577       @param {Function} [config.callback] Called when the sync operation
578         finishes.
579       @param {String} [config.entity] The HTTP request entity body.
580       @param {Object} config.headers The HTTP request headers.
581       @param {String} config.method The HTTP request method.
582       @param {Number} [config.timeout] Time until the HTTP request is aborted.
583       @param {String} config.url The URL of the HTTP resource.
584     @return {Object} The resulting `Y.io()` request object.
585     @protected
586     @since 3.6.0
587     **/
588     _sendSyncIORequest: function (config) {
589         return Y.io(config.url, {
590             'arguments': {
591                 action  : config.action,
592                 callback: config.callback,
593                 url     : config.url
594             },
596             context: this,
597             data   : config.entity,
598             headers: config.headers,
599             method : config.method,
600             timeout: config.timeout,
602             on: {
603                 start  : this._onSyncIOStart,
604                 failure: this._onSyncIOFailure,
605                 success: this._onSyncIOSuccess,
606                 end    : this._onSyncIOEnd
607             }
608         });
609     },
611     /**
612     Utility which takes a tokenized `url` string and substitutes its
613     placeholders using a specified `data` object.
615     This method will property URL-encode any values before substituting them.
616     Also, only expect it to work with String and Number values.
618     @example
619         var url = this._substituteURL('/users/{name}', {id: 'Eric F'});
620         // => "/users/Eric%20F"
622     @method _substituteURL
623     @param {String} url Tokenized URL string to substitute placeholder values.
624     @param {Object} data Set of data to fill in the `url`'s placeholders.
625     @return {String} Substituted URL.
626     @protected
627     @since 3.6.0
628     **/
629     _substituteURL: function (url, data) {
630         if (!url) {
631             return '';
632         }
634         var values = {};
636         // Creates a hash of the string and number values only to be used to
637         // replace any placeholders in a tokenized `url`.
638         Y.Object.each(data, function (v, k) {
639             if (Lang.isString(v) || Lang.isNumber(v)) {
640                 // URL-encode any string or number values.
641                 values[k] = encodeURIComponent(v);
642             }
643         });
645         return Lang.sub(url, values);
646     },
648     // -- Event Handlers -------------------------------------------------------
650     /**
651     Called when the `Y.io` request has finished, after "success" or "failure"
652     has been determined.
654     This is a no-op by default, but provides a hook for overriding.
656     @method _onSyncIOEnd
657     @param {String} txId The `Y.io` transaction id.
658     @param {Object} details Extra details carried through from `sync()`:
659       @param {String} details.action The sync action performed.
660       @param {Function} [details.callback] The function to call after syncing.
661       @param {String} details.url The URL of the requested resource.
662     @protected
663     @since 3.6.0
664     **/
665     _onSyncIOEnd: function (txId, details) {},
667     /**
668     Called when the `Y.io` request has finished unsuccessfully.
670     By default this calls the `details.callback` function passing it the HTTP
671     status code and message as an error object along with the response body.
673     @method _onSyncIOFailure
674     @param {String} txId The `Y.io` transaction id.
675     @param {Object} res The `Y.io` response object.
676     @param {Object} details Extra details carried through from `sync()`:
677       @param {String} details.action The sync action performed.
678       @param {Function} [details.callback] The function to call after syncing.
679       @param {String} details.url The URL of the requested resource.
680     @protected
681     @since 3.6.0
682     **/
683     _onSyncIOFailure: function (txId, res, details) {
684         var callback = details.callback;
686         if (callback) {
687             callback({
688                 code: res.status,
689                 msg : res.statusText
690             }, res);
691         }
692     },
694     /**
695     Called when the `Y.io` request has finished successfully.
697     By default this calls the `details.callback` function passing it the
698     response body.
700     @method _onSyncIOSuccess
701     @param {String} txId The `Y.io` transaction id.
702     @param {Object} res The `Y.io` response object.
703     @param {Object} details Extra details carried through from `sync()`:
704       @param {String} details.action The sync action performed.
705       @param {Function} [details.callback] The function to call after syncing.
706       @param {String} details.url The URL of the requested resource.
707     @protected
708     @since 3.6.0
709     **/
710     _onSyncIOSuccess: function (txId, res, details) {
711         var callback = details.callback;
713         if (callback) {
714             callback(null, res);
715         }
716     },
718     /**
719     Called when the `Y.io` request is made.
721     This is a no-op by default, but provides a hook for overriding.
723     @method _onSyncIOStart
724     @param {String} txId The `Y.io` transaction id.
725     @param {Object} details Extra details carried through from `sync()`:
726       @param {String} detials.action The sync action performed.
727       @param {Function} [details.callback] The function to call after syncing.
728       @param {String} details.url The URL of the requested resource.
729     @protected
730     @since 3.6.0
731     **/
732     _onSyncIOStart: function (txId, details) {}
735 // -- Namespace ----------------------------------------------------------------
737 Y.namespace('ModelSync').REST = RESTSync;
740 }, '3.13.0', {"requires": ["model", "io-base", "json-stringify"]});