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