@qooxdoo/framework
Version:
The JS Framework for Coders
1,017 lines (838 loc) • 27.4 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
2006 Derrell Lipman
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* Derrell Lipman (derrell)
************************************************************************ */
/**
* Transports requests to a server using the native XmlHttpRequest object.
*
* This class should not be used directly by client programmers.
*/
qx.Class.define("qx.io.remote.transport.XmlHttp",
{
extend : qx.io.remote.transport.Abstract,
implement: [ qx.core.IDisposable ],
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics :
{
/**
* Capabilities of this transport type.
*
* @internal
*/
handles :
{
synchronous : true,
asynchronous : true,
crossDomain : false,
fileUpload : false,
programmaticFormFields : false,
responseTypes : [ "text/plain", "text/javascript", "application/json", "application/xml", "text/html" ]
},
/**
* Return a new XMLHttpRequest object suitable for the client browser.
*
* @return {Object} native XMLHttpRequest object
* @signature function()
*/
createRequestObject : qx.core.Environment.select("engine.name",
{
"default" : function() {
return new XMLHttpRequest;
},
// IE7's native XmlHttp does not care about trusted zones. To make this
// work in the localhost scenario, you can use the following registry setting:
//
// [HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\
// FeatureControl\FEATURE_XMLHTTP_RESPECT_ZONEPOLICY]
// "Iexplore.exe"=dword:00000001
//
// Generally it seems that the ActiveXObject is more stable. jQuery
// seems to use it always. We prefer the ActiveXObject for the moment, but allow
// fallback to XMLHTTP if ActiveX is disabled.
"mshtml" : function()
{
if (window.ActiveXObject && qx.xml.Document.XMLHTTP) {
return new ActiveXObject(qx.xml.Document.XMLHTTP);
}
if (window.XMLHttpRequest) {
return new XMLHttpRequest;
}
}
}),
/**
* Whether the transport type is supported by the client.
*
* @return {Boolean} supported or not
*/
isSupported : function() {
return !!this.createRequestObject();
},
/** The timeout for Xhr requests */
__timeout: 0,
/**
* Sets the timeout for requests
* @deprecated {6.0} This method is deprecated from the start because synchronous I/O itself is deprecated
* in the W3C spec {@link https://xhr.spec.whatwg.org/} and timeouts are indicative of synchronous I/O and/or
* other server issues. However, this API is still supported by many browsers and this API is useful
* for code which has not made the transition to asynchronous I/O
*/
setTimeout: function(timeout) {
this.__timeout = timeout;
},
/**
* Returns the timeout for requests
*/
getTimeout: function() {
return this.__timeout;
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/**
* If true and the responseType property is set to "application/json", getResponseContent() will
* return a Javascript map containing the JSON contents, i. e. the result qx.lang.Json.parse().
* If false, the raw string data will be returned and the parsing must be done manually.
* This is useful for special JSON dialects / extensions which are not supported by
* qx.lang.Json.
*/
parseJson :
{
check : "Boolean",
init : true
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
/*
---------------------------------------------------------------------------
CORE METHODS
---------------------------------------------------------------------------
*/
__localRequest : false,
__lastReadyState : 0,
__request : null,
/**
* Returns the native request object
*
* @return {Object} native XmlHTTPRequest object
*/
getRequest : function()
{
if (this.__request === null)
{
this.__request = qx.io.remote.transport.XmlHttp.createRequestObject();
this.__request.onreadystatechange = qx.lang.Function.bind(this._onreadystatechange, this);
}
return this.__request;
},
/*
---------------------------------------------------------------------------
USER METHODS
---------------------------------------------------------------------------
*/
/**
* Implementation for sending the request
*
*/
send : function()
{
this.__lastReadyState = 0;
var vRequest = this.getRequest();
var vMethod = this.getMethod();
var vAsynchronous = this.getAsynchronous();
var vUrl = this.getUrl();
// --------------------------------------
// Local handling
// --------------------------------------
var vLocalRequest = (window.location.protocol === "file:" && !(/^http(s){0,1}\:/.test(vUrl)));
this.__localRequest = vLocalRequest;
// --------------------------------------
// Adding URL parameters
// --------------------------------------
var vParameters = this.getParameters(false);
var vParametersList = [];
for (var vId in vParameters)
{
var value = vParameters[vId];
if (value instanceof Array)
{
for (var i=0; i<value.length; i++) {
vParametersList.push(encodeURIComponent(vId) + "=" + encodeURIComponent(value[i]));
}
}
else
{
vParametersList.push(encodeURIComponent(vId) + "=" + encodeURIComponent(value));
}
}
if (vParametersList.length > 0) {
vUrl += (vUrl.indexOf("?") >= 0 ? "&" : "?") + vParametersList.join("&");
}
// --------------------------------------------------------
// Adding data parameters (if no data is already present)
// --------------------------------------------------------
if (this.getData() === null)
{
var vParameters = this.getParameters(true);
var vParametersList = [];
for (var vId in vParameters)
{
var value = vParameters[vId];
if (value instanceof Array)
{
for (var i=0; i<value.length; i++)
{
vParametersList.push(encodeURIComponent(vId) +
"=" +
encodeURIComponent(value[i]));
}
}
else
{
vParametersList.push(encodeURIComponent(vId) +
"=" +
encodeURIComponent(value));
}
}
if (vParametersList.length > 0)
{
this.setData(vParametersList.join("&"));
}
}
var encode64 = function(input)
{
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
do
{
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output += keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
}
while (i < input.length);
return output;
};
// --------------------------------------
// Opening connection
// --------------------------------------
try
{
if (this.getUsername())
{
if (this.getUseBasicHttpAuth())
{
vRequest.open(vMethod, vUrl, vAsynchronous);
vRequest.setRequestHeader('Authorization', 'Basic ' + encode64(this.getUsername() + ':' + this.getPassword()));
}
else
{
vRequest.open(vMethod, vUrl, vAsynchronous, this.getUsername(), this.getPassword());
}
}
else
{
vRequest.open(vMethod, vUrl, vAsynchronous);
}
}
catch(ex)
{
this.error("Failed with exception: " + ex);
this.failed();
return;
}
// Apply timeout
var timeout = qx.io.remote.transport.XmlHttp.getTimeout();
if (timeout && vAsynchronous) {
vRequest.timeout = timeout;
}
// --------------------------------------
// Applying request header
// --------------------------------------
// Removed adding a referer header as this is not allowed anymore on most
// browsers
// See issue https://github.com/qooxdoo/qooxdoo/issues/9298
var vRequestHeaders = this.getRequestHeaders();
for (var vId in vRequestHeaders) {
vRequest.setRequestHeader(vId, vRequestHeaders[vId]);
}
// --------------------------------------
// Sending data
// --------------------------------------
try {
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote.data"))
{
this.debug("Request: " + this.getData());
}
}
// IE9 executes the call synchronous when the call is to file protocol
// See [BUG #4762] for details
if (
vLocalRequest && vAsynchronous &&
qx.core.Environment.get("engine.name") == "mshtml" &&
(qx.core.Environment.get("engine.version") == 9 &&
qx.core.Environment.get("browser.documentmode") == 9)
) {
qx.event.Timer.once(function() {
vRequest.send(this.getData());
}, this, 0);
} else {
vRequest.send(this.getData());
}
}
catch(ex)
{
if (vLocalRequest) {
this.failedLocally();
}
else
{
this.error("Failed to send data to URL '" + vUrl + "': " + ex, "send");
this.failed();
}
return;
}
// --------------------------------------
// Readystate for sync requests
// --------------------------------------
if (!vAsynchronous) {
this._onreadystatechange();
}
},
/**
* Force the transport into the failed state ("failed").
*
* This method should be used only if the requests URI was local
* access. I.e. it started with "file://".
*
*/
failedLocally : function()
{
if (this.getState() === "failed") {
return;
}
// should only occur on "file://" access
this.warn("Could not load from file: " + this.getUrl());
this.failed();
},
/*
---------------------------------------------------------------------------
EVENT HANDLER
---------------------------------------------------------------------------
*/
/**
* Listener method for change of the "readystate".
* Sets the internal state and informs the transport layer.
*
* @signature function(e)
* @param e {Event} native event
*/
_onreadystatechange : qx.event.GlobalError.observeMethod(function(e)
{
// Ignoring already stopped requests
switch(this.getState())
{
case "completed":
case "aborted":
case "failed":
case "timeout":
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote")) {
this.warn("Ignore Ready State Change");
}
}
return;
}
// Checking status code
var vReadyState = this.getReadyState();
if (vReadyState == 4)
{
// The status code is only meaningful when we reach ready state 4.
// (Important for Opera since it goes through other states before
// reaching 4, and the status code is not valid before 4 is reached.)
if (!qx.io.remote.Exchange.wasSuccessful(this.getStatusCode(), vReadyState, this.__localRequest)) {
// Fix for bug #2272
// The IE doesn't set the state to 'sending' even though the send method
// is called. This only occurs if the server (which is called) goes
// down or a network failure occurs.
if (this.getState() === "configured") {
this.setState("sending");
}
this.failed();
return;
}
}
// Sometimes the xhr call skips the send state
if (vReadyState == 3 && this.__lastReadyState == 1) {
this.setState(qx.io.remote.Exchange._nativeMap[++this.__lastReadyState]);
}
// Updating internal state
while (this.__lastReadyState < vReadyState) {
this.setState(qx.io.remote.Exchange._nativeMap[++this.__lastReadyState]);
}
}),
/*
---------------------------------------------------------------------------
READY STATE
---------------------------------------------------------------------------
*/
/**
* Get the ready state of this transports request.
*
* For qx.io.remote.transport.XmlHttp, ready state is a number between 1 to 4.
*
* @return {Integer} ready state number
*/
getReadyState : function()
{
var vReadyState = null;
try {
vReadyState = this.getRequest().readyState;
} catch(ex) {}
return vReadyState;
},
/*
---------------------------------------------------------------------------
REQUEST HEADER SUPPORT
---------------------------------------------------------------------------
*/
/**
* Set a request header to this transports request.
*
* @param vLabel {String} Request header name
* @param vValue {var} Request header value
*/
setRequestHeader : function(vLabel, vValue) {
this.getRequestHeaders()[vLabel] = vValue;
},
/*
---------------------------------------------------------------------------
RESPONSE HEADER SUPPORT
---------------------------------------------------------------------------
*/
/**
* Returns a specific header provided by the server upon sending a request,
* with header name determined by the argument headerName.
*
* Only available at readyState 3 and 4 universally and in readyState 2
* in Gecko.
*
* Please note: Some servers/proxies (such as Selenium RC) will capitalize
* response header names. This is in accordance with RFC 2616[1], which
* states that HTTP 1.1 header names are case-insensitive, so your
* application should be case-agnostic when dealing with response headers.
*
* [1]<a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">RFC 2616: HTTP Message Headers</a>
*
* @param vLabel {String} Response header name
* @return {String|null} Response header value
*/
getResponseHeader : function(vLabel)
{
var vResponseHeader = null;
try {
vResponseHeader = this.getRequest().getResponseHeader(vLabel) || null;
} catch(ex) {}
return vResponseHeader;
},
/**
* Returns all response headers of the request.
*
* @return {var} response headers
*/
getStringResponseHeaders : function()
{
var vSourceHeader = null;
try
{
var vLoadHeader = this.getRequest().getAllResponseHeaders();
if (vLoadHeader) {
vSourceHeader = vLoadHeader;
}
}
catch(ex) {}
return vSourceHeader;
},
/**
* Provides a hash of all response headers.
*
* @return {var} hash of all response headers
*/
getResponseHeaders : function()
{
var vSourceHeader = this.getStringResponseHeaders();
var vHeader = {};
if (vSourceHeader)
{
var vValues = vSourceHeader.split(/[\r\n]+/g);
for (var i=0, l=vValues.length; i<l; i++)
{
var vPair = vValues[i].match(/^([^:]+)\s*:\s*(.+)$/i);
if (vPair) {
vHeader[vPair[1]] = vPair[2];
}
}
}
return vHeader;
},
/*
---------------------------------------------------------------------------
STATUS SUPPORT
---------------------------------------------------------------------------
*/
/**
* Returns the current status code of the request if available or -1 if not.
*
* @return {Integer} current status code
*/
getStatusCode : function()
{
var vStatusCode = -1;
try {
vStatusCode = this.getRequest().status;
// [BUG #4476]
// IE sometimes tells 1223 when it should be 204
if (vStatusCode === 1223) {
vStatusCode = 204;
}
} catch(ex) {}
return vStatusCode;
},
/**
* Provides the status text for the current request if available and null
* otherwise.
*
* @return {String} current status code text
*/
getStatusText : function()
{
var vStatusText = "";
try {
vStatusText = this.getRequest().statusText;
} catch(ex) {}
return vStatusText;
},
/*
---------------------------------------------------------------------------
RESPONSE DATA SUPPORT
---------------------------------------------------------------------------
*/
/**
* Provides the response text from the request when available and null
* otherwise. By passing true as the "partial" parameter of this method,
* incomplete data will be made available to the caller.
*
* @return {String} Content of the response as string
*/
getResponseText : function()
{
var vResponseText = null;
try
{
vResponseText = this.getRequest().responseText;
}
catch(ex)
{
vResponseText = null;
}
return vResponseText;
},
/**
* Provides the XML provided by the response if any and null otherwise. By
* passing true as the "partial" parameter of this method, incomplete data will
* be made available to the caller.
*
* @return {String} Content of the response as XML
* @throws {Error} If an error within the response occurs.
*/
getResponseXml : function()
{
var vResponseXML = null;
var vStatus = this.getStatusCode();
var vReadyState = this.getReadyState();
if (qx.io.remote.Exchange.wasSuccessful(vStatus, vReadyState, this.__localRequest))
{
try {
vResponseXML = this.getRequest().responseXML;
} catch(ex) {}
}
// Typical behaviour on file:// on mshtml
// Could we check this with something like: /^file\:/.test(path); ?
// No browser check here, because it doesn't seem to break other browsers
// * test for this.req.responseXML's objecthood added by *
// * FRM, 20050816 *
if (typeof vResponseXML == "object" && vResponseXML != null)
{
if (!vResponseXML.documentElement)
{
// Clear xml file declaration, this breaks non unicode files (like ones with Umlauts)
var s = String(this.getRequest().responseText).replace(/<\?xml[^\?]*\?>/, "");
vResponseXML.loadXML(s);
}
// Re-check if fixed...
if (!vResponseXML.documentElement) {
throw new Error("Missing Document Element!");
}
if (vResponseXML.documentElement.tagName == "parseerror") {
throw new Error("XML-File is not well-formed!");
}
}
else
{
throw new Error("Response was not a valid xml document [" + this.getRequest().responseText + "]");
}
return vResponseXML;
},
/**
* Returns the length of the content as fetched thus far
*
* @return {Integer} Length of the response text.
*/
getFetchedLength : function()
{
var vText = this.getResponseText();
return typeof vText == "string" ? vText.length : 0;
},
/**
* Returns the content of the response
*
* @return {null | String} Response content if available
*/
getResponseContent : function()
{
var state = this.getState();
if (state !== "completed" && state != "failed")
{
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote")) {
this.warn("Transfer not complete or failed, ignoring content!");
}
}
return null;
}
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote")) {
this.debug("Returning content for responseType: " + this.getResponseType());
}
}
var vText = this.getResponseText();
if (state == "failed")
{
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote.data"))
{
this.debug("Failed: " + vText);
}
}
return vText;
}
switch(this.getResponseType())
{
case "text/plain":
case "text/html":
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote.data"))
{
this.debug("Response: " + vText);
}
}
return vText;
case "application/json":
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote.data"))
{
this.debug("Response: " + vText);
}
}
try {
if (vText && vText.length > 0)
{
var ret;
if (this.getParseJson()){
ret = qx.lang.Json.parse(vText);
ret = (ret === 0 ? 0 : (ret || null));
} else {
ret = vText;
}
return ret;
}
else
{
return null;
}
}
catch(ex)
{
this.error("Could not execute json: [" + vText + "]", ex);
return "<pre>Could not execute json: \n" + vText + "\n</pre>";
}
case "text/javascript":
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote.data"))
{
this.debug("Response: " + vText);
}
}
try {
if(vText && vText.length > 0)
{
var ret = window.eval(vText);
return (ret === 0 ? 0 : (ret || null));
}
else
{
return null;
}
} catch(ex) {
this.error("Could not execute javascript: [" + vText + "]", ex);
return null;
}
case "application/xml":
vText = this.getResponseXml();
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote.data"))
{
this.debug("Response: " + vText);
}
}
return (vText === 0 ? 0 : (vText || null));
default:
this.warn("No valid responseType specified (" + this.getResponseType() + ")!");
return null;
}
},
/*
---------------------------------------------------------------------------
APPLY ROUTINES
---------------------------------------------------------------------------
*/
/**
* Apply method for the "state" property.
* Fires an event for each state value to inform the listeners.
*
* @param value {var} Current value
* @param old {var} Previous value
*/
_applyState : function(value, old)
{
if (qx.core.Environment.get("qx.debug"))
{
if (qx.core.Environment.get("qx.debug.io.remote")) {
this.debug("State: " + value);
}
}
switch(value)
{
case "created":
this.fireEvent("created");
break;
case "configured":
this.fireEvent("configured");
break;
case "sending":
this.fireEvent("sending");
break;
case "receiving":
this.fireEvent("receiving");
break;
case "completed":
this.fireEvent("completed");
break;
case "failed":
this.fireEvent("failed");
break;
case "aborted":
this.getRequest().abort();
this.fireEvent("aborted");
break;
case "timeout":
this.getRequest().abort();
this.fireEvent("timeout");
break;
}
}
},
/*
*****************************************************************************
DEFER
*****************************************************************************
*/
defer : function()
{
// basic registration to qx.io.remote.Exchange
// the real availability check (activeX stuff and so on) follows at the first real request
qx.io.remote.Exchange.registerType(qx.io.remote.transport.XmlHttp, "qx.io.remote.transport.XmlHttp");
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
var vRequest = this.getRequest();
if (vRequest)
{
// Clean up state change handler
// Note that for IE the proper way to do this is to set it to a
// dummy function, not null (Google on "onreadystatechange dummy IE unhook")
// http://groups.google.com/group/Google-Web-Toolkit-Contributors/browse_thread/thread/7e7ee67c191a6324
vRequest.onreadystatechange = (function() {});
// Aborting
switch(vRequest.readyState)
{
case 1:
case 2:
case 3:
vRequest.abort();
}
}
this.__request = null;
}
});