├── LICENSE ├── README.markdown ├── XMLHttpRequest.js └── bower.json /LICENSE: -------------------------------------------------------------------------------- 1 | XMLHttpRequest.js Copyright (C) 2011 Sergey Ilinsky (http://www.ilinsky.com) 2 | 3 | This work is free software; you can redistribute it and/or modify it under the 4 | terms of the GNU Lesser General Public License as published by the Free Software 5 | Foundation; either version 2.1 of the License, or (at your option) any later 6 | version. 7 | 8 | This work is distributed in the hope that it will be useful, but without any 9 | warranty; without even the implied warranty of merchantability or fitness for a 10 | particular purpose. See the GNU Lesser General Public License for more details. 11 | 12 | You should have received a copy of the GNU Lesser General Public License along 13 | with this library; 14 | if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, 15 | Boston, MA 02111-1307 USA -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Scope of implementation 2 | ===== 3 | 4 | 1. Deliver unobtrusive standard-compliant cross-browser implementation of the 5 | [XMLHttpRequest 1.0][1] 6 | 2. Fix browsers quirks observed in their native XMLHttpRequest object implementations 7 | 3. Enable transparent sniffing of XMLHttpRequest object activity 8 | 9 | How To Use 10 | ===== 11 | 12 | ```html 13 |
14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | XMLHttpRequest 2 features 21 | ===== 22 | 23 | The library does not and will not add support for any features found in [XMLHttpRequest 2][2] since 24 | it is not possible to provide complete fallback implementation in older Internet Explorer browser 25 | for which this library was primarily developed. If you use this library, I recommend starting adding 26 | conditional HTML comments to limit exposure of the library only to browsers where it is really needed. 27 | 28 | ```html 29 | 30 | 33 | 34 | ``` 35 | 36 | 37 | 38 | Links to online resources 39 | ===== 40 | 41 | 1. [XMLHttpRequest object implementation explained][3] 42 | 2. [XMLHttpRequest 1.0 specification][1] 43 | 44 | [1]: http://www.w3.org/TR/2007/WD-XMLHttpRequest-20071026/ 45 | [2]: http://www.w3.org/TR/XMLHttpRequest/ 46 | [3]: http://www.ilinsky.com/articles/XMLHttpRequest -------------------------------------------------------------------------------- /XMLHttpRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * XMLHttpRequest.js Copyright (C) 2011 Sergey Ilinsky (http://www.ilinsky.com) 3 | * 4 | * This work is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; either version 2.1 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This work is distributed in the hope that it will be useful, 10 | * but without any warranty; without even the implied warranty of 11 | * merchantability or fitness for a particular purpose. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with this library; if not, write to the Free Software Foundation, Inc., 16 | * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | */ 18 | 19 | (function () { 20 | 21 | // Save reference to earlier defined object implementation (if any) 22 | var oXMLHttpRequest = window.XMLHttpRequest; 23 | 24 | // Define on browser type 25 | var bGecko = !!window.controllers; 26 | var bIE = !!window.document.namespaces; 27 | var bIE7 = bIE && window.navigator.userAgent.match(/MSIE 7.0/); 28 | 29 | // Enables "XMLHttpRequest()" call next to "new XMLHttpRequest()" 30 | function fXMLHttpRequest() { 31 | if (!window.XMLHttpRequest || bIE7) { 32 | this._object = new window.ActiveXObject("Microsoft.XMLHTTP"); 33 | } // only use initial XHR object internally if current reference to XHR is our normalized replacement 34 | else if (window.XMLHttpRequest.isNormalizedObject) { 35 | this._object = new oXMLHttpRequest(); 36 | } // otherwise use whatever is currently referenced by XMLHttpRequest 37 | else { 38 | this._object = new window.XMLHttpRequest(); 39 | } 40 | this._listeners = []; 41 | } 42 | 43 | // Constructor 44 | function cXMLHttpRequest() { 45 | return new fXMLHttpRequest; 46 | } 47 | cXMLHttpRequest.prototype = fXMLHttpRequest.prototype; 48 | 49 | // BUGFIX: Firefox with Firebug installed would break pages if not executed 50 | if (bGecko && oXMLHttpRequest.wrapped) { 51 | cXMLHttpRequest.wrapped = oXMLHttpRequest.wrapped; 52 | } 53 | 54 | // Marker to be able to easily identify our object 55 | cXMLHttpRequest.isNormalizedObject = true; 56 | 57 | // Constants 58 | cXMLHttpRequest.UNSENT = 0; 59 | cXMLHttpRequest.OPENED = 1; 60 | cXMLHttpRequest.HEADERS_RECEIVED = 2; 61 | cXMLHttpRequest.LOADING = 3; 62 | cXMLHttpRequest.DONE = 4; 63 | 64 | // Interface level constants 65 | cXMLHttpRequest.prototype.UNSENT = cXMLHttpRequest.UNSENT; 66 | cXMLHttpRequest.prototype.OPENED = cXMLHttpRequest.OPENED; 67 | cXMLHttpRequest.prototype.HEADERS_RECEIVED = cXMLHttpRequest.HEADERS_RECEIVED; 68 | cXMLHttpRequest.prototype.LOADING = cXMLHttpRequest.LOADING; 69 | cXMLHttpRequest.prototype.DONE = cXMLHttpRequest.DONE; 70 | 71 | // Public Properties 72 | cXMLHttpRequest.prototype.readyState = cXMLHttpRequest.UNSENT; 73 | cXMLHttpRequest.prototype.responseText = ''; 74 | cXMLHttpRequest.prototype.responseXML = null; 75 | cXMLHttpRequest.prototype.status = 0; 76 | cXMLHttpRequest.prototype.statusText = ''; 77 | 78 | // Priority proposal 79 | cXMLHttpRequest.prototype.priority = "NORMAL"; 80 | 81 | // Instance-level Events Handlers 82 | cXMLHttpRequest.prototype.onreadystatechange = null; 83 | 84 | // Class-level Events Handlers 85 | cXMLHttpRequest.onreadystatechange = null; 86 | cXMLHttpRequest.onopen = null; 87 | cXMLHttpRequest.onsend = null; 88 | cXMLHttpRequest.onabort = null; 89 | 90 | // Public Methods 91 | cXMLHttpRequest.prototype.open = function(sMethod, sUrl, bAsync, sUser, sPassword) { 92 | // http://www.w3.org/TR/XMLHttpRequest/#the-open-method 93 | var sLowerCaseMethod = sMethod.toLowerCase(); 94 | if (sLowerCaseMethod == "connect" || sLowerCaseMethod == "trace" || sLowerCaseMethod == "track") { 95 | // Using a generic error and an int - not too sure all browsers support correctly 96 | // http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#securityerror, so, this is safer 97 | // XXX should do better than that, but this is OT to XHR. 98 | throw new Error(18); 99 | } 100 | 101 | // Delete headers, required when object is reused 102 | delete this._headers; 103 | 104 | // When bAsync parameter value is omitted, use true as default 105 | if (arguments.length < 3) { 106 | bAsync = true; 107 | } 108 | 109 | // Save async parameter for fixing Gecko bug with missing readystatechange in synchronous requests 110 | this._async = bAsync; 111 | 112 | // Set the onreadystatechange handler 113 | var oRequest = this; 114 | var nState = this.readyState; 115 | var fOnUnload = null; 116 | 117 | // BUGFIX: IE - memory leak on page unload (inter-page leak) 118 | if (bIE && bAsync) { 119 | fOnUnload = function() { 120 | if (nState != cXMLHttpRequest.DONE) { 121 | fCleanTransport(oRequest); 122 | // Safe to abort here since onreadystatechange handler removed 123 | oRequest.abort(); 124 | } 125 | }; 126 | window.attachEvent("onunload", fOnUnload); 127 | } 128 | 129 | // Add method sniffer 130 | if (cXMLHttpRequest.onopen) { 131 | cXMLHttpRequest.onopen.apply(this, arguments); 132 | } 133 | 134 | if (arguments.length > 4) { 135 | this._object.open(sMethod, sUrl, bAsync, sUser, sPassword); 136 | } else if (arguments.length > 3) { 137 | this._object.open(sMethod, sUrl, bAsync, sUser); 138 | } else { 139 | this._object.open(sMethod, sUrl, bAsync); 140 | } 141 | 142 | this.readyState = cXMLHttpRequest.OPENED; 143 | fReadyStateChange(this); 144 | 145 | this._object.onreadystatechange = function() { 146 | if (bGecko && !bAsync) { 147 | return; 148 | } 149 | 150 | // Synchronize state 151 | oRequest.readyState = oRequest._object.readyState; 152 | fSynchronizeValues(oRequest); 153 | 154 | // BUGFIX: Firefox fires unnecessary DONE when aborting 155 | if (oRequest._aborted) { 156 | // Reset readyState to UNSENT 157 | oRequest.readyState = cXMLHttpRequest.UNSENT; 158 | 159 | // Return now 160 | return; 161 | } 162 | 163 | if (oRequest.readyState == cXMLHttpRequest.DONE) { 164 | // Free up queue 165 | delete oRequest._data; 166 | 167 | // Uncomment these lines for bAsync 168 | /** 169 | * if (bAsync) { 170 | * fQueue_remove(oRequest); 171 | * } 172 | */ 173 | 174 | fCleanTransport(oRequest); 175 | 176 | // Uncomment this block if you need a fix for IE cache 177 | /** 178 | * // BUGFIX: IE - cache issue 179 | * if (!oRequest._object.getResponseHeader("Date")) { 180 | * // Save object to cache 181 | * oRequest._cached = oRequest._object; 182 | * 183 | * // Instantiate a new transport object 184 | * cXMLHttpRequest.call(oRequest); 185 | * 186 | * // Re-send request 187 | * if (sUser) { 188 | * if (sPassword) { 189 | * oRequest._object.open(sMethod, sUrl, bAsync, sUser, sPassword); 190 | * } else { 191 | * oRequest._object.open(sMethod, sUrl, bAsync); 192 | * } 193 | * 194 | * oRequest._object.setRequestHeader("If-Modified-Since", oRequest._cached.getResponseHeader("Last-Modified") || new window.Date(0)); 195 | * // Copy headers set 196 | * if (oRequest._headers) { 197 | * for (var sHeader in oRequest._headers) { 198 | * // Some frameworks prototype objects with functions 199 | * if (typeof oRequest._headers[sHeader] == "string") { 200 | * oRequest._object.setRequestHeader(sHeader, oRequest._headers[sHeader]); 201 | * } 202 | * } 203 | * } 204 | * oRequest._object.onreadystatechange = function() { 205 | * // Synchronize state 206 | * oRequest.readyState = oRequest._object.readyState; 207 | * 208 | * if (oRequest._aborted) { 209 | * // 210 | * oRequest.readyState = cXMLHttpRequest.UNSENT; 211 | * 212 | * // Return 213 | * return; 214 | * } 215 | * 216 | * if (oRequest.readyState == cXMLHttpRequest.DONE) { 217 | * // Clean Object 218 | * fCleanTransport(oRequest); 219 | * 220 | * // get cached request 221 | * if (oRequest.status == 304) { 222 | * oRequest._object = oRequest._cached; 223 | * } 224 | * 225 | * // 226 | * delete oRequest._cached; 227 | * 228 | * // 229 | * fSynchronizeValues(oRequest); 230 | * 231 | * // 232 | * fReadyStateChange(oRequest); 233 | * 234 | * // BUGFIX: IE - memory leak in interrupted 235 | * if (bIE && bAsync) { 236 | * window.detachEvent("onunload", fOnUnload); 237 | * } 238 | * 239 | * } 240 | * }; 241 | * oRequest._object.send(null); 242 | * 243 | * // Return now - wait until re-sent request is finished 244 | * return; 245 | * }; 246 | */ 247 | 248 | // BUGFIX: IE - memory leak in interrupted 249 | if (bIE && bAsync) { 250 | window.detachEvent("onunload", fOnUnload); 251 | } 252 | 253 | // BUGFIX: Some browsers (Internet Explorer, Gecko) fire OPEN readystate twice 254 | if (nState != oRequest.readyState) { 255 | fReadyStateChange(oRequest); 256 | } 257 | 258 | nState = oRequest.readyState; 259 | } 260 | }; 261 | }; 262 | 263 | cXMLHttpRequest.prototype.send = function(vData) { 264 | // Add method sniffer 265 | if (cXMLHttpRequest.onsend) { 266 | cXMLHttpRequest.onsend.apply(this, arguments); 267 | } 268 | 269 | if (!arguments.length) { 270 | vData = null; 271 | } 272 | 273 | // BUGFIX: Safari - fails sending documents created/modified dynamically, so an explicit serialization required 274 | // BUGFIX: IE - rewrites any custom mime-type to "text/xml" in case an XMLNode is sent 275 | // BUGFIX: Gecko - fails sending Element (this is up to the implementation either to standard) 276 | if (vData && vData.nodeType) { 277 | vData = window.XMLSerializer ? new window.XMLSerializer().serializeToString(vData) : vData.xml; 278 | if (!this._headers["Content-Type"]) { 279 | this._object.setRequestHeader("Content-Type", "application/xml"); 280 | } 281 | } 282 | 283 | this._data = vData; 284 | 285 | /** 286 | * // Add to queue 287 | * if (this._async) { 288 | * fQueue_add(this); 289 | * } else { */ 290 | fXMLHttpRequest_send(this); 291 | /** 292 | * } 293 | */ 294 | }; 295 | 296 | cXMLHttpRequest.prototype.abort = function() { 297 | // Add method sniffer 298 | if (cXMLHttpRequest.onabort) { 299 | cXMLHttpRequest.onabort.apply(this, arguments); 300 | } 301 | 302 | // BUGFIX: Gecko - unnecessary DONE when aborting 303 | if (this.readyState > cXMLHttpRequest.UNSENT) { 304 | this._aborted = true; 305 | } 306 | 307 | this._object.abort(); 308 | 309 | // BUGFIX: IE - memory leak 310 | fCleanTransport(this); 311 | 312 | this.readyState = cXMLHttpRequest.UNSENT; 313 | 314 | delete this._data; 315 | 316 | /* if (this._async) { 317 | * fQueue_remove(this); 318 | * } 319 | */ 320 | }; 321 | 322 | cXMLHttpRequest.prototype.getAllResponseHeaders = function() { 323 | return this._object.getAllResponseHeaders(); 324 | }; 325 | 326 | cXMLHttpRequest.prototype.getResponseHeader = function(sName) { 327 | return this._object.getResponseHeader(sName); 328 | }; 329 | 330 | cXMLHttpRequest.prototype.setRequestHeader = function(sName, sValue) { 331 | // BUGFIX: IE - cache issue 332 | if (!this._headers) { 333 | this._headers = {}; 334 | } 335 | 336 | this._headers[sName] = sValue; 337 | 338 | return this._object.setRequestHeader(sName, sValue); 339 | }; 340 | 341 | // EventTarget interface implementation 342 | cXMLHttpRequest.prototype.addEventListener = function(sName, fHandler, bUseCapture) { 343 | for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++) { 344 | if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture) { 345 | return; 346 | } 347 | } 348 | 349 | // Add listener 350 | this._listeners.push([sName, fHandler, bUseCapture]); 351 | }; 352 | 353 | cXMLHttpRequest.prototype.removeEventListener = function(sName, fHandler, bUseCapture) { 354 | for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++) { 355 | if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture) { 356 | break; 357 | } 358 | } 359 | 360 | // Remove listener 361 | if (oListener) { 362 | this._listeners.splice(nIndex, 1); 363 | } 364 | }; 365 | 366 | cXMLHttpRequest.prototype.dispatchEvent = function(oEvent) { 367 | var oEventPseudo = { 368 | 'type': oEvent.type, 369 | 'target': this, 370 | 'currentTarget': this, 371 | 'eventPhase': 2, 372 | 'bubbles': oEvent.bubbles, 373 | 'cancelable': oEvent.cancelable, 374 | 'timeStamp': oEvent.timeStamp, 375 | 'stopPropagation': function() {}, // There is no flow 376 | 'preventDefault': function() {}, // There is no default action 377 | 'initEvent': function() {} // Original event object should be initialized 378 | }; 379 | 380 | // Execute onreadystatechange 381 | if (oEventPseudo.type == "readystatechange" && this.onreadystatechange) { 382 | (this.onreadystatechange.handleEvent || this.onreadystatechange).apply(this, [oEventPseudo]); 383 | } 384 | 385 | 386 | // Execute listeners 387 | for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++) { 388 | if (oListener[0] == oEventPseudo.type && !oListener[2]) { 389 | (oListener[1].handleEvent || oListener[1]).apply(this, [oEventPseudo]); 390 | } 391 | } 392 | 393 | }; 394 | 395 | // 396 | cXMLHttpRequest.prototype.toString = function() { 397 | return '[' + "object" + ' ' + "XMLHttpRequest" + ']'; 398 | }; 399 | 400 | cXMLHttpRequest.toString = function() { 401 | return '[' + "XMLHttpRequest" + ']'; 402 | }; 403 | 404 | /** 405 | * // Queue manager 406 | * var oQueuePending = {"CRITICAL":[],"HIGH":[],"NORMAL":[],"LOW":[],"LOWEST":[]}, 407 | * aQueueRunning = []; 408 | * function fQueue_add(oRequest) { 409 | * oQueuePending[oRequest.priority in oQueuePending ? oRequest.priority : "NORMAL"].push(oRequest); 410 | * // 411 | * setTimeout(fQueue_process); 412 | * }; 413 | * 414 | * function fQueue_remove(oRequest) { 415 | * for (var nIndex = 0, bFound = false; nIndex < aQueueRunning.length; nIndex++) 416 | * if (bFound) { 417 | * aQueueRunning[nIndex - 1] = aQueueRunning[nIndex]; 418 | * } else { 419 | * if (aQueueRunning[nIndex] == oRequest) { 420 | * bFound = true; 421 | * } 422 | * } 423 | * 424 | * if (bFound) { 425 | * aQueueRunning.length--; 426 | * } 427 | * 428 | * 429 | * // 430 | * setTimeout(fQueue_process); 431 | * }; 432 | * 433 | * function fQueue_process() { 434 | * if (aQueueRunning.length < 6) { 435 | * for (var sPriority in oQueuePending) { 436 | * if (oQueuePending[sPriority].length) { 437 | * var oRequest = oQueuePending[sPriority][0]; 438 | * oQueuePending[sPriority] = oQueuePending[sPriority].slice(1); 439 | * // 440 | * aQueueRunning.push(oRequest); 441 | * // Send request 442 | * fXMLHttpRequest_send(oRequest); 443 | * break; 444 | * } 445 | * } 446 | * } 447 | * }; 448 | */ 449 | 450 | // Helper function 451 | function fXMLHttpRequest_send(oRequest) { 452 | oRequest._object.send(oRequest._data); 453 | 454 | // BUGFIX: Gecko - missing readystatechange calls in synchronous requests 455 | if (bGecko && !oRequest._async) { 456 | oRequest.readyState = cXMLHttpRequest.OPENED; 457 | 458 | // Synchronize state 459 | fSynchronizeValues(oRequest); 460 | 461 | // Simulate missing states 462 | while (oRequest.readyState < cXMLHttpRequest.DONE) { 463 | oRequest.readyState++; 464 | fReadyStateChange(oRequest); 465 | // Check if we are aborted 466 | if (oRequest._aborted) { 467 | return; 468 | } 469 | } 470 | } 471 | } 472 | 473 | function fReadyStateChange(oRequest) { 474 | // Sniffing code 475 | if (cXMLHttpRequest.onreadystatechange){ 476 | cXMLHttpRequest.onreadystatechange.apply(oRequest); 477 | } 478 | 479 | 480 | // Fake event 481 | oRequest.dispatchEvent({ 482 | 'type': "readystatechange", 483 | 'bubbles': false, 484 | 'cancelable': false, 485 | 'timeStamp': new Date().getTime() 486 | }); 487 | } 488 | 489 | function fGetDocument(oRequest) { 490 | var oDocument = oRequest.responseXML; 491 | var sResponse = oRequest.responseText; 492 | // Try parsing responseText 493 | if (bIE && sResponse && oDocument && !oDocument.documentElement && oRequest.getResponseHeader("Content-Type").match(/[^\/]+\/[^\+]+\+xml/)) { 494 | oDocument = new window.ActiveXObject("Microsoft.XMLDOM"); 495 | oDocument.async = false; 496 | oDocument.validateOnParse = false; 497 | oDocument.loadXML(sResponse); 498 | } 499 | 500 | // Check if there is no error in document 501 | if (oDocument){ 502 | if ((bIE && oDocument.parseError != 0) || !oDocument.documentElement || (oDocument.documentElement && oDocument.documentElement.tagName == "parsererror")) { 503 | return null; 504 | } 505 | } 506 | return oDocument; 507 | } 508 | 509 | function fSynchronizeValues(oRequest) { 510 | try { oRequest.responseText = oRequest._object.responseText; } catch (e) {} 511 | try { oRequest.responseXML = fGetDocument(oRequest._object); } catch (e) {} 512 | try { oRequest.status = oRequest._object.status; } catch (e) {} 513 | try { oRequest.statusText = oRequest._object.statusText; } catch (e) {} 514 | } 515 | 516 | function fCleanTransport(oRequest) { 517 | // BUGFIX: IE - memory leak (on-page leak) 518 | oRequest._object.onreadystatechange = new window.Function; 519 | } 520 | 521 | // Internet Explorer 5.0 (missing apply) 522 | if (!window.Function.prototype.apply) { 523 | window.Function.prototype.apply = function(oRequest, oArguments) { 524 | if (!oArguments) { 525 | oArguments = []; 526 | } 527 | oRequest.__func = this; 528 | oRequest.__func(oArguments[0], oArguments[1], oArguments[2], oArguments[3], oArguments[4]); 529 | delete oRequest.__func; 530 | }; 531 | } 532 | 533 | // Register new object with window 534 | window.XMLHttpRequest = cXMLHttpRequest; 535 | 536 | })(); 537 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmlhttprequest", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/ilinsky/xmlhttprequest", 5 | "authors": [ 6 | "Sergey Ilinsky