94c6fe165383a9250dd5430347286bf279e11134
[jquery.git] / src / xhr.js
1 (function( jQuery ) {
2
3 var rquery_xhr = /\?/,
4         rhash = /#.*$/,
5         rheaders = /^(.*?):\s*(.*?)\r?$/mg, // IE leaves an \r character at EOL
6         rnoContent = /^(?:GET|HEAD)$/,
7         rts = /([?&])_=[^&]*/,
8         rurl = /^(\w+:)?\/\/([^\/?#]+)/,
9         
10         sliceFunc = Array.prototype.slice;
11         
12 // Creates a jQuery xhr object
13 jQuery.xhr = function( _native ) {
14
15         if ( _native ) {
16                 return jQuery.ajaxSettings.xhr();
17         }
18         
19         function reset( force ) {
20                 
21                 // We only need to reset if we went through the init phase
22                 // (with the exception of object creation)
23                 if ( force || internal ) {
24                         
25                         // Reset callbacks lists
26                         deferred = jQuery.deferred();
27                         completeDeferred = jQuery._deferred();
28                         
29                         xhr.success = xhr.then = deferred.then;
30                         xhr.error = xhr.fail = deferred.fail;
31                         xhr.complete = completeDeferred.then;
32                         
33                         // Reset private variables
34                         requestHeaders = {};
35                         responseHeadersString = responseHeaders = internal = done = timeoutTimer = s = undefined;
36
37                         // Reset state
38                         xhr.readyState = 0;
39                         sendFlag = 0;
40
41                         // Remove responseX fields
42                         for ( var name in xhr ) {
43                                 if ( /^response/.test(name) ) {
44                                         delete xhr[name];
45                                 }
46                         }
47                 }
48         }
49
50         function init() {
51
52                 var // Options extraction
53
54                         // Remove hash character (#7531: first for string promotion)
55                         url = s.url = ( "" + s.url ).replace( rhash , "" ),
56
57                         // Uppercase the type
58                         type = s.type = s.type.toUpperCase(),
59
60                         // Determine if request has content
61                         hasContent = s.hasContent = ! rnoContent.test( type ),
62
63                         // Extract dataTypes list
64                         dataType = s.dataType,
65                         dataTypes = s.dataTypes = dataType ? jQuery.trim(dataType).toLowerCase().split(/\s+/) : ["*"],
66
67                         // Determine if a cross-domain request is in order
68                         parts = rurl.exec( url.toLowerCase() ),
69                         loc = location,
70                         crossDomain = s.crossDomain = !!( parts && ( parts[1] && parts[1] != loc.protocol || parts[2] != loc.host ) ),
71
72                         // Get other options locally
73                         data = s.data,
74                         originalContentType = s.contentType,
75                         prefilters = s.prefilters,
76                         accepts = s.accepts,
77                         headers = s.headers,
78
79                         // Other Variables
80                         transportDataType,
81                         i;
82
83                 // Convert data if not already a string
84                 if ( data && s.processData && typeof data != "string" ) {
85                         data = s.data = jQuery.param( data , s.traditional );
86                 }
87
88                 // Get internal
89                 internal = jQuery.xhr.prefilter( s ).transport( s );
90                 
91                 // Re-actualize url & data
92                 url = s.url;
93                 data = s.data;
94
95                 // If internal was found
96                 if ( internal ) {
97
98                         // Get transportDataType
99                         transportDataType = dataTypes[0];
100
101                         // More options handling for requests with no content
102                         if ( ! hasContent ) {
103
104                                 // If data is available, append data to url
105                                 if ( data ) {
106                                         url += (rquery_xhr.test(url) ? "&" : "?") + data;
107                                 }
108
109                                 // Add anti-cache in url if needed
110                                 if ( s.cache === false ) {
111
112                                         var ts = jQuery.now(),
113                                                 // try replacing _= if it is there
114                                                 ret = url.replace(rts, "$1_=" + ts );
115
116                                         // if nothing was replaced, add timestamp to the end
117                                         url = ret + ((ret == url) ? (rquery_xhr.test(url) ? "&" : "?") + "_=" + ts : "");
118                                 }
119
120                                 s.url = url;
121                         }
122
123                         // Set the correct header, if data is being sent
124                         if ( ( data && hasContent ) || originalContentType ) {
125                                 requestHeaders["content-type"] = s.contentType;
126                         }
127
128                         // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
129                         if ( s.ifModified ) {
130                                 if ( jQuery_lastModified[url] ) {
131                                         requestHeaders["if-modified-since"] = jQuery_lastModified[url];
132                                 }
133                                 if ( jQuery_etag[url] ) {
134                                         requestHeaders["if-none-match"] = jQuery_etag[url];
135                                 }
136                         }
137
138                         // Set the Accepts header for the server, depending on the dataType
139                         requestHeaders.accept = transportDataType && accepts[ transportDataType ] ?
140                                 accepts[ transportDataType ] + ( transportDataType !== "*" ? ", */*; q=0.01" : "" ) :
141                                 accepts[ "*" ];
142
143                         // Check for headers option
144                         for ( i in headers ) {
145                                 requestHeaders[ i.toLowerCase() ] = headers[ i ];
146                         }
147                 }
148                 
149                 callbackContext = s.context || s;
150                 globalEventContext = s.context ? jQuery(s.context) : jQuery.event;
151                 
152                 for ( i in { success:1, error:1, complete:1 } ) {
153                         xhr[ i ]( s[ i ] );
154                 }
155
156                 // Watch for a new set of requests
157                 if ( s.global && jQuery.active++ === 0 ) {
158                         jQuery.event.trigger( "ajaxStart" );
159                 }
160
161                 done = whenDone;
162         }
163
164         function whenDone(status, statusText, response, headers) {
165
166                 // Called once
167                 done = undefined;
168
169                 // Reset sendFlag
170                 sendFlag = 0;
171
172                 // Cache response headers
173                 responseHeadersString = headers || "";
174
175                 // Clear timeout if it exists
176                 if ( timeoutTimer ) {
177                         clearTimeout(timeoutTimer);
178                 }
179
180                 var // Reference url
181                         url = s.url,
182                         // and ifModified status
183                         ifModified = s.ifModified,
184
185                         // Is it a success?
186                         isSuccess = 0,
187                         // Stored success
188                         success,
189                         // Stored error
190                         error = statusText;
191
192                 // If not timeout, force a jQuery-compliant status text
193                 if ( statusText != "timeout" ) {
194                         statusText = ( status >= 200 && status < 300 ) ?
195                                 "success" :
196                                 ( status === 304 ? "notmodified" : "error" );
197                 }
198
199                 // If successful, handle type chaining
200                 if ( statusText === "success" || statusText === "notmodified" ) {
201
202                         // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
203                         if ( ifModified ) {
204                                 var lastModified = xhr.getResponseHeader("Last-Modified"),
205                                         etag = xhr.getResponseHeader("Etag");
206
207                                 if (lastModified) {
208                                         jQuery_lastModified[url] = lastModified;
209                                 }
210                                 if (etag) {
211                                         jQuery_etag[url] = etag;
212                                 }
213                         }
214
215                         if ( ifModified && statusText === "notmodified" ) {
216
217                                 success = null;
218                                 isSuccess = 1;
219
220                         } else {
221                                 // Chain data conversions and determine the final value
222                                 // (if an exception is thrown in the process, it'll be notified as an error)
223                                 try {
224                                         
225                                         var i,
226                                                 current,
227                                                 prev,
228                                                 checker,
229                                                 conv1,
230                                                 conv2,
231                                                 oneConv,
232                                                 convertion,
233                                                 dataTypes = s.dataTypes,
234                                                 dataCheckers = s.dataCheckers,
235                                                 dataConverters = s.dataConverters,
236                                                 dataFilter = s.dataFilter,
237                                                 responses = {
238                                                         "xml": "XML",
239                                                         "text": "Text"
240                                                 };
241                                         
242                                         for( i = 0 ; i < dataTypes.length ; i++ ) {
243                                                 
244                                                 current = dataTypes[ i ];
245                                                 
246                                                 if ( i ) {
247                                                         
248                                                         prev = dataTypes[ i - 1 ];
249                                                         
250                                                         if ( prev === "*" ) {
251                                                                 
252                                                                 prev = current;
253                                                                 
254                                                         } else if ( current !== "*" && prev !== current ) {
255                                                         
256                                                                 oneConv = conv1 = 
257                                                                         dataConverters[ ( conversion = prev + " => " + current ) ] ||
258                                                                         dataConverters[ "* => " + current ];
259                                                                 
260                                                                 if ( ! oneConv && prev !== "text" && current !== "text" ) {
261                                                                         conv1 = dataConverters[ prev + " => text" ] || dataConverters[ "* => text" ];
262                                                                         conv2 = dataConverters[ "text => " + current ];
263                                                                 }
264                                                                 if ( oneConv || conv1 && conv2 ) {
265                                                                         response = oneConv ? conv1( response ) : conv2( conv1( response ) );
266                                                                 } else {
267                                                                         throw "no " + conversion;
268                                                                 }
269                                                         }
270                                                 }
271                                                 
272                                                 checker = dataCheckers[ current ];
273                                                 
274                                                 if ( response != null && checker ) {
275                                                         checker( response );
276                                                 }
277                                                 
278                                                 if ( responses[ current ] ) {
279                                                         xhr[ "response" + responses[ current ] ] = response;
280                                                         responses[ current ] = 0;
281                                                 }
282                                                 
283                                                 if ( ! i && dataFilter ) {
284                                                         
285                                                         response = dataFilter( response );
286                                                         
287                                                         dataTypes = s.dataTypes;
288                                                         dataFilter = 0;
289                                                         i--;
290                                                 }
291                                         }
292
293                                         // We have a real success
294                                         success = response;
295                                         isSuccess = 1;
296
297                                 } catch(e) {
298
299                                         statusText = "parsererror";
300                                         error = "" + e;
301
302                                 }
303                         }
304
305                 } else { // if not success, mark it as an error
306
307                                 error = error || statusText;
308
309                 }
310
311                 // Set data for the fake xhr object
312                 xhr.status = status;
313                 xhr.statusText = statusText;
314
315                 // Keep local copies of vars in case callbacks re-use the xhr
316                 var _s = s,
317                         _deferred = deferred,
318                         _completeDeferred = completeDeferred,
319                         _callbackContext = callbackContext,
320                         _globalEventContext = globalEventContext;
321                         
322                         
323                 // Set state if the xhr hasn't been re-used
324                 function _setState( value ) {
325                         if ( xhr.readyState && s === _s ) {
326                                 setState( value );
327                         }
328                 }
329
330                 // Really completed?
331                 if ( status && s.async ) {
332                         setState( 2 );
333                         _setState( 3 );
334                 }
335
336                 // We're done
337                 _setState( 4 );
338                 
339                 // Success/Error
340                 if ( isSuccess ) {
341                         _deferred.fire( _callbackContext , [ success , statusText , xhr ] );
342                 } else {
343                         _deferred.fireReject( _callbackContext , [ xhr , statusText , error ] );
344                 }
345                 
346                 if ( _s.global ) {
347                         _globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ xhr , _s , isSuccess ? success : error ] );
348                 }
349                 
350                 // Complete
351                 _completeDeferred.fire( _callbackContext, [ xhr , statusText ] );
352                 
353                 if ( _s.global ) {
354                         _globalEventContext.trigger( "ajaxComplete", [xhr, _s] );
355                         // Handle the global AJAX counter
356                         if ( ! --jQuery.active ) {
357                                 jQuery.event.trigger( "ajaxStop" );
358                         }
359                 }
360         }
361
362         // Ready state control
363         function checkState( expected , test ) {
364                 if ( expected !== true && ( expected === false || test === false || xhr.readyState !== expected ) ) {
365                         jQuery.error("INVALID_STATE_ERR");
366                 }
367         }
368
369         // Ready state change
370         function setState( value ) {
371                 xhr.readyState = value;
372                 if ( jQuery.isFunction( xhr.onreadystatechange ) ) {
373                         xhr.onreadystatechange();
374                 }
375         }
376
377         var // jQuery lists
378                 jQuery_lastModified = jQuery.lastModified,
379                 jQuery_etag = jQuery.etag,
380                 // Options object
381                 s,
382                 // Callback stuff
383                 callbackContext,
384                 globalEventContext,
385                 deferred,
386                 completeDeferred,
387                 // Headers (they are sent all at once)
388                 requestHeaders,
389                 // Response headers
390                 responseHeadersString,
391                 responseHeaders,
392                 // Done callback
393                 done,
394                 // transport
395                 internal,
396                 // timeout handle
397                 timeoutTimer,
398                 // The send flag
399                 sendFlag,
400                 // Fake xhr
401                 xhr = {
402                         // state
403                         readyState: 0,
404
405                         // Callback
406                         onreadystatechange: null,
407
408                         // Open
409                         open: function(type, url, async, username, password) {
410
411                                 xhr.abort();
412                                 reset();
413
414                                 s = {
415                                         type: type,
416                                         url: url,
417                                         async: async,
418                                         username: username,
419                                         password: password
420                                 };
421
422                                 setState(1);
423
424                                 return xhr;
425                         },
426
427                         // Send
428                         send: function(data, moreOptions) {
429
430                                 checkState(1 , !sendFlag);
431
432                                 s.data = data;
433
434                                 s = jQuery.extend( true,
435                                         {},
436                                         jQuery.ajaxSettings,
437                                         s,
438                                         moreOptions || ( moreOptions === false ? { global: false } : {} ) );
439
440                                 if ( moreOptions ) {
441                                         // We force the original context
442                                         // (plain objects used as context get extended)
443                                         s.context = moreOptions.context;
444                                 }
445
446                                 init();
447
448                                 // If not internal, abort
449                                 if ( ! internal ) {
450                                         done( 0 , "transport not found" );
451                                         return false;
452                                 }
453
454                                 // Allow custom headers/mimetypes and early abort
455                                 if ( s.beforeSend ) {
456
457                                         var _s = s;
458
459                                         if ( s.beforeSend.call(callbackContext, xhr, s) === false || ! xhr.readyState || _s !== s ) {
460
461                                                 // Abort if not done
462                                                 if ( xhr.readyState && _s === s ) {
463                                                         xhr.abort();
464                                                 }
465
466                                                 // Handle the global AJAX counter
467                                                 if ( _s.global && ! --jQuery.active ) {
468                                                         jQuery.event.trigger( "ajaxStop" );
469                                                 }
470
471                                                 return false;
472                                         }
473                                 }
474
475                                 sendFlag = 1;
476
477                                 // Send global event
478                                 if ( s.global ) {
479                                         globalEventContext.trigger("ajaxSend", [xhr, s]);
480                                 }
481
482                                 // Timeout
483                                 if ( s.async && s.timeout > 0 ) {
484                                         timeoutTimer = setTimeout(function(){
485                                                 xhr.abort("timeout");
486                                         }, s.timeout);
487                                 }
488
489                                 if ( s.async ) {
490                                         setState(1);
491                                 }
492
493                                 try {
494
495                                         internal.send(requestHeaders, done);
496                                         return xhr;
497
498                                 } catch (e) {
499
500                                         if ( done ) {
501
502                                                 done(0, "error", "" + e);
503
504                                         } else {
505
506                                                 jQuery.error(e);
507
508                                         }
509                                 }
510
511                                 return false;
512                         },
513
514                         // Caches the header
515                         setRequestHeader: function(name,value) {
516                                 checkState(1, !sendFlag);
517                                 requestHeaders[ name.toLowerCase() ] = value;
518                                 return xhr;
519                         },
520
521                         // Raw string
522                         getAllResponseHeaders: function() {
523                                 return xhr.readyState <= 1 ? "" : responseHeadersString;
524                         },
525
526                         // Builds headers hashtable if needed
527                         getResponseHeader: function( key ) {
528
529                                 if ( xhr.readyState <= 1 ) {
530
531                                         return null;
532
533                                 }
534
535                                 if ( responseHeaders === undefined ) {
536
537                                         responseHeaders = {};
538
539                                         if ( typeof responseHeadersString === "string" ) {
540
541                                                 var match;
542
543                                                 while( ( match = rheaders.exec( responseHeadersString ) ) ) {
544                                                         responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
545                                                 }
546                                         }
547                                 }
548                                 return responseHeaders[ key.toLowerCase() ];
549                         },
550
551                         // Cancel the request
552                         abort: function(statusText) {
553                                 if (internal) {
554                                         internal.abort( statusText || "abort" );
555                                 }
556                                 xhr.readyState = 0;
557                         }
558                 };
559
560         // Init data (so that we can bind callbacks early
561         reset(1);
562
563         // Return the xhr emulation
564         return xhr;
565 };
566
567 // Execute or select from functions in a given structure of options
568 function xhr_selectOrExecute( structure , s ) {
569
570         var dataTypes = s.dataTypes,
571                 transportDataType,
572                 list,
573                 selected,
574                 i,
575                 length,
576                 checked = {},
577                 flag,
578                 noSelect = structure !== "transports";
579                 
580         function initSearch( dataType ) {
581
582                 flag = transportDataType !== dataType && ! checked[ dataType ];
583
584                 if ( flag ) {
585
586                         checked[ dataType ] = 1;
587                         transportDataType = dataType;
588                         list = s[ structure ][ dataType ];
589                         i = -1;
590                         length = list ? list.length : 0 ;
591                 }
592
593                 return flag;
594         }
595
596         initSearch( dataTypes[ 0 ] );
597
598         for ( i = 0 ; ( noSelect || ! selected ) && i <= length ; i++ ) {
599                 
600                 if ( i === length ) {
601
602                         initSearch( "*" );
603
604                 } else {
605
606                         selected = list[ i ]( s , determineDataType );
607
608                         // If we got redirected to another dataType
609                         // Search there (if not in progress or already tried)
610                         if ( typeof( selected ) === "string" &&
611                                 initSearch( selected ) ) {
612
613                                 dataTypes.unshift( selected );
614                                 selected = 0;
615                         }
616                 }
617         }
618
619         return noSelect ? jQuery.xhr : selected;
620 }
621
622 // Add an element to one of the xhr structures in ajaxSettings
623 function xhr_addElement( structure , args ) {
624                 
625         var i,
626                 j,
627                 start = 0,
628                 length = args.length,
629                 dataTypes = [ "*" ],
630                 dLength = 1,
631                 dataType,
632                 functors = [],
633                 first,
634                 append,
635                 list;
636                 
637         if ( length ) {
638                 
639                 first = jQuery.type( args[ 0 ] );
640                 
641                 if ( first === "object" ) {
642                         return xhr_selectOrExecute( structure , args[ 0 ] );
643                 }
644                 
645                 structure = jQuery.ajaxSettings[ structure ];
646                 
647                 if ( first !== "function" ) {
648                         
649                         dataTypes = args[ 0 ].toLowerCase().split(/\s+/);
650                         dLength = dataTypes.length;
651                         start = 1;
652                         
653                 }
654                 
655                 if ( dLength && start < length ) {
656                         
657                         functors = sliceFunc.call( args , start );
658                         
659                         length -= start;
660                                         
661                         for( i = 0 ; i < dLength ; i++ ) {
662                                 
663                                 dataType = dataTypes[ i ];
664                                 
665                                 first = /^\+/.test( dataType );
666                                 
667                                 if (first) {
668                                         dataType = dataType.substr(1);
669                                 }
670                                 
671                                 if ( dataType !== "" ) {
672                                 
673                                         append = Array.prototype[ first ? "unshift" : "push" ];
674                                         
675                                         list = structure[ dataType ] = structure[ dataType ] || [];
676                         
677                                         for ( j = 0; j < length; j++ ) {
678                                                 append.call( list , functors[ j ] );
679                                         }
680                                 }
681                         }
682                 }
683         }
684         
685         return jQuery.xhr;
686 }
687
688 // Install prefilter & transport methods
689 jQuery.each( [ "prefilter" , "transport" ] , function( _ , name ) {
690         _ = name + "s";
691         jQuery.xhr[ name ] = function() {
692                 return xhr_addElement( _ , arguments );
693         };
694 } );
695         
696 // Utility function that handles dataType when response is received
697 // (for those transports that can give text or xml responses)
698 function determineDataType( s , ct , text , xml ) {
699
700         var autoDataType = s.autoDataType,
701                 type,
702                 regexp,
703                 dataTypes = s.dataTypes,
704                 transportDataType = dataTypes[0],
705                 response;
706
707         // Auto (xml, json, script or text determined given headers)
708         if ( transportDataType === "*" ) {
709
710                 for ( type in autoDataType ) {
711                         if ( ( regexp = autoDataType[ type ] ) && regexp.test( ct ) ) {
712                                 transportDataType = dataTypes[0] = type;
713                                 break;
714                         }
715                 }
716         }
717
718         // xml and parsed as such
719         if ( transportDataType === "xml" &&
720                 xml &&
721                 xml.documentElement /* #4958 */ ) {
722
723                 response = xml;
724
725         // Text response was provided
726         } else {
727
728                 response = text;
729
730                 // If it's not really text, defer to dataConverters
731                 if ( transportDataType !== "text" ) {
732                         dataTypes.unshift( "text" );
733                 }
734
735         }
736
737         return response;
738 }
739
740 })( jQuery );