1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 /*global ActiveXObject */
  8 
  9 /**
 10   @class
 11 
 12   A response represents a single response from a server request and handles the communication to
 13   the server.  An instance of this class is returned whenever you call `SC.Request.send()`.
 14 
 15   SproutCore only defines one concrete subclass of `SC.Response`,`SC.XHRResponse`. In order to
 16   use `SC.Request` with a non-XHR request type you should create a custom class that extends
 17   `SC.Response` and set your custom class as the value of `responseClass` on all requests.
 18 
 19   For example,
 20 
 21       var request = SC.Request.getUrl(resourceAddress)
 22                               .set('responseClass', MyApp.CustomProtocolResponse)
 23                               .send();
 24 
 25   To extend `SC.Response`, please look at the property and methods listed below. For more examples,
 26   please look at the code in `SC.XHRResponse`.
 27 
 28   @extend SC.Object
 29   @since SproutCore 1.0
 30 */
 31 SC.Response = SC.Object.extend(
 32 /** @scope SC.Response.prototype */ {
 33 
 34   /**
 35     Walk like a duck
 36 
 37     @type Boolean
 38   */
 39   isResponse: YES,
 40 
 41   /**
 42     Becomes true if there was a failure.  Makes this into an error object.
 43 
 44     @type Boolean
 45     @default NO
 46   */
 47   isError: NO,
 48 
 49   /**
 50     Always the current response
 51 
 52     @field
 53     @type SC.Response
 54     @default `this`
 55   */
 56   errorValue: function() {
 57     return this;
 58   }.property().cacheable(),
 59 
 60   /**
 61     The error object generated when this becomes an error
 62 
 63     @type SC.Error
 64     @default null
 65   */
 66   errorObject: null,
 67 
 68   /**
 69     Request used to generate this response.  This is a copy of the original
 70     request object as you may have modified the original request object since
 71     then.
 72 
 73     To retrieve the original request object use originalRequest.
 74 
 75     @type SC.Request
 76     @default null
 77   */
 78   request: null,
 79 
 80   /**
 81     The request object that originated this request series.  Mostly this is
 82     useful if you are looking for a reference to the original request.  To
 83     inspect actual properties you should use request instead.
 84 
 85     @field
 86     @type SC.Request
 87     @observes request
 88   */
 89   originalRequest: function() {
 90     var ret = this.get('request');
 91     while (ret.get('source')) { ret = ret.get('source'); }
 92     return ret;
 93   }.property('request').cacheable(),
 94 
 95   /**
 96     Type of request. Must be an HTTP method. Based on the request.
 97 
 98     @field
 99     @type String
100     @observes request
101   */
102   type: function() {
103     return this.getPath('request.type');
104   }.property('request').cacheable(),
105 
106   /**
107     URL of request.
108 
109     @field
110     @type String
111     @observes request
112   */
113   address: function() {
114     return this.getPath('request.address');
115   }.property('request').cacheable(),
116 
117   /**
118     If set then will attempt to automatically parse response as JSON
119     regardless of headers.
120 
121     @field
122     @type Boolean
123     @default NO
124     @observes request
125   */
126   isJSON: function() {
127     return this.getPath('request.isJSON') || NO;
128   }.property('request').cacheable(),
129 
130   /**
131     If set, then will attempt to automatically parse response as XML
132     regardless of headers.
133 
134     @field
135     @type Boolean
136     @default NO
137     @observes request
138   */
139   isXML: function() {
140     return this.getPath('request.isXML') || NO;
141   }.property('request').cacheable(),
142 
143   /**
144     Returns the hash of listeners set on the request.
145 
146     @field
147     @type Hash
148     @observes request
149   */
150   listeners: function() {
151     return this.getPath('request.listeners');
152   }.property('request').cacheable(),
153 
154   /**
155     The response status code.
156 
157     @type Number
158     @default -100
159   */
160   status: -100, // READY
161 
162   /**
163     Headers from the response. Computed on-demand
164 
165     @type Hash
166     @default null
167   */
168   headers: null,
169 
170   /**
171     The response body or the parsed JSON. Returns a SC.Error instance
172     if there is a JSON parsing error. If isJSON was set, will be parsed
173     automatically.
174 
175     @field
176     @type {Hash|String|SC.Error}
177   */
178   body: function() {
179     // TODO: support XML
180     // TODO: why not use the content-type header?
181     var ret = this.get('encodedBody');
182     if (ret && this.get('isJSON')) {
183       try {
184         ret = SC.json.decode(ret);
185       } catch(e) {
186         return SC.Error.create({
187           message: e.name + ': ' + e.message,
188           label: 'Response',
189           errorValue: this });
190       }
191     }
192     return ret;
193   }.property('encodedBody').cacheable(),
194 
195   /**
196     @private
197     @deprecated Use body instead.
198 
199     Alias for body.
200 
201     @type Hash|String
202     @see #body
203   */
204   response: function() {
205     return this.get('body');
206   }.property('body').cacheable(),
207 
208   /**
209     Set to YES if response is cancelled
210 
211     @type Boolean
212     @default NO
213   */
214   isCancelled: NO,
215 
216   /**
217     Set to YES if the request timed out. Set to NO if the request has
218     completed before the timeout value. Set to null if the timeout timer is
219     still ticking.
220 
221     @type Boolean
222     @default null
223   */
224   timedOut: null,
225 
226   /**
227     The timer tracking the timeout
228 
229     @type Number
230     @default null
231   */
232   timeoutTimer: null,
233 
234 
235   // ..........................................................
236   // METHODS
237   //
238 
239   /**
240     Called by the request manager when its time to actually run. This will
241     invoke any callbacks on the source request then invoke transport() to
242     begin the actual request.
243   */
244   fire: function() {
245     var req = this.get('request'),
246         source = req ? req.get('source') : null;
247 
248     // first give the source a chance to fixup the request and response
249     // then freeze req so no more changes can happen.
250     if (source && source.willSend) { source.willSend(req, this); }
251     req.freeze();
252 
253     // if the source did not cancel the request, then invoke the transport
254     // to actually trigger the request.  This might receive a response
255     // immediately if it is synchronous.
256     if (!this.get('isCancelled')) { this.invokeTransport(); }
257 
258     // If the request specified a timeout value, then set a timer for it now.
259     var timeout = req.get('timeout');
260     if (timeout) {
261       var timer = SC.Timer.schedule({
262         target: this,
263         action: 'timeoutReached',
264         interval: timeout,
265         repeats: NO
266       });
267       this.set('timeoutTimer', timer);
268     }
269 
270     // if the transport did not cancel the request for some reason, let the
271     // source know that the request was sent
272     if (!this.get('isCancelled') && source && source.didSend) {
273       source.didSend(req, this);
274     }
275   },
276 
277   /**
278     Called by `SC.Response#fire()`. Starts the transport by invoking the
279     `SC.Response#receive()` function.
280   */
281   invokeTransport: function() {
282     this.receive(function(proceed) { this.set('status', 200); }, this);
283   },
284 
285   /**
286     Invoked by the transport when it receives a response. The passed-in
287     callback will be invoked to actually process the response. If cancelled
288     we will pass NO. You should clean up instead.
289 
290     Invokes callbacks on the source request also.
291 
292     @param {Function} callback the function to receive
293     @param {Object} context context to execute the callback in
294     @returns {SC.Response} receiver
295   */
296   receive: function(callback, context) {
297     if (!this.get('timedOut')) {
298       // If we had a timeout timer scheduled, invalidate it now.
299       var timer = this.get('timeoutTimer');
300       if (timer) { timer.invalidate(); }
301       this.set('timedOut', NO);
302     }
303 
304     var req = this.get('request');
305     var source = req ? req.get('source') : null;
306 
307     SC.run(function() {
308       // invoke the source, giving a chance to fixup the response or (more
309       // likely) cancel the request.
310       if (source && source.willReceive) { source.willReceive(req, this); }
311 
312       // invoke the callback.  note if the response was cancelled or not
313       callback.call(context, !this.get('isCancelled'));
314 
315       // if we weren't cancelled, then give the source first crack at handling
316       // the response.  if the source doesn't want listeners to be notified,
317       // it will cancel the response.
318       if (!this.get('isCancelled') && source && source.didReceive) {
319         source.didReceive(req, this);
320       }
321 
322       // notify listeners if we weren't cancelled.
323       if (!this.get('isCancelled')) { this.notify(); }
324     }, this);
325 
326     // no matter what, remove from inflight queue
327     SC.Request.manager.transportDidClose(this);
328     return this;
329   },
330 
331   /**
332     Default method just closes the connection. It will also mark the request
333     as cancelled, which will not call any listeners.
334   */
335   cancel: function() {
336     if (!this.get('isCancelled')) {
337       this.set('isCancelled', YES);
338       this.cancelTransport();
339       SC.Request.manager.transportDidClose(this);
340     }
341   },
342 
343   /**
344     Default method just closes the connection.
345 
346     @returns {Boolean} YES if this response has not timed out yet, NO otherwise
347   */
348   timeoutReached: function() {
349     // If we already received a response yet the timer still fired for some
350     // reason, do nothing.
351     if (this.get('timedOut') === null) {
352       this.set('timedOut', YES);
353       this.cancelTransport();
354 
355       // Invokes any relevant callbacks and notifies registered listeners, if
356       // any. In the event of a timeout, we set the status to 0 since we
357       // didn't actually get a response from the server.
358       this.receive(function(proceed) {
359         if (!proceed) { return; }
360 
361         // Set our value to an error.
362         var error = SC.$error("HTTP Request timed out", "Request", 0);
363         error.set("errorValue", this);
364         this.set('isError', YES);
365         this.set('errorObject', error);
366         this.set('status', 0);
367       }, this);
368 
369       return YES;
370     }
371 
372     return NO;
373   },
374 
375   /**
376     Override with concrete implementation to actually cancel the transport.
377   */
378   cancelTransport: function() {},
379 
380   /**
381     @private
382 
383     Will notify each listener. Returns true if any of the listeners handle.
384   */
385   _notifyListeners: function(listeners, status) {
386     var notifiers = listeners[status], args, target, action;
387     if (!notifiers) { return NO; }
388 
389     var handled = NO;
390     var len = notifiers.length;
391 
392     for (var i = 0; i < len; i++) {
393       var notifier = notifiers[i];
394       args = (notifier.args || []).copy();
395       args.unshift(this);
396 
397       target = notifier.target;
398       action = notifier.action;
399       if (SC.typeOf(action) === SC.T_STRING) { action = target[action]; }
400 
401       handled = action.apply(target, args);
402     }
403 
404     return handled;
405   },
406 
407   /**
408     Notifies any saved target/action. Call whenever you cancel, or end.
409 
410     @returns {SC.Response} receiver
411   */
412   notify: function() {
413     var listeners = this.get('listeners'),
414         status = this.get('status'),
415         baseStat = Math.floor(status / 100) * 100,
416         handled = NO;
417 
418     if (!listeners) { return this; }
419 
420     handled = this._notifyListeners(listeners, status);
421     if (!handled && baseStat !== status) { handled = this._notifyListeners(listeners, baseStat); }
422     if (!handled && status !== 0) { handled = this._notifyListeners(listeners, 0); }
423 
424     return this;
425   },
426 
427   /**
428     String representation of the response object
429 
430     @returns {String}
431   */
432   toString: function() {
433     var ret = sc_super();
434     return "%@<%@ %@, status=%@".fmt(ret, this.get('type'), this.get('address'), this.get('status'));
435   }
436 
437 });
438 
439 /**
440   @class
441 
442   Concrete implementation of `SC.Response` that implements support for using XHR requests. This is
443   the default response class that `SC.Request` uses and it is able to create cross-browser
444   compatible XHR requests to the address defined on a request and to notify according to the status
445   code fallbacks registered on the request.
446 
447   You will not typically deal with this class other than to receive an instance of it when handling
448   `SC.Request` responses. For more information on how to create a request and handle an XHR response,
449   please @see SC.Request.
450 
451   @extends SC.Response
452   @since SproutCore 1.0
453 */
454 SC.XHRResponse = SC.Response.extend(
455 /** @scope SC.XHRResponse.prototype */{
456 
457   /**
458     Implement transport-specific support for fetching all headers
459   */
460   headers: function() {
461     var xhr = this.get('rawRequest'),
462         str = xhr ? xhr.getAllResponseHeaders() : null,
463         ret = {};
464 
465     if (!str) { return ret; }
466 
467     str.split("\n").forEach(function(header) {
468       var idx = header.indexOf(':'),
469           key, value;
470 
471       if (idx >= 0) {
472         key = header.slice(0,idx);
473         value = header.slice(idx + 1).trim();
474         ret[key] = value;
475       }
476     }, this);
477 
478     return ret;
479   }.property('status').cacheable(),
480 
481   /**
482     Returns a header value if found.
483 
484     @param {String} key The header key
485     @returns {String}
486   */
487   header: function(key) {
488     var xhr = this.get('rawRequest');
489     return xhr ? xhr.getResponseHeader(key) : null;
490   },
491 
492   /**
493     Implement transport-specific support for fetching tasks
494 
495     @field
496     @type String
497     @default #rawRequest
498   */
499   encodedBody: function() {
500     var xhr = this.get('rawRequest');
501 
502     if (!xhr) { return null; }
503     if (this.get('isXML')) { return xhr.responseXML; }
504 
505     return xhr.responseText;
506   }.property('status').cacheable(),
507 
508   /**
509     Cancels the request.
510   */
511   cancelTransport: function() {
512     var rawRequest = this.get('rawRequest');
513     if (rawRequest) { rawRequest.abort(); }
514     this.set('rawRequest', null);
515   },
516 
517 
518   /**
519     Starts the transport of the request
520 
521     @returns {XMLHttpRequest|ActiveXObject}
522   */
523   invokeTransport: function() {
524     var listener, listeners, listenersForKey,
525       rawRequest,
526       request = this.get('request'),
527       transport, handleReadyStateChange, async, headers;
528 
529     rawRequest = this.createRequest();
530     this.set('rawRequest', rawRequest);
531 
532     // configure async callback - differs per browser...
533     async = !!request.get('isAsynchronous');
534 
535     if (async) {
536       if (SC.platform.get('supportsXHR2ProgressEvent')) {
537         // XMLHttpRequest Level 2
538 
539         // Add progress event listeners that were specified on the request.
540         listeners = request.get("listeners");
541         if (listeners) {
542           for (var key in listeners) {
543 
544             // Make sure the key is not an HTTP numeric status code.
545             if (isNaN(parseInt(key, 10))) {
546               // We still allow multiple notifiers on progress events, but we
547               // don't try to optimize this by using a single listener, because
548               // it is highly unlikely that the developer will add duplicate
549               // progress event notifiers and if they did, it is also unlikely
550               // that they would expect them to cascade in the way that the
551               // status code notifiers do.
552               listenersForKey = listeners[key];
553               for (var i = 0, len = listenersForKey.length; i < len; i++) {
554                 listener = listenersForKey[i];
555 
556                 var keyTarget = key.split('.');
557                 if (SC.none(keyTarget[1])) {
558                   SC.Event.add(rawRequest, keyTarget[0], listener.target, listener.action, listener.args);
559                 } else {
560                   SC.Event.add(rawRequest[keyTarget[0]], keyTarget[1], listener.target, listener.action, listener.args);
561                 }
562               }
563             }
564           }
565         }
566 
567         if (SC.platform.get('supportsXHR2LoadEndEvent')) {
568           SC.Event.add(rawRequest, 'loadend', this, this.finishRequest);
569         } else {
570           SC.Event.add(rawRequest, 'load', this, this.finishRequest);
571           SC.Event.add(rawRequest, 'error', this, this.finishRequest);
572           SC.Event.add(rawRequest, 'abort', this, this.finishRequest);
573         }
574       } else if (window.XMLHttpRequest && rawRequest.addEventListener) {
575         // XMLHttpRequest Level 1 + support for addEventListener (IE prior to version 9.0 lacks support for addEventListener)
576         SC.Event.add(rawRequest, 'readystatechange', this, this.finishRequest);
577       } else {
578         transport = this;
579         handleReadyStateChange = function() {
580           if (!transport) { return null; }
581           var ret = transport.finishRequest();
582           if (ret) { transport = null; }
583           return ret;
584         };
585         rawRequest.onreadystatechange = handleReadyStateChange;
586       }
587     }
588 
589     // initiate request.
590     rawRequest.open(this.get('type'), this.get('address'), async);
591 
592     // headers need to be set *after* the open call.
593     headers = this.getPath('request.headers');
594     for (var headerKey in headers) {
595       rawRequest.setRequestHeader(headerKey, headers[headerKey]);
596     }
597 
598     // Do we need to allow Cookies for x-domain requests?
599     if (!this.getPath('request.isSameDomain') && this.getPath('request.allowCredentials')) {
600       rawRequest.withCredentials = true;
601     }
602 
603     // now send the actual request body - for sync requests browser will
604     // block here
605     rawRequest.send(this.getPath('request.encodedBody'));
606     if (!async) { this.finishRequest(); }
607 
608     return rawRequest;
609   },
610 
611   /**
612     Creates the correct XMLHttpRequest object for this browser.
613 
614     You can override this if you need to, for example, create an XHR on a
615     different domain name from an iframe.
616 
617     @returns {XMLHttpRequest|ActiveXObject}
618   */
619   createRequest: function() {
620     var rawRequest;
621 
622     // check native support first
623     if (window.XMLHttpRequest) {
624       rawRequest = new XMLHttpRequest();
625     } else {
626       // There are two relevant Microsoft MSXML object types.
627       // See here for more information:
628       // http://www.snook.ca/archives/javascript/xmlhttprequest_activex_ie/
629       // http://blogs.msdn.com/b/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx
630       // http://msdn.microsoft.com/en-us/library/windows/desktop/ms763742(v=vs.85).aspx
631       try { rawRequest = new ActiveXObject("MSXML2.XMLHTTP.6.0");  } catch(e) {}
632       try { if (!rawRequest) rawRequest = new ActiveXObject("MSXML2.XMLHTTP");  } catch(e) {}
633     }
634 
635     return rawRequest;
636   },
637 
638   /**
639     @private
640 
641     Called by the XHR when it responds with some final results.
642 
643     @param {XMLHttpRequest} rawRequest the actual request
644     @returns {Boolean} YES if completed, NO otherwise
645   */
646   finishRequest: function(evt) {
647     var listener, listeners, listenersForKey,
648       rawRequest = this.get('rawRequest'),
649       readyState = rawRequest.readyState,
650       request,
651       error, status, msg;
652 
653     if (readyState === 4 && !this.get('timedOut')) {
654       this.receive(function(proceed) {
655         if (!proceed) { return; }
656 
657         // collect the status and decide if we're in an error state or not
658         status = -1;
659         try {
660           status = rawRequest.status || 0;
661           // IE mangles 204 to 1223. See http://bugs.jquery.com/ticket/1450 and many others
662           status = status === 1223 ? 204 : status;
663         } catch (e) {}
664 
665         // if there was an error - setup error and save it
666         if ((status < 200) || (status >= 300)) {
667 
668           try {
669             msg = rawRequest.statusText || '';
670           } catch(e2) {
671             msg = '';
672           }
673 
674           error = SC.$error(msg || "HTTP Request failed", "Request", status);
675           error.set("errorValue", this);
676           this.set('isError', YES);
677           this.set('errorObject', error);
678         }
679 
680         // set the status - this will trigger changes on related properties
681         this.set('status', status);
682       }, this);
683 
684       // Avoid memory leaks
685       if (SC.platform.get('supportsXHR2ProgressEvent')) {
686         // XMLHttpRequest Level 2
687 
688         if (SC.platform.get('supportsXHR2LoadEndEvent')) {
689           SC.Event.remove(rawRequest, 'loadend', this, this.finishRequest);
690         } else {
691           SC.Event.remove(rawRequest, 'load', this, this.finishRequest);
692           SC.Event.remove(rawRequest, 'error', this, this.finishRequest);
693           SC.Event.remove(rawRequest, 'abort', this, this.finishRequest);
694         }
695 
696         request = this.get('request');
697         listeners = request.get("listeners");
698         if (listeners) {
699           for (var key in listeners) {
700 
701             // Make sure the key is not an HTTP numeric status code.
702             if (isNaN(parseInt(key, 10))) {
703               listenersForKey = listeners[key];
704               for (var i = 0, len = listenersForKey.length; i < len; i++) {
705                 listener = listenersForKey[i];
706 
707                 var keyTarget = key.split('.');
708                 if (SC.none(keyTarget[1])) {
709                   SC.Event.remove(rawRequest, keyTarget[0], listener.target, listener.action, listener.args);
710                 } else {
711                   SC.Event.remove(rawRequest[keyTarget[0]], keyTarget[1], listener.target, listener.action, listener.args);
712                 }
713               }
714             }
715           }
716         }
717       } else if (window.XMLHttpRequest && rawRequest.addEventListener) {
718         // XMLHttpRequest Level 1 + support for addEventListener (IE prior to version 9.0 lacks support for addEventListener)
719         SC.Event.remove(rawRequest, 'readystatechange', this, this.finishRequest);
720       } else {
721         rawRequest.onreadystatechange = null;
722       }
723 
724       return YES;
725     }
726     return NO;
727   }
728 
729 });
730