Changeset View
Changeset View
Standalone View
Standalone View
externals/javelin/src/lib/Request.js
- This file was added.
| /** | |||||
| * @requires javelin-install | |||||
| * javelin-stratcom | |||||
| * javelin-util | |||||
| * javelin-behavior | |||||
| * javelin-json | |||||
| * javelin-dom | |||||
| * javelin-resource | |||||
| * @provides javelin-request | |||||
| * @javelin | |||||
| */ | |||||
| /** | |||||
| * Make basic AJAX XMLHTTPRequests. | |||||
| * | |||||
| * @group workflow | |||||
| */ | |||||
| JX.install('Request', { | |||||
| construct : function(uri, handler) { | |||||
| this.setURI(uri); | |||||
| if (handler) { | |||||
| this.listen('done', handler); | |||||
| } | |||||
| }, | |||||
| events : ['start', 'open', 'send', 'statechange', 'done', 'error', 'finally', | |||||
| 'uploadprogress'], | |||||
| members : { | |||||
| _xhrkey : null, | |||||
| _transport : null, | |||||
| _sent : false, | |||||
| _finished : false, | |||||
| _block : null, | |||||
| _data : null, | |||||
| _getSameOriginTransport : function() { | |||||
| try { | |||||
| try { | |||||
| return new XMLHttpRequest(); | |||||
| } catch (x) { | |||||
| return new ActiveXObject("Msxml2.XMLHTTP"); | |||||
| } | |||||
| } catch (x) { | |||||
| return new ActiveXObject("Microsoft.XMLHTTP"); | |||||
| } | |||||
| }, | |||||
| _getCORSTransport : function() { | |||||
| try { | |||||
| var xport = new XMLHttpRequest(); | |||||
| if ('withCredentials' in xport) { | |||||
| // XHR supports CORS | |||||
| } else if (typeof XDomainRequest != 'undefined') { | |||||
| xport = new XDomainRequest(); | |||||
| } | |||||
| return xport; | |||||
| } catch (x) { | |||||
| return new XDomainRequest(); | |||||
| } | |||||
| }, | |||||
| getTransport : function() { | |||||
| if (!this._transport) { | |||||
| this._transport = this.getCORS() ? this._getCORSTransport() : | |||||
| this._getSameOriginTransport(); | |||||
| } | |||||
| return this._transport; | |||||
| }, | |||||
| send : function() { | |||||
| if (this._sent || this._finished) { | |||||
| if (__DEV__) { | |||||
| if (this._sent) { | |||||
| JX.$E( | |||||
| 'JX.Request.send(): ' + | |||||
| 'attempting to send a Request that has already been sent.'); | |||||
| } | |||||
| if (this._finished) { | |||||
| JX.$E( | |||||
| 'JX.Request.send(): ' + | |||||
| 'attempting to send a Request that has finished or aborted.'); | |||||
| } | |||||
| } | |||||
| return; | |||||
| } | |||||
| // Fire the "start" event before doing anything. A listener may | |||||
| // perform pre-processing or validation on this request | |||||
| this.invoke('start', this); | |||||
| if (this._finished) { | |||||
| return; | |||||
| } | |||||
| var xport = this.getTransport(); | |||||
| xport.onreadystatechange = JX.bind(this, this._onreadystatechange); | |||||
| if (xport.upload) { | |||||
| xport.upload.onprogress = JX.bind(this, this._onuploadprogress); | |||||
| } | |||||
| var method = this.getMethod().toUpperCase(); | |||||
| if (__DEV__) { | |||||
| if (this.getRawData()) { | |||||
| if (method != 'POST') { | |||||
| JX.$E( | |||||
| 'JX.Request.send(): ' + | |||||
| 'attempting to send post data over GET. You must use POST.'); | |||||
| } | |||||
| } | |||||
| } | |||||
| var list_of_pairs = this._data || []; | |||||
| list_of_pairs.push(['__ajax__', true]); | |||||
| this._block = JX.Stratcom.allocateMetadataBlock(); | |||||
| list_of_pairs.push(['__metablock__', this._block]); | |||||
| var q = (this.getDataSerializer() || | |||||
| JX.Request.defaultDataSerializer)(list_of_pairs); | |||||
| var uri = this.getURI(); | |||||
| // If we're sending a file, submit the metadata via the URI instead of | |||||
| // via the request body, because the entire post body will be consumed by | |||||
| // the file content. | |||||
| if (method == 'GET' || this.getRawData()) { | |||||
| uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q; | |||||
| } | |||||
| if (this.getTimeout()) { | |||||
| this._timer = setTimeout( | |||||
| JX.bind( | |||||
| this, | |||||
| this._fail, | |||||
| JX.Request.ERROR_TIMEOUT), | |||||
| this.getTimeout()); | |||||
| } | |||||
| xport.open(method, uri, true); | |||||
| // Must happen after xport.open so that listeners can modify the transport | |||||
| // Some transport properties can only be set after the transport is open | |||||
| this.invoke('open', this); | |||||
| if (this._finished) { | |||||
| return; | |||||
| } | |||||
| this.invoke('send', this); | |||||
| if (this._finished) { | |||||
| return; | |||||
| } | |||||
| if (method == 'POST') { | |||||
| if (this.getRawData()) { | |||||
| xport.send(this.getRawData()); | |||||
| } else { | |||||
| xport.setRequestHeader( | |||||
| 'Content-Type', | |||||
| 'application/x-www-form-urlencoded'); | |||||
| xport.send(q); | |||||
| } | |||||
| } else { | |||||
| xport.send(null); | |||||
| } | |||||
| this._sent = true; | |||||
| }, | |||||
| abort : function() { | |||||
| this._cleanup(); | |||||
| }, | |||||
| _onuploadprogress : function(progress) { | |||||
| this.invoke('uploadprogress', progress); | |||||
| }, | |||||
| _onreadystatechange : function() { | |||||
| var xport = this.getTransport(); | |||||
| var response; | |||||
| try { | |||||
| this.invoke('statechange', this); | |||||
| if (this._finished) { | |||||
| return; | |||||
| } | |||||
| if (xport.readyState != 4) { | |||||
| return; | |||||
| } | |||||
| // XHR requests to 'file:///' domains return 0 for success, which is why | |||||
| // we treat it as a good result in addition to HTTP 2XX responses. | |||||
| if (xport.status !== 0 && (xport.status < 200 || xport.status >= 300)) { | |||||
| this._fail(); | |||||
| return; | |||||
| } | |||||
| if (__DEV__) { | |||||
| var expect_guard = this.getExpectCSRFGuard(); | |||||
| if (!xport.responseText.length) { | |||||
| JX.$E( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'server returned an empty response.'); | |||||
| } | |||||
| if (expect_guard && xport.responseText.indexOf('for (;;);') != 0) { | |||||
| JX.$E( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'server returned an invalid response.'); | |||||
| } | |||||
| if (expect_guard && xport.responseText == 'for (;;);') { | |||||
| JX.$E( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'server returned an empty response.'); | |||||
| } | |||||
| } | |||||
| response = this._extractResponse(xport); | |||||
| if (!response) { | |||||
| JX.$E( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'server returned an invalid response.'); | |||||
| } | |||||
| } catch (exception) { | |||||
| if (__DEV__) { | |||||
| JX.log( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'caught exception processing response: '+exception); | |||||
| } | |||||
| this._fail(); | |||||
| return; | |||||
| } | |||||
| try { | |||||
| this._handleResponse(response); | |||||
| this._cleanup(); | |||||
| } catch (exception) { | |||||
| // In Firefox+Firebug, at least, something eats these. :/ | |||||
| setTimeout(function() { | |||||
| throw exception; | |||||
| }, 0); | |||||
| } | |||||
| }, | |||||
| _extractResponse : function(xport) { | |||||
| var text = xport.responseText; | |||||
| if (this.getExpectCSRFGuard()) { | |||||
| text = text.substring('for (;;);'.length); | |||||
| } | |||||
| var type = this.getResponseType().toUpperCase(); | |||||
| if (type == 'TEXT') { | |||||
| return text; | |||||
| } else if (type == 'JSON' || type == 'JAVELIN') { | |||||
| return JX.JSON.parse(text); | |||||
| } else if (type == 'XML') { | |||||
| var doc; | |||||
| try { | |||||
| if (typeof DOMParser != 'undefined') { | |||||
| var parser = new DOMParser(); | |||||
| doc = parser.parseFromString(text, "text/xml"); | |||||
| } else { // IE | |||||
| // an XDomainRequest | |||||
| doc = new ActiveXObject("Microsoft.XMLDOM"); | |||||
| doc.async = false; | |||||
| doc.loadXML(xport.responseText); | |||||
| } | |||||
| return doc.documentElement; | |||||
| } catch (exception) { | |||||
| if (__DEV__) { | |||||
| JX.log( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'caught exception extracting response: '+exception); | |||||
| } | |||||
| this._fail(); | |||||
| return null; | |||||
| } | |||||
| } | |||||
| if (__DEV__) { | |||||
| JX.$E( | |||||
| 'JX.Request("'+this.getURI()+'", ...): '+ | |||||
| 'unrecognized response type.'); | |||||
| } | |||||
| return null; | |||||
| }, | |||||
| _fail : function(error) { | |||||
| this._cleanup(); | |||||
| this.invoke('error', error, this); | |||||
| this.invoke('finally'); | |||||
| }, | |||||
| _done : function(response) { | |||||
| this._cleanup(); | |||||
| if (response.onload) { | |||||
| for (var ii = 0; ii < response.onload.length; ii++) { | |||||
| (new Function(response.onload[ii]))(); | |||||
| } | |||||
| } | |||||
| var payload; | |||||
| if (this.getRaw()) { | |||||
| payload = response; | |||||
| } else { | |||||
| payload = response.payload; | |||||
| JX.Request._parseResponsePayload(payload); | |||||
| } | |||||
| this.invoke('done', payload, this); | |||||
| this.invoke('finally'); | |||||
| }, | |||||
| _cleanup : function() { | |||||
| this._finished = true; | |||||
| clearTimeout(this._timer); | |||||
| this._timer = null; | |||||
| // Should not abort the transport request if it has already completed | |||||
| // Otherwise, we may see an "HTTP request aborted" error in the console | |||||
| // despite it possibly having succeeded. | |||||
| if (this._transport && this._transport.readyState != 4) { | |||||
| this._transport.abort(); | |||||
| } | |||||
| }, | |||||
| setData : function(dictionary) { | |||||
| this._data = null; | |||||
| this.addData(dictionary); | |||||
| return this; | |||||
| }, | |||||
| addData : function(dictionary) { | |||||
| if (!this._data) { | |||||
| this._data = []; | |||||
| } | |||||
| for (var k in dictionary) { | |||||
| this._data.push([k, dictionary[k]]); | |||||
| } | |||||
| return this; | |||||
| }, | |||||
| setDataWithListOfPairs : function(list_of_pairs) { | |||||
| this._data = list_of_pairs; | |||||
| return this; | |||||
| }, | |||||
| _handleResponse : function(response) { | |||||
| if (this.getResponseType().toUpperCase() == 'JAVELIN') { | |||||
| if (response.error) { | |||||
| this._fail(response.error); | |||||
| } else { | |||||
| JX.Stratcom.mergeData( | |||||
| this._block, | |||||
| response.javelin_metadata || {}); | |||||
| var when_complete = JX.bind(this, function() { | |||||
| this._done(response); | |||||
| JX.initBehaviors(response.javelin_behaviors || {}); | |||||
| }); | |||||
| if (response.javelin_resources) { | |||||
| JX.Resource.load(response.javelin_resources, when_complete); | |||||
| } else { | |||||
| when_complete(); | |||||
| } | |||||
| } | |||||
| } else { | |||||
| this._cleanup(); | |||||
| this.invoke('done', response, this); | |||||
| this.invoke('finally'); | |||||
| } | |||||
| } | |||||
| }, | |||||
| statics : { | |||||
| ERROR_TIMEOUT : -9000, | |||||
| defaultDataSerializer : function(list_of_pairs) { | |||||
| var uri = []; | |||||
| for (var ii = 0; ii < list_of_pairs.length; ii++) { | |||||
| var pair = list_of_pairs[ii]; | |||||
| var name = encodeURIComponent(pair[0]); | |||||
| var value = encodeURIComponent(pair[1]); | |||||
| uri.push(name + '=' + value); | |||||
| } | |||||
| return uri.join('&'); | |||||
| }, | |||||
| /** | |||||
| * When we receive a JSON blob, parse it to introduce meaningful objects | |||||
| * where there are magic keys for placeholders. | |||||
| * | |||||
| * Objects with the magic key '__html' are translated into JX.HTML objects. | |||||
| * | |||||
| * This function destructively modifies its input. | |||||
| */ | |||||
| _parseResponsePayload: function(parent, index) { | |||||
| var recurse = JX.Request._parseResponsePayload; | |||||
| var obj = (typeof index !== 'undefined') ? parent[index] : parent; | |||||
| if (JX.isArray(obj)) { | |||||
| for (var ii = 0; ii < obj.length; ii++) { | |||||
| recurse(obj, ii); | |||||
| } | |||||
| } else if (obj && typeof obj == 'object') { | |||||
| if (obj.__html != null) { | |||||
| parent[index] = JX.$H(obj.__html); | |||||
| } else { | |||||
| for (var key in obj) { | |||||
| recurse(obj, key); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| properties : { | |||||
| URI : null, | |||||
| dataSerializer : null, | |||||
| /** | |||||
| * Configure which HTTP method to use for the request. Permissible values | |||||
| * are "POST" (default) or "GET". | |||||
| * | |||||
| * @param string HTTP method, one of "POST" or "GET". | |||||
| */ | |||||
| method : 'POST', | |||||
| /** | |||||
| * Set the data parameter of transport.send. Useful if you want to send a | |||||
| * file or FormData. Not that you cannot send raw data and data at the same | |||||
| * time. | |||||
| * | |||||
| * @param Data, argument to transport.send | |||||
| */ | |||||
| rawData: null, | |||||
| raw : false, | |||||
| /** | |||||
| * Configure a timeout, in milliseconds. If the request has not resolved | |||||
| * (either with success or with an error) within the provided timeframe, | |||||
| * it will automatically fail with error JX.Request.ERROR_TIMEOUT. | |||||
| * | |||||
| * @param int Timeout, in milliseconds (e.g. 3000 = 3 seconds). | |||||
| */ | |||||
| timeout : null, | |||||
| /** | |||||
| * Whether or not we should expect the CSRF guard in the response. | |||||
| * | |||||
| * @param bool | |||||
| */ | |||||
| expectCSRFGuard : true, | |||||
| /** | |||||
| * Whether it should be a CORS (Cross-Origin Resource Sharing) request to | |||||
| * a third party domain other than the current site. | |||||
| * | |||||
| * @param bool | |||||
| */ | |||||
| CORS : false, | |||||
| /** | |||||
| * Type of the response. | |||||
| * | |||||
| * @param enum 'JAVELIN', 'JSON', 'XML', 'TEXT' | |||||
| */ | |||||
| responseType : 'JAVELIN' | |||||
| } | |||||
| }); | |||||