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