b9db717712f3839776cf8377b16a56a73288249e
[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                                                 dataConverters = s.dataConverters,
235                                                 dataFilter = s.dataFilter,
236                                                 responses = {
237                                                         "xml": "XML",
238                                                         "text": "Text"
239                                                 };
240                                         
241                                         for( i = 0 ; i < dataTypes.length ; i++ ) {
242                                                 
243                                                 current = dataTypes[ i ];
244                                                 
245                                                 if ( responses[ current ] ) {
246                                                         xhr[ "response" + responses[ current ] ] = response;
247                                                         responses[ current ] = 0;
248                                                 }
249                                                 
250                                                 if ( i ) {
251                                                         
252                                                         if ( prev !== "*" && current !== "*" && prev !== current ) {
253                                                         
254                                                                 oneConv = conv1 = 
255                                                                         dataConverters[ ( conversion = prev + " " + current ) ] ||
256                                                                         dataConverters[ "* " + current ];
257                                                                 
258                                                                 if ( oneConv !== true ) {
259                                                                         
260                                                                         if ( ! oneConv && prev !== "text" && current !== "text" ) {
261                                                                                 conv1 = dataConverters[ prev + " text" ] || dataConverters[ "* text" ];
262                                                                                 conv2 = dataConverters[ "text " + current ];
263                                                                         }
264                                                                         
265                                                                         if ( oneConv || conv1 && conv2 ) {
266                                                                                 response = oneConv ? conv1( response ) : conv2( conv1( response ) );
267                                                                         } else {
268                                                                                 throw "no " + conversion;
269                                                                         }
270                                                                 }
271                                                         }
272                                                 } else if ( dataFilter ) {
273                                                         
274                                                         response = s.dataFilter( response );
275                                                         dataTypes = s.dataTypes;
276                                                 }
277                                                 
278                                                 prev = current;
279                                         }
280
281                                         // We have a real success
282                                         success = response;
283                                         isSuccess = 1;
284
285                                 } catch(e) {
286
287                                         statusText = "parsererror";
288                                         error = "" + e;
289
290                                 }
291                         }
292
293                 } else { // if not success, mark it as an error
294
295                                 error = error || statusText;
296
297                 }
298
299                 // Set data for the fake xhr object
300                 xhr.status = status;
301                 xhr.statusText = statusText;
302
303                 // Keep local copies of vars in case callbacks re-use the xhr
304                 var _s = s,
305                         _deferred = deferred,
306                         _completeDeferred = completeDeferred,
307                         _callbackContext = callbackContext,
308                         _globalEventContext = globalEventContext;
309                         
310                         
311                 // Set state if the xhr hasn't been re-used
312                 function _setState( value ) {
313                         if ( xhr.readyState && s === _s ) {
314                                 setState( value );
315                         }
316                 }
317
318                 // Really completed?
319                 if ( status && s.async ) {
320                         setState( 2 );
321                         _setState( 3 );
322                 }
323
324                 // We're done
325                 _setState( 4 );
326                 
327                 // Success/Error
328                 if ( isSuccess ) {
329                         _deferred.fire( _callbackContext , [ success , statusText , xhr ] );
330                 } else {
331                         _deferred.fireReject( _callbackContext , [ xhr , statusText , error ] );
332                 }
333                 
334                 if ( _s.global ) {
335                         _globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ xhr , _s , isSuccess ? success : error ] );
336                 }
337                 
338                 // Complete
339                 _completeDeferred.fire( _callbackContext, [ xhr , statusText ] );
340                 
341                 if ( _s.global ) {
342                         _globalEventContext.trigger( "ajaxComplete", [xhr, _s] );
343                         // Handle the global AJAX counter
344                         if ( ! --jQuery.active ) {
345                                 jQuery.event.trigger( "ajaxStop" );
346                         }
347                 }
348         }
349
350         // Ready state control
351         function checkState( expected , test ) {
352                 if ( expected !== true && ( expected === false || test === false || xhr.readyState !== expected ) ) {
353                         jQuery.error("INVALID_STATE_ERR");
354                 }
355         }
356
357         // Ready state change
358         function setState( value ) {
359                 xhr.readyState = value;
360                 if ( jQuery.isFunction( xhr.onreadystatechange ) ) {
361                         xhr.onreadystatechange();
362                 }
363         }
364
365         var // jQuery lists
366                 jQuery_lastModified = jQuery.lastModified,
367                 jQuery_etag = jQuery.etag,
368                 // Options object
369                 s,
370                 // Callback stuff
371                 callbackContext,
372                 globalEventContext,
373                 deferred,
374                 completeDeferred,
375                 // Headers (they are sent all at once)
376                 requestHeaders,
377                 // Response headers
378                 responseHeadersString,
379                 responseHeaders,
380                 // Done callback
381                 done,
382                 // transport
383                 internal,
384                 // timeout handle
385                 timeoutTimer,
386                 // The send flag
387                 sendFlag,
388                 // Fake xhr
389                 xhr = {
390                         // state
391                         readyState: 0,
392
393                         // Callback
394                         onreadystatechange: null,
395
396                         // Open
397                         open: function(type, url, async, username, password) {
398
399                                 xhr.abort();
400                                 reset();
401
402                                 s = {
403                                         type: type,
404                                         url: url,
405                                         async: async,
406                                         username: username,
407                                         password: password
408                                 };
409
410                                 setState(1);
411
412                                 return xhr;
413                         },
414
415                         // Send
416                         send: function(data, moreOptions) {
417
418                                 checkState(1 , !sendFlag);
419
420                                 s.data = data;
421
422                                 s = jQuery.extend( true,
423                                         {},
424                                         jQuery.ajaxSettings,
425                                         s,
426                                         moreOptions || ( moreOptions === false ? { global: false } : {} ) );
427
428                                 if ( moreOptions ) {
429                                         // We force the original context
430                                         // (plain objects used as context get extended)
431                                         s.context = moreOptions.context;
432                                 }
433
434                                 init();
435
436                                 // If not internal, abort
437                                 if ( ! internal ) {
438                                         done( 0 , "transport not found" );
439                                         return false;
440                                 }
441
442                                 // Allow custom headers/mimetypes and early abort
443                                 if ( s.beforeSend ) {
444
445                                         var _s = s;
446
447                                         if ( s.beforeSend.call(callbackContext, xhr, s) === false || ! xhr.readyState || _s !== s ) {
448
449                                                 // Abort if not done
450                                                 if ( xhr.readyState && _s === s ) {
451                                                         xhr.abort();
452                                                 }
453
454                                                 // Handle the global AJAX counter
455                                                 if ( _s.global && ! --jQuery.active ) {
456                                                         jQuery.event.trigger( "ajaxStop" );
457                                                 }
458
459                                                 return false;
460                                         }
461                                 }
462
463                                 sendFlag = 1;
464
465                                 // Send global event
466                                 if ( s.global ) {
467                                         globalEventContext.trigger("ajaxSend", [xhr, s]);
468                                 }
469
470                                 // Timeout
471                                 if ( s.async && s.timeout > 0 ) {
472                                         timeoutTimer = setTimeout(function(){
473                                                 xhr.abort("timeout");
474                                         }, s.timeout);
475                                 }
476
477                                 if ( s.async ) {
478                                         setState(1);
479                                 }
480
481                                 try {
482
483                                         internal.send(requestHeaders, done);
484                                         return xhr;
485
486                                 } catch (e) {
487
488                                         if ( done ) {
489
490                                                 done(0, "error", "" + e);
491
492                                         } else {
493
494                                                 jQuery.error(e);
495
496                                         }
497                                 }
498
499                                 return false;
500                         },
501
502                         // Caches the header
503                         setRequestHeader: function(name,value) {
504                                 checkState(1, !sendFlag);
505                                 requestHeaders[ name.toLowerCase() ] = value;
506                                 return xhr;
507                         },
508
509                         // Raw string
510                         getAllResponseHeaders: function() {
511                                 return xhr.readyState <= 1 ? "" : responseHeadersString;
512                         },
513
514                         // Builds headers hashtable if needed
515                         getResponseHeader: function( key ) {
516
517                                 if ( xhr.readyState <= 1 ) {
518
519                                         return null;
520
521                                 }
522
523                                 if ( responseHeaders === undefined ) {
524
525                                         responseHeaders = {};
526
527                                         if ( typeof responseHeadersString === "string" ) {
528
529                                                 var match;
530
531                                                 while( ( match = rheaders.exec( responseHeadersString ) ) ) {
532                                                         responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
533                                                 }
534                                         }
535                                 }
536                                 return responseHeaders[ key.toLowerCase() ];
537                         },
538
539                         // Cancel the request
540                         abort: function(statusText) {
541                                 if (internal) {
542                                         internal.abort( statusText || "abort" );
543                                 }
544                                 xhr.readyState = 0;
545                         }
546                 };
547
548         // Init data (so that we can bind callbacks early
549         reset(1);
550
551         // Return the xhr emulation
552         return xhr;
553 };
554
555 // Execute or select from functions in a given structure of options
556 function xhr_selectOrExecute( structure , s ) {
557
558         var dataTypes = s.dataTypes,
559                 transportDataType,
560                 list,
561                 selected,
562                 i,
563                 length,
564                 checked = {},
565                 flag,
566                 noSelect = structure !== "transports";
567                 
568         function initSearch( dataType ) {
569
570                 flag = transportDataType !== dataType && ! checked[ dataType ];
571
572                 if ( flag ) {
573
574                         checked[ dataType ] = 1;
575                         transportDataType = dataType;
576                         list = s[ structure ][ dataType ];
577                         i = -1;
578                         length = list ? list.length : 0 ;
579                 }
580
581                 return flag;
582         }
583
584         initSearch( dataTypes[ 0 ] );
585
586         for ( i = 0 ; ( noSelect || ! selected ) && i <= length ; i++ ) {
587                 
588                 if ( i === length ) {
589
590                         initSearch( "*" );
591
592                 } else {
593
594                         selected = list[ i ]( s , determineDataType );
595
596                         // If we got redirected to another dataType
597                         // Search there (if not in progress or already tried)
598                         if ( typeof( selected ) === "string" &&
599                                 initSearch( selected ) ) {
600
601                                 dataTypes.unshift( selected );
602                                 selected = 0;
603                         }
604                 }
605         }
606
607         return noSelect ? jQuery.xhr : selected;
608 }
609
610 // Add an element to one of the xhr structures in ajaxSettings
611 function xhr_addElement( structure , args ) {
612                 
613         var i,
614                 j,
615                 start = 0,
616                 length = args.length,
617                 dataTypes = [ "*" ],
618                 dLength = 1,
619                 dataType,
620                 functors = [],
621                 first,
622                 append,
623                 list;
624                 
625         if ( length ) {
626                 
627                 first = jQuery.type( args[ 0 ] );
628                 
629                 if ( first === "object" ) {
630                         return xhr_selectOrExecute( structure , args[ 0 ] );
631                 }
632                 
633                 structure = jQuery.ajaxSettings[ structure ];
634                 
635                 if ( first !== "function" ) {
636                         
637                         dataTypes = args[ 0 ].toLowerCase().split(/\s+/);
638                         dLength = dataTypes.length;
639                         start = 1;
640                         
641                 }
642                 
643                 if ( dLength && start < length ) {
644                         
645                         functors = sliceFunc.call( args , start );
646                         
647                         length -= start;
648                                         
649                         for( i = 0 ; i < dLength ; i++ ) {
650                                 
651                                 dataType = dataTypes[ i ];
652                                 
653                                 first = /^\+/.test( dataType );
654                                 
655                                 if (first) {
656                                         dataType = dataType.substr(1);
657                                 }
658                                 
659                                 if ( dataType !== "" ) {
660                                 
661                                         append = Array.prototype[ first ? "unshift" : "push" ];
662                                         
663                                         list = structure[ dataType ] = structure[ dataType ] || [];
664                         
665                                         for ( j = 0; j < length; j++ ) {
666                                                 append.call( list , functors[ j ] );
667                                         }
668                                 }
669                         }
670                 }
671         }
672         
673         return jQuery.xhr;
674 }
675
676 // Install prefilter & transport methods
677 jQuery.each( [ "prefilter" , "transport" ] , function( _ , name ) {
678         _ = name + "s";
679         jQuery.xhr[ name ] = function() {
680                 return xhr_addElement( _ , arguments );
681         };
682 } );
683         
684 // Utility function that handles dataType when response is received
685 // (for those transports that can give text or xml responses)
686 function determineDataType( s , ct , text , xml ) {
687
688         var autoDataType = s.autoDataType,
689                 type,
690                 regexp,
691                 dataTypes = s.dataTypes,
692                 transportDataType = dataTypes[0],
693                 response;
694
695         // Auto (xml, json, script or text determined given headers)
696         if ( transportDataType === "*" ) {
697
698                 for ( type in autoDataType ) {
699                         if ( ( regexp = autoDataType[ type ] ) && regexp.test( ct ) ) {
700                                 transportDataType = dataTypes[0] = type;
701                                 break;
702                         }
703                 }
704         }
705
706         // xml and parsed as such
707         if ( transportDataType === "xml" &&
708                 xml &&
709                 xml.documentElement /* #4958 */ ) {
710
711                 response = xml;
712
713         // Text response was provided
714         } else {
715
716                 response = text;
717
718                 // If it's not really text, defer to dataConverters
719                 if ( transportDataType !== "text" ) {
720                         dataTypes.unshift( "text" );
721                 }
722
723         }
724
725         return response;
726 }
727
728 })( jQuery );