Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / Connection.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * The Connection class encapsulates a connection to the page's originating domain, allowing requests to be made either
17  * to a configured URL, or to a URL specified at request time.
18  *
19  * Requests made by this class are asynchronous, and will return immediately. No data from the server will be available
20  * to the statement immediately following the {@link #request} call. To process returned data, use a success callback
21  * in the request options object, or an {@link #requestcomplete event listener}.
22  *
23  * # File Uploads
24  *
25  * File uploads are not performed using normal "Ajax" techniques, that is they are not performed using XMLHttpRequests.
26  * Instead the form is submitted in the standard manner with the DOM <form> element temporarily modified to have its
27  * target set to refer to a dynamically generated, hidden <iframe> which is inserted into the document but removed
28  * after the return data has been gathered.
29  *
30  * The server response is parsed by the browser to create the document for the IFRAME. If the server is using JSON to
31  * send the return object, then the Content-Type header must be set to "text/html" in order to tell the browser to
32  * insert the text unchanged into the document body.
33  *
34  * Characters which are significant to an HTML parser must be sent as HTML entities, so encode `<` as `&lt;`, `&` as
35  * `&amp;` etc.
36  *
37  * The response text is retrieved from the document, and a fake XMLHttpRequest object is created containing a
38  * responseText property in order to conform to the requirements of event handlers and callbacks.
39  *
40  * Be aware that file upload packets are sent with the content type multipart/form and some server technologies
41  * (notably JEE) may require some custom processing in order to retrieve parameter names and parameter values from the
42  * packet content.
43  *
44  * Also note that it's not possible to check the response code of the hidden iframe, so the success handler will ALWAYS fire.
45  */
46 Ext.define('Ext.data.Connection', {
47     mixins: {
48         observable: 'Ext.util.Observable'
49     },
50
51     statics: {
52         requestId: 0
53     },
54
55     url: null,
56     async: true,
57     method: null,
58     username: '',
59     password: '',
60
61     /**
62      * @cfg {Boolean} disableCaching
63      * True to add a unique cache-buster param to GET requests.
64      */
65     disableCaching: true,
66
67     /**
68      * @cfg {Boolean} withCredentials
69      * True to set `withCredentials = true` on the XHR object
70      */
71     withCredentials: false,
72
73     /**
74      * @cfg {Boolean} cors
75      * True to enable CORS support on the XHR object. Currently the only effect of this option
76      * is to use the XDomainRequest object instead of XMLHttpRequest if the browser is IE8 or above.
77      */
78     cors: false,
79
80     /**
81      * @cfg {String} disableCachingParam
82      * Change the parameter which is sent went disabling caching through a cache buster.
83      */
84     disableCachingParam: '_dc',
85
86     /**
87      * @cfg {Number} timeout
88      * The timeout in milliseconds to be used for requests.
89      */
90     timeout : 30000,
91
92     /**
93      * @cfg {Object} extraParams
94      * Any parameters to be appended to the request.
95      */
96
97     useDefaultHeader : true,
98     defaultPostHeader : 'application/x-www-form-urlencoded; charset=UTF-8',
99     useDefaultXhrHeader : true,
100     defaultXhrHeader : 'XMLHttpRequest',
101
102     constructor : function(config) {
103         config = config || {};
104         Ext.apply(this, config);
105
106         this.addEvents(
107             /**
108              * @event beforerequest
109              * Fires before a network request is made to retrieve a data object.
110              * @param {Ext.data.Connection} conn This Connection object.
111              * @param {Object} options The options config object passed to the {@link #request} method.
112              */
113             'beforerequest',
114             /**
115              * @event requestcomplete
116              * Fires if the request was successfully completed.
117              * @param {Ext.data.Connection} conn This Connection object.
118              * @param {Object} response The XHR object containing the response data.
119              * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
120              * @param {Object} options The options config object passed to the {@link #request} method.
121              */
122             'requestcomplete',
123             /**
124              * @event requestexception
125              * Fires if an error HTTP status was returned from the server.
126              * See [HTTP Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
127              * for details of HTTP status codes.
128              * @param {Ext.data.Connection} conn This Connection object.
129              * @param {Object} response The XHR object containing the response data.
130              * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
131              * @param {Object} options The options config object passed to the {@link #request} method.
132              */
133             'requestexception'
134         );
135         this.requests = {};
136         this.mixins.observable.constructor.call(this);
137     },
138
139     /**
140      * Sends an HTTP request to a remote server.
141      *
142      * **Important:** Ajax server requests are asynchronous, and this call will
143      * return before the response has been received. Process any returned data
144      * in a callback function.
145      *
146      *     Ext.Ajax.request({
147      *         url: 'ajax_demo/sample.json',
148      *         success: function(response, opts) {
149      *             var obj = Ext.decode(response.responseText);
150      *             console.dir(obj);
151      *         },
152      *         failure: function(response, opts) {
153      *             console.log('server-side failure with status code ' + response.status);
154      *         }
155      *     });
156      *
157      * To execute a callback function in the correct scope, use the `scope` option.
158      *
159      * @param {Object} options An object which may contain the following properties:
160      *
161      * (The options object may also contain any other property which might be needed to perform
162      * postprocessing in a callback because it is passed to callback functions.)
163      *
164      * @param {String/Function} options.url The URL to which to send the request, or a function
165      * to call which returns a URL string. The scope of the function is specified by the `scope` option.
166      * Defaults to the configured `url`.
167      *
168      * @param {Object/String/Function} options.params An object containing properties which are
169      * used as parameters to the request, a url encoded string or a function to call to get either. The scope
170      * of the function is specified by the `scope` option.
171      *
172      * @param {String} options.method The HTTP method to use
173      * for the request. Defaults to the configured method, or if no method was configured,
174      * "GET" if no parameters are being sent, and "POST" if parameters are being sent.  Note that
175      * the method name is case-sensitive and should be all caps.
176      *
177      * @param {Function} options.callback The function to be called upon receipt of the HTTP response.
178      * The callback is called regardless of success or failure and is passed the following parameters:
179      * @param {Object} options.callback.options The parameter to the request call.
180      * @param {Boolean} options.callback.success True if the request succeeded.
181      * @param {Object} options.callback.response The XMLHttpRequest object containing the response data.
182      * See [www.w3.org/TR/XMLHttpRequest/](http://www.w3.org/TR/XMLHttpRequest/) for details about
183      * accessing elements of the response.
184      *
185      * @param {Function} options.success The function to be called upon success of the request.
186      * The callback is passed the following parameters:
187      * @param {Object} options.success.response The XMLHttpRequest object containing the response data.
188      * @param {Object} options.success.options The parameter to the request call.
189      *
190      * @param {Function} options.failure The function to be called upon success of the request.
191      * The callback is passed the following parameters:
192      * @param {Object} options.failure.response The XMLHttpRequest object containing the response data.
193      * @param {Object} options.failure.options The parameter to the request call.
194      *
195      * @param {Object} options.scope The scope in which to execute the callbacks: The "this" object for
196      * the callback function. If the `url`, or `params` options were specified as functions from which to
197      * draw values, then this also serves as the scope for those function calls. Defaults to the browser
198      * window.
199      *
200      * @param {Number} options.timeout The timeout in milliseconds to be used for this request.
201      * Defaults to 30 seconds.
202      *
203      * @param {Ext.Element/HTMLElement/String} options.form The `<form>` Element or the id of the `<form>`
204      * to pull parameters from.
205      *
206      * @param {Boolean} options.isUpload **Only meaningful when used with the `form` option.**
207      *
208      * True if the form object is a file upload (will be set automatically if the form was configured
209      * with **`enctype`** `"multipart/form-data"`).
210      *
211      * File uploads are not performed using normal "Ajax" techniques, that is they are **not**
212      * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with the
213      * DOM `<form>` element temporarily modified to have its [target][] set to refer to a dynamically
214      * generated, hidden `<iframe>` which is inserted into the document but removed after the return data
215      * has been gathered.
216      *
217      * The server response is parsed by the browser to create the document for the IFRAME. If the
218      * server is using JSON to send the return object, then the [Content-Type][] header must be set to
219      * "text/html" in order to tell the browser to insert the text unchanged into the document body.
220      *
221      * The response text is retrieved from the document, and a fake XMLHttpRequest object is created
222      * containing a `responseText` property in order to conform to the requirements of event handlers
223      * and callbacks.
224      *
225      * Be aware that file upload packets are sent with the content type [multipart/form][] and some server
226      * technologies (notably JEE) may require some custom processing in order to retrieve parameter names
227      * and parameter values from the packet content.
228      *
229      * [target]: http://www.w3.org/TR/REC-html40/present/frames.html#adef-target
230      * [Content-Type]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
231      * [multipart/form]: http://www.faqs.org/rfcs/rfc2388.html
232      *
233      * @param {Object} options.headers Request headers to set for the request.
234      *
235      * @param {Object} options.xmlData XML document to use for the post. Note: This will be used instead
236      * of params for the post data. Any params will be appended to the URL.
237      *
238      * @param {Object/String} options.jsonData JSON data to use as the post. Note: This will be used
239      * instead of params for the post data. Any params will be appended to the URL.
240      *
241      * @param {Boolean} options.disableCaching True to add a unique cache-buster param to GET requests.
242      *
243      * @param {Boolean} options.withCredentials True to add the withCredentials property to the XHR object
244      *
245      * @return {Object} The request object. This may be used to cancel the request.
246      */
247     request : function(options) {
248         options = options || {};
249         var me = this,
250             scope = options.scope || window,
251             username = options.username || me.username,
252             password = options.password || me.password || '',
253             async,
254             requestOptions,
255             request,
256             headers,
257             xhr;
258
259         if (me.fireEvent('beforerequest', me, options) !== false) {
260
261             requestOptions = me.setOptions(options, scope);
262
263             if (this.isFormUpload(options) === true) {
264                 this.upload(options.form, requestOptions.url, requestOptions.data, options);
265                 return null;
266             }
267
268             // if autoabort is set, cancel the current transactions
269             if (options.autoAbort === true || me.autoAbort) {
270                 me.abort();
271             }
272
273             // create a connection object
274
275             if ((options.cors === true || me.cors === true) && Ext.isIe && Ext.ieVersion >= 8) {
276                 xhr = new XDomainRequest();
277             } else {
278                 xhr = this.getXhrInstance();
279             }
280
281             async = options.async !== false ? (options.async || me.async) : false;
282
283             // open the request
284             if (username) {
285                 xhr.open(requestOptions.method, requestOptions.url, async, username, password);
286             } else {
287                 xhr.open(requestOptions.method, requestOptions.url, async);
288             }
289
290             if (options.withCredentials === true || me.withCredentials === true) {
291                 xhr.withCredentials = true;
292             }
293
294             headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
295
296             // create the transaction object
297             request = {
298                 id: ++Ext.data.Connection.requestId,
299                 xhr: xhr,
300                 headers: headers,
301                 options: options,
302                 async: async,
303                 timeout: setTimeout(function() {
304                     request.timedout = true;
305                     me.abort(request);
306                 }, options.timeout || me.timeout)
307             };
308             me.requests[request.id] = request;
309             me.latestId = request.id;
310             // bind our statechange listener
311             if (async) {
312                 xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me, [request]);
313             }
314
315             // start the request!
316             xhr.send(requestOptions.data);
317             if (!async) {
318                 return this.onComplete(request);
319             }
320             return request;
321         } else {
322             Ext.callback(options.callback, options.scope, [options, undefined, undefined]);
323             return null;
324         }
325     },
326
327     /**
328      * Uploads a form using a hidden iframe.
329      * @param {String/HTMLElement/Ext.Element} form The form to upload
330      * @param {String} url The url to post to
331      * @param {String} params Any extra parameters to pass
332      * @param {Object} options The initial options
333      */
334     upload: function(form, url, params, options) {
335         form = Ext.getDom(form);
336         options = options || {};
337
338         var id = Ext.id(),
339                 frame = document.createElement('iframe'),
340                 hiddens = [],
341                 encoding = 'multipart/form-data',
342                 buf = {
343                     target: form.target,
344                     method: form.method,
345                     encoding: form.encoding,
346                     enctype: form.enctype,
347                     action: form.action
348                 }, hiddenItem;
349
350         /*
351          * Originally this behaviour was modified for Opera 10 to apply the secure URL after
352          * the frame had been added to the document. It seems this has since been corrected in
353          * Opera so the behaviour has been reverted, the URL will be set before being added.
354          */
355         Ext.fly(frame).set({
356             id: id,
357             name: id,
358             cls: Ext.baseCSSPrefix + 'hide-display',
359             src: Ext.SSL_SECURE_URL
360         });
361
362         document.body.appendChild(frame);
363
364         // This is required so that IE doesn't pop the response up in a new window.
365         if (document.frames) {
366            document.frames[id].name = id;
367         }
368
369         Ext.fly(form).set({
370             target: id,
371             method: 'POST',
372             enctype: encoding,
373             encoding: encoding,
374             action: url || buf.action
375         });
376
377         // add dynamic params
378         if (params) {
379             Ext.iterate(Ext.Object.fromQueryString(params), function(name, value){
380                 hiddenItem = document.createElement('input');
381                 Ext.fly(hiddenItem).set({
382                     type: 'hidden',
383                     value: value,
384                     name: name
385                 });
386                 form.appendChild(hiddenItem);
387                 hiddens.push(hiddenItem);
388             });
389         }
390
391         Ext.fly(frame).on('load', Ext.Function.bind(this.onUploadComplete, this, [frame, options]), null, {single: true});
392         form.submit();
393
394         Ext.fly(form).set(buf);
395         Ext.each(hiddens, function(h) {
396             Ext.removeNode(h);
397         });
398     },
399
400     /**
401      * @private
402      * Callback handler for the upload function. After we've submitted the form via the iframe this creates a bogus
403      * response object to simulate an XHR and populates its responseText from the now-loaded iframe's document body
404      * (or a textarea inside the body). We then clean up by removing the iframe
405      */
406     onUploadComplete: function(frame, options) {
407         var me = this,
408             // bogus response object
409             response = {
410                 responseText: '',
411                 responseXML: null
412             }, doc, firstChild;
413
414         try {
415             doc = frame.contentWindow.document || frame.contentDocument || window.frames[frame.id].document;
416             if (doc) {
417                 if (doc.body) {
418                     if (/textarea/i.test((firstChild = doc.body.firstChild || {}).tagName)) { // json response wrapped in textarea
419                         response.responseText = firstChild.value;
420                     } else {
421                         response.responseText = doc.body.innerHTML;
422                     }
423                 }
424                 //in IE the document may still have a body even if returns XML.
425                 response.responseXML = doc.XMLDocument || doc;
426             }
427         } catch (e) {
428         }
429
430         me.fireEvent('requestcomplete', me, response, options);
431
432         Ext.callback(options.success, options.scope, [response, options]);
433         Ext.callback(options.callback, options.scope, [options, true, response]);
434
435         setTimeout(function(){
436             Ext.removeNode(frame);
437         }, 100);
438     },
439
440     /**
441      * Detects whether the form is intended to be used for an upload.
442      * @private
443      */
444     isFormUpload: function(options){
445         var form = this.getForm(options);
446         if (form) {
447             return (options.isUpload || (/multipart\/form-data/i).test(form.getAttribute('enctype')));
448         }
449         return false;
450     },
451
452     /**
453      * Gets the form object from options.
454      * @private
455      * @param {Object} options The request options
456      * @return {HTMLElement} The form, null if not passed
457      */
458     getForm: function(options){
459         return Ext.getDom(options.form) || null;
460     },
461
462     /**
463      * Sets various options such as the url, params for the request
464      * @param {Object} options The initial options
465      * @param {Object} scope The scope to execute in
466      * @return {Object} The params for the request
467      */
468     setOptions: function(options, scope){
469         var me =  this,
470             params = options.params || {},
471             extraParams = me.extraParams,
472             urlParams = options.urlParams,
473             url = options.url || me.url,
474             jsonData = options.jsonData,
475             method,
476             disableCache,
477             data;
478
479
480         // allow params to be a method that returns the params object
481         if (Ext.isFunction(params)) {
482             params = params.call(scope, options);
483         }
484
485         // allow url to be a method that returns the actual url
486         if (Ext.isFunction(url)) {
487             url = url.call(scope, options);
488         }
489
490         url = this.setupUrl(options, url);
491
492         //<debug>
493         if (!url) {
494             Ext.Error.raise({
495                 options: options,
496                 msg: 'No URL specified'
497             });
498         }
499         //</debug>
500
501         // check for xml or json data, and make sure json data is encoded
502         data = options.rawData || options.xmlData || jsonData || null;
503         if (jsonData && !Ext.isPrimitive(jsonData)) {
504             data = Ext.encode(data);
505         }
506
507         // make sure params are a url encoded string and include any extraParams if specified
508         if (Ext.isObject(params)) {
509             params = Ext.Object.toQueryString(params);
510         }
511
512         if (Ext.isObject(extraParams)) {
513             extraParams = Ext.Object.toQueryString(extraParams);
514         }
515
516         params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : '');
517
518         urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams;
519
520         params = this.setupParams(options, params);
521
522         // decide the proper method for this request
523         method = (options.method || me.method || ((params || data) ? 'POST' : 'GET')).toUpperCase();
524         this.setupMethod(options, method);
525
526
527         disableCache = options.disableCaching !== false ? (options.disableCaching || me.disableCaching) : false;
528         // if the method is get append date to prevent caching
529         if (method === 'GET' && disableCache) {
530             url = Ext.urlAppend(url, (options.disableCachingParam || me.disableCachingParam) + '=' + (new Date().getTime()));
531         }
532
533         // if the method is get or there is json/xml data append the params to the url
534         if ((method == 'GET' || data) && params) {
535             url = Ext.urlAppend(url, params);
536             params = null;
537         }
538
539         // allow params to be forced into the url
540         if (urlParams) {
541             url = Ext.urlAppend(url, urlParams);
542         }
543
544         return {
545             url: url,
546             method: method,
547             data: data || params || null
548         };
549     },
550
551     /**
552      * Template method for overriding url
553      * @template
554      * @private
555      * @param {Object} options
556      * @param {String} url
557      * @return {String} The modified url
558      */
559     setupUrl: function(options, url){
560         var form = this.getForm(options);
561         if (form) {
562             url = url || form.action;
563         }
564         return url;
565     },
566
567
568     /**
569      * Template method for overriding params
570      * @template
571      * @private
572      * @param {Object} options
573      * @param {String} params
574      * @return {String} The modified params
575      */
576     setupParams: function(options, params) {
577         var form = this.getForm(options),
578             serializedForm;
579         if (form && !this.isFormUpload(options)) {
580             serializedForm = Ext.Element.serializeForm(form);
581             params = params ? (params + '&' + serializedForm) : serializedForm;
582         }
583         return params;
584     },
585
586     /**
587      * Template method for overriding method
588      * @template
589      * @private
590      * @param {Object} options
591      * @param {String} method
592      * @return {String} The modified method
593      */
594     setupMethod: function(options, method){
595         if (this.isFormUpload(options)) {
596             return 'POST';
597         }
598         return method;
599     },
600
601     /**
602      * Setup all the headers for the request
603      * @private
604      * @param {Object} xhr The xhr object
605      * @param {Object} options The options for the request
606      * @param {Object} data The data for the request
607      * @param {Object} params The params for the request
608      */
609     setupHeaders: function(xhr, options, data, params){
610         var me = this,
611             headers = Ext.apply({}, options.headers || {}, me.defaultHeaders || {}),
612             contentType = me.defaultPostHeader,
613             jsonData = options.jsonData,
614             xmlData = options.xmlData,
615             key,
616             header;
617
618         if (!headers['Content-Type'] && (data || params)) {
619             if (data) {
620                 if (options.rawData) {
621                     contentType = 'text/plain';
622                 } else {
623                     if (xmlData && Ext.isDefined(xmlData)) {
624                         contentType = 'text/xml';
625                     } else if (jsonData && Ext.isDefined(jsonData)) {
626                         contentType = 'application/json';
627                     }
628                 }
629             }
630             headers['Content-Type'] = contentType;
631         }
632
633         if (me.useDefaultXhrHeader && !headers['X-Requested-With']) {
634             headers['X-Requested-With'] = me.defaultXhrHeader;
635         }
636         // set up all the request headers on the xhr object
637         try{
638             for (key in headers) {
639                 if (headers.hasOwnProperty(key)) {
640                     header = headers[key];
641                     xhr.setRequestHeader(key, header);
642                 }
643
644             }
645         } catch(e) {
646             me.fireEvent('exception', key, header);
647         }
648         return headers;
649     },
650
651     /**
652      * Creates the appropriate XHR transport for the browser.
653      * @private
654      */
655     getXhrInstance: (function(){
656         var options = [function(){
657             return new XMLHttpRequest();
658         }, function(){
659             return new ActiveXObject('MSXML2.XMLHTTP.3.0');
660         }, function(){
661             return new ActiveXObject('MSXML2.XMLHTTP');
662         }, function(){
663             return new ActiveXObject('Microsoft.XMLHTTP');
664         }], i = 0,
665             len = options.length,
666             xhr;
667
668         for(; i < len; ++i) {
669             try{
670                 xhr = options[i];
671                 xhr();
672                 break;
673             }catch(e){}
674         }
675         return xhr;
676     })(),
677
678     /**
679      * Determines whether this object has a request outstanding.
680      * @param {Object} [request] Defaults to the last transaction
681      * @return {Boolean} True if there is an outstanding request.
682      */
683     isLoading : function(request) {
684         if (!request) {
685             request = this.getLatest();
686         }
687         if (!(request && request.xhr)) {
688             return false;
689         }
690         // if there is a connection and readyState is not 0 or 4
691         var state = request.xhr.readyState;
692         return !(state === 0 || state == 4);
693     },
694
695     /**
696      * Aborts an active request.
697      * @param {Object} [request] Defaults to the last request
698      */
699     abort : function(request) {
700         var me = this;
701         
702         if (!request) {
703             request = me.getLatest();
704         }
705
706         if (request && me.isLoading(request)) {
707             /*
708              * Clear out the onreadystatechange here, this allows us
709              * greater control, the browser may/may not fire the function
710              * depending on a series of conditions.
711              */
712             request.xhr.onreadystatechange = null;
713             request.xhr.abort();
714             me.clearTimeout(request);
715             if (!request.timedout) {
716                 request.aborted = true;
717             }
718             me.onComplete(request);
719             me.cleanup(request);
720         }
721     },
722     
723     /**
724      * Aborts all active requests
725      */
726     abortAll: function(){
727         var requests = this.requests,
728             id;
729         
730         for (id in requests) {
731             if (requests.hasOwnProperty(id)) {
732                 this.abort(requests[id]);
733             }
734         }
735     },
736     
737     /**
738      * Gets the most recent request
739      * @private
740      * @return {Object} The request. Null if there is no recent request
741      */
742     getLatest: function(){
743         var id = this.latestId,
744             request;
745             
746         if (id) {
747             request = this.requests[id];
748         }
749         return request || null;
750     },
751
752     /**
753      * Fires when the state of the xhr changes
754      * @private
755      * @param {Object} request The request
756      */
757     onStateChange : function(request) {
758         if (request.xhr.readyState == 4) {
759             this.clearTimeout(request);
760             this.onComplete(request);
761             this.cleanup(request);
762         }
763     },
764
765     /**
766      * Clears the timeout on the request
767      * @private
768      * @param {Object} The request
769      */
770     clearTimeout: function(request){
771         clearTimeout(request.timeout);
772         delete request.timeout;
773     },
774
775     /**
776      * Cleans up any left over information from the request
777      * @private
778      * @param {Object} The request
779      */
780     cleanup: function(request){
781         request.xhr = null;
782         delete request.xhr;
783     },
784
785     /**
786      * To be called when the request has come back from the server
787      * @private
788      * @param {Object} request
789      * @return {Object} The response
790      */
791     onComplete : function(request) {
792         var me = this,
793             options = request.options,
794             result,
795             success,
796             response;
797
798         try {
799             result = me.parseStatus(request.xhr.status);
800         } catch (e) {
801             // in some browsers we can't access the status if the readyState is not 4, so the request has failed
802             result = {
803                 success : false,
804                 isException : false
805             };
806         }
807         success = result.success;
808
809         if (success) {
810             response = me.createResponse(request);
811             me.fireEvent('requestcomplete', me, response, options);
812             Ext.callback(options.success, options.scope, [response, options]);
813         } else {
814             if (result.isException || request.aborted || request.timedout) {
815                 response = me.createException(request);
816             } else {
817                 response = me.createResponse(request);
818             }
819             me.fireEvent('requestexception', me, response, options);
820             Ext.callback(options.failure, options.scope, [response, options]);
821         }
822         Ext.callback(options.callback, options.scope, [options, success, response]);
823         delete me.requests[request.id];
824         return response;
825     },
826
827     /**
828      * Checks if the response status was successful
829      * @param {Number} status The status code
830      * @return {Object} An object containing success/status state
831      */
832     parseStatus: function(status) {
833         // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223
834         status = status == 1223 ? 204 : status;
835
836         var success = (status >= 200 && status < 300) || status == 304,
837             isException = false;
838
839         if (!success) {
840             switch (status) {
841                 case 12002:
842                 case 12029:
843                 case 12030:
844                 case 12031:
845                 case 12152:
846                 case 13030:
847                     isException = true;
848                     break;
849             }
850         }
851         return {
852             success: success,
853             isException: isException
854         };
855     },
856
857     /**
858      * Creates the response object
859      * @private
860      * @param {Object} request
861      */
862     createResponse : function(request) {
863         var xhr = request.xhr,
864             headers = {},
865             lines = xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n'),
866             count = lines.length,
867             line, index, key, value, response;
868
869         while (count--) {
870             line = lines[count];
871             index = line.indexOf(':');
872             if(index >= 0) {
873                 key = line.substr(0, index).toLowerCase();
874                 if (line.charAt(index + 1) == ' ') {
875                     ++index;
876                 }
877                 headers[key] = line.substr(index + 1);
878             }
879         }
880
881         request.xhr = null;
882         delete request.xhr;
883
884         response = {
885             request: request,
886             requestId : request.id,
887             status : xhr.status,
888             statusText : xhr.statusText,
889             getResponseHeader : function(header){ return headers[header.toLowerCase()]; },
890             getAllResponseHeaders : function(){ return headers; },
891             responseText : xhr.responseText,
892             responseXML : xhr.responseXML
893         };
894
895         // If we don't explicitly tear down the xhr reference, IE6/IE7 will hold this in the closure of the
896         // functions created with getResponseHeader/getAllResponseHeaders
897         xhr = null;
898         return response;
899     },
900
901     /**
902      * Creates the exception object
903      * @private
904      * @param {Object} request
905      */
906     createException : function(request) {
907         return {
908             request : request,
909             requestId : request.id,
910             status : request.aborted ? -1 : 0,
911             statusText : request.aborted ? 'transaction aborted' : 'communication failure',
912             aborted: request.aborted,
913             timedout: request.timedout
914         };
915     }
916 });
917