/**
* This class manages a pending Ajax request. Instances of this type are created by the
* `{@link Ext.data.Connection#request}` method.
* @since 6.0.0
*/
Ext.define('Ext.data.request.Ajax', {
extend: 'Ext.data.request.Base',
alias: 'request.ajax',
requires: [
'Ext.data.flash.BinaryXhr'
],
statics: {
/**
* Checks if the response status was successful
* @param {Number} status The status code
* @param {Object} response The Response object
* @return {Object} An object containing success/status state
* @private
*/
parseStatus: function(status, response) {
var len;
if (response) {
//We have to account for binary response type
if (response.responseType === 'arraybuffer') {
len = response.byteLength;
} else if (response.responseText) {
len = response.responseText.length;
}
}
// see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223
status = status == 1223 ? 204 : status;
var success = (status >= 200 && status < 300) || status == 304 || (status == 0 && Ext.isNumber(len)),
isException = false;
if (!success) {
switch (status) {
case 12002:
case 12029:
case 12030:
case 12031:
case 12152:
case 13030:
isException = true;
break;
}
}
return {
success: success,
isException: isException
};
}
},
start: function(data) {
var me = this,
options = me.options,
requestOptions = me.requestOptions,
isXdr = me.isXdr,
xhr, headers;
xhr = me.xhr = me.openRequest(options, requestOptions, me.async, me.username, me.password);
// XDR doesn't support setting any headers
if (!isXdr) {
headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
}
if (me.async) {
if (!isXdr) {
xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me);
}
}
if (isXdr) {
me.processXdrRequest(me, xhr);
}
// Parent will set the timeout if needed
me.callParent([data]);
// start the request!
xhr.send(data);
if (!me.async) {
return me.onComplete();
}
return me;
},
/**
* Aborts an active request.
*/
abort: function(force) {
var me = this,
xhr = me.xhr;
if (force || me.isLoading()) {
/*
* Clear out the onreadystatechange here, this allows us
* greater control, the browser may/may not fire the function
* depending on a series of conditions.
*/
try {
xhr.onreadystatechange = null;
}
catch (e) {
// Setting onreadystatechange to null can cause problems in IE, see
// http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_a_1.html
xhr.onreadystatechange = Ext.emptyFn;
}
xhr.abort();
me.callParent([force]);
me.onComplete();
me.cleanup();
}
},
/**
* Cleans up any left over information from the request
*/
cleanup: function() {
this.xhr = null;
delete this.xhr;
},
isLoading: function() {
var me = this,
xhr = me.xhr,
state = xhr && xhr.readyState,
C = Ext.data.flash && Ext.data.flash.BinaryXhr;
if (!xhr || me.aborted || me.timedout) {
return false;
}
// if there is a connection and readyState is not 0 or 4, or in case of
// BinaryXHR, not 4
if (C && xhr instanceof C) {
return state !== 4;
}
return state !== 0 && state !== 4;
},
/**
* Creates and opens an appropriate XHR transport for a given request on this browser.
* This logic is contained in an individual method to allow for overrides to process all
* of the parameters and options and return a suitable, open connection.
* @private
*/
openRequest: function(options, requestOptions, async, username, password) {
var me = this,
xhr = me.newRequest(options);
if (username) {
xhr.open(requestOptions.method, requestOptions.url, async, username, password);
}
else {
if (me.isXdr) {
xhr.open(requestOptions.method, requestOptions.url);
}
else {
xhr.open(requestOptions.method, requestOptions.url, async);
}
}
if (options.binary || me.binary) {
if (window.Uint8Array) {
xhr.responseType = 'arraybuffer';
}
else if (xhr.overrideMimeType) {
// In some older non-IE browsers, e.g. ff 3.6, that do not
// support Uint8Array, a mime type override is required so that
// the unprocessed binary data can be read from the responseText
// (see createResponse())
xhr.overrideMimeType('text\/plain; charset=x-user-defined');
//<debug>
}
else if (!Ext.isIE) {
Ext.log.warn("Your browser does not support loading binary data using Ajax.");
//</debug>
}
}
if (options.withCredentials || me.withCredentials) {
xhr.withCredentials = true;
}
return xhr;
},
/**
* Creates the appropriate XHR transport for a given request on this browser. On IE
* this may be an `XDomainRequest` rather than an `XMLHttpRequest`.
* @private
*/
newRequest: function(options) {
var me = this,
xhr;
if (options.binaryData) {
// This is a binary data request. Handle submission differently for differnet browsers
if (window.Uint8Array) {
// On browsers that support this, use the native XHR object
xhr = me.getXhrInstance();
}
else {
// catch all for all other browser types
xhr = new Ext.data.flash.BinaryXhr();
}
}
else if (me.cors && Ext.isIE9m) {
xhr = me.getXdrInstance();
me.isXdr = true;
}
else {
xhr = me.getXhrInstance();
me.isXdr = false;
}
return xhr;
},
* Setup all the headers for the request
* @private
* @param {Object} xhr The xhr object
* @param {Object} options The options for the request
* @param {Object} data The data for the request
* @param {Object} params The params for the request
*/
setupHeaders: function(xhr, options, data, params) {
var me = this,
headers = Ext.apply({}, options.headers || {}, me.defaultHeaders),
contentType = me.defaultPostHeader,
jsonData = options.jsonData,
xmlData = options.xmlData,
type = 'Content-Type',
useHeader = me.useDefaultXhrHeader,
key, header;
if (!headers.hasOwnProperty(type) && (data || params)) {
if (data) {
if (options.rawData) {
contentType = 'text/plain';
}
else {
if (xmlData && Ext.isDefined(xmlData)) {
contentType = 'text/xml';
}
else if (jsonData && Ext.isDefined(jsonData)) {
contentType = 'application/json';
}
}
}
headers[type] = contentType;
}
if (useHeader && !headers['X-Requested-With']) {
headers['X-Requested-With'] = me.defaultXhrHeader;
}
// If undefined/null, remove it and don't set the header.
// Allow the browser to do so.
if (headers[type] === undefined || headers[type] === null) {
delete headers[type];
}
// set up all the request headers on the xhr object
try {
for (key in headers) {
if (headers.hasOwnProperty(key)) {
header = headers[key];
xhr.setRequestHeader(key, header);
}
}
}
catch(e) {
// TODO Request shouldn't fire events from its owner
me.owner.fireEvent('exception', key, header);
}
return headers;
},
/**
* Creates the appropriate XDR transport for this browser.
* - IE 7 and below don't support CORS
* - IE 8 and 9 support CORS with native XDomainRequest object
* - IE 10 (and above?) supports CORS with native XMLHttpRequest object
* @private
*/
getXdrInstance: function() {
var xdr;
if (Ext.ieVersion >= 8) {
xdr = new XDomainRequest();
}
else {
Ext.raise({
msg: 'Your browser does not support CORS'
});
}
return xdr;
},
/**
* Creates the appropriate XHR transport for this browser.
* @private
*/
getXhrInstance: (function() {
var options = [function() {
return new XMLHttpRequest();
}, function() {
return new ActiveXObject('MSXML2.XMLHTTP.3.0'); // jshint ignore:line
}, function() {
return new ActiveXObject('MSXML2.XMLHTTP'); // jshint ignore:line
}, function() {
return new ActiveXObject('Microsoft.XMLHTTP'); // jshint ignore:line
}], i = 0,
len = options.length,
xhr;
for (; i < len; ++i) {
try {
xhr = options[i];
xhr();
break;
} catch(e) {
}
}
return xhr;
}()),
processXdrRequest: function(request, xhr) {
var me = this;
// Mutate the request object as per XDR spec.
delete request.headers;
request.contentType = request.options.contentType || me.defaultXdrContentType;
xhr.onload = Ext.Function.bind(me.onStateChange, me, [true]);
xhr.onerror = xhr.ontimeout = Ext.Function.bind(me.onStateChange, me, [false]);
},
processXdrResponse: function(response, xhr) {
// Mutate the response object as per XDR spec.
response.getAllResponseHeaders = function() {
return [];
};
response.getResponseHeader = function() {
return '';
};
response.contentType = xhr.contentType || this.defaultXdrContentType;
},
onStateChange: function(xdrResult) {
var me = this,
xhr = me.xhr,
globalEvents = Ext.GlobalEvents;
// Using CORS with IE doesn't support readyState so we fake it.
if ((xhr && xhr.readyState == 4) || me.isXdr) {
me.clearTimer();
me.onComplete(xdrResult);
me.cleanup();
if (globalEvents.hasListeners.idle) {
globalEvents.fireEvent('idle');
}
}
},
/**
* To be called when the request has come back from the server
* @param {Object} request
* @return {Object} The response
* @private
*/
onComplete: function(xdrResult) {
var me = this,
owner = me.owner,
options = me.options,
xhr = me.xhr,
failure = { success: false, isException: false },
result, success, response;
if (!xhr || me.destroyed) {
return me.result = failure;
}
try {
result = Ext.data.request.Ajax.parseStatus(xhr.status, xhr);
if (result.success) {
// This is quite difficult to reproduce, however if we abort a request
// just before it returns from the server, occasionally the status will be
// returned correctly but the request is still yet to be complete.
result.success = xhr.readyState === 4;
}
}
catch (e) {
// In some browsers we can't access the status if the readyState is not 4,
// so the request has failed
result = failure;
}
success = me.success = me.isXdr ? xdrResult : result.success;
if (success) {
response = me.createResponse(xhr);
if (owner.hasListeners.requestcomplete) {
owner.fireEvent('requestcomplete', owner, response, options);
}
if (options.success) {
Ext.callback(options.success, options.scope, [response, options]);
}
}
else {
if (result.isException || me.aborted || me.timedout) {
response = me.createException(xhr);
}
else {
response = me.createResponse(xhr);
}
if (owner.hasListeners.requestexception) {
owner.fireEvent('requestexception', owner, response, options);
}
if (options.failure) {
Ext.callback(options.failure, options.scope, [response, options]);
}
}
me.result = response;
if (options.callback) {
Ext.callback(options.callback, options.scope, [options, success, response]);
}
owner.onRequestComplete(me);
me.callParent([xdrResult]);
return response;
},
/**
* Creates the response object
* @param {Object} request
* @private
*/
createResponse: function(xhr) {
var me = this,
isXdr = me.isXdr,
headers = {},
lines = isXdr ? [] : xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n'),
count = lines.length,
line, index, key, response, byteArray;
while (count--) {
line = lines[count];
index = line.indexOf(':');
if (index >= 0) {
key = line.substr(0, index).toLowerCase();
if (line.charAt(index + 1) == ' ') {
++index;
}
headers[key] = line.substr(index + 1);
}
}
response = {
request: me,
requestId: me.id,
status: xhr.status,
statusText: xhr.statusText,
getResponseHeader: function(header) {
return headers[header.toLowerCase()];
},
getAllResponseHeaders: function() {
return headers;
}
};
if (isXdr) {
me.processXdrResponse(response, xhr);
}
if (me.binary) {
response.responseBytes = me.getByteArray(xhr);
}
else {
// an error is thrown when trying to access responseText or responseXML
// on an xhr object with responseType of 'arraybuffer', so only attempt
// to set these properties in the response if we're not dealing with
// binary data
response.responseText = xhr.responseText;
response.responseXML = xhr.responseXML;
}
return response;
},
destroy: function() {
this.xhr = null;
this.callParent();
},
privates: {
/**
* Gets binary data from the xhr response object and returns it as a byte array
* @param {Object} xhr the xhr response object
* @return {Uint8Array/Array}
* @private
*/
getByteArray: function(xhr) {
var response = xhr.response,
responseBody = xhr.responseBody,
Cls = Ext.data.flash && Ext.data.flash.BinaryXhr,
byteArray, responseText, len, i;
if (xhr instanceof Cls) {
// If this was a BinaryXHR request via flash, we already have the bytes ready
byteArray = xhr.responseBytes;
}
else if (window.Uint8Array) {
// Modern browsers (including IE10) have a native byte array
// which can be created by passing the ArrayBuffer (returned as
// the xhr.response property) to the Uint8Array constructor.
byteArray = response ? new Uint8Array(response) : [];
}
else if (Ext.isIE9p) {
// In IE9 and below the responseBody property contains a byte array
// but it is not directly accessible using javascript.
// In IE9p we can get the bytes by constructing a VBArray
// using the responseBody and then converting it to an Array.
try {
byteArray = new VBArray(responseBody).toArray(); // jshint ignore:line
}
catch(e) {
// If the binary response is empty, the VBArray constructor will
// choke on the responseBody. We can't simply do a null check
// on responseBody because responseBody is always falsy when it
// contains binary data.
byteArray = [];
}
}
else if (Ext.isIE) {
// IE8 and below also have a VBArray constructor, but throw a
// "VBArray Expected" error if you try to pass the responseBody to
// the VBArray constructor.
// http://msdn.microsoft.com/en-us/library/ye3x9by3%28v=vs.71%29.aspx
// so we have to use vbscript injection to access the bytes
if (!this.self.vbScriptInjected) {
this.injectVBScript();
}
getIEByteArray(xhr.responseBody, byteArray = []); // jshint ignore:line
}
else {
// in other older browsers make a best-effort attempt to read the
// bytes from responseText
byteArray = [];
responseText = xhr.responseText;
len = responseText.length;
for (i = 0; i < len; i++) {
// Some characters have an extra byte 0xF7 in the high order
// position. Throw away the high order byte and then push the
// result onto the byteArray.
byteArray.push(responseText.charCodeAt(i) & 0xFF);
}
}
return byteArray;
},
/**
* Injects a vbscript tag containing a 'getIEByteArray' method for reading
* binary data from an xhr response in IE8 and below.
* @private
*/
injectVBScript: function() {
var scriptTag = document.createElement('script');
scriptTag.type = 'text/vbscript';
scriptTag.text = [
'Function getIEByteArray(byteArray, out)',
'Dim len, i',
'len = LenB(byteArray)',
'For i = 1 to len',
'out.push(AscB(MidB(byteArray, i, 1)))',
'Next',
'End Function'
].join('\n');
Ext.getHead().dom.appendChild(scriptTag);
this.self.vbScriptInjected = true;
}
}
});