[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 /** 2 * @requires javelin-install 3 * javelin-stratcom 4 * javelin-util 5 * javelin-behavior 6 * javelin-json 7 * javelin-dom 8 * javelin-resource 9 * javelin-routable 10 * @provides javelin-request 11 * @javelin 12 */ 13 14 /** 15 * Make basic AJAX XMLHTTPRequests. 16 */ 17 JX.install('Request', { 18 construct : function(uri, handler) { 19 this.setURI(uri); 20 if (handler) { 21 this.listen('done', handler); 22 } 23 }, 24 25 events : ['start', 'open', 'send', 'statechange', 'done', 'error', 'finally', 26 'uploadprogress'], 27 28 members : { 29 30 _xhrkey : null, 31 _transport : null, 32 _sent : false, 33 _finished : false, 34 _block : null, 35 _data : null, 36 37 _getSameOriginTransport : function() { 38 try { 39 try { 40 return new XMLHttpRequest(); 41 } catch (x) { 42 return new ActiveXObject("Msxml2.XMLHTTP"); 43 } 44 } catch (x) { 45 return new ActiveXObject("Microsoft.XMLHTTP"); 46 } 47 }, 48 49 _getCORSTransport : function() { 50 try { 51 var xport = new XMLHttpRequest(); 52 if ('withCredentials' in xport) { 53 // XHR supports CORS 54 } else if (typeof XDomainRequest != 'undefined') { 55 xport = new XDomainRequest(); 56 } 57 return xport; 58 } catch (x) { 59 return new XDomainRequest(); 60 } 61 }, 62 63 getTransport : function() { 64 if (!this._transport) { 65 this._transport = this.getCORS() ? this._getCORSTransport() : 66 this._getSameOriginTransport(); 67 } 68 return this._transport; 69 }, 70 71 getRoutable: function() { 72 var routable = new JX.Routable(); 73 routable.listen('start', JX.bind(this, function() { 74 // Pass the event to allow other listeners to "start" to configure this 75 // request before it fires. 76 JX.Stratcom.pass(JX.Stratcom.context()); 77 this.send(); 78 })); 79 this.listen('finally', JX.bind(routable, routable.done)); 80 return routable; 81 }, 82 83 send : function() { 84 if (this._sent || this._finished) { 85 if (__DEV__) { 86 if (this._sent) { 87 JX.$E( 88 'JX.Request.send(): ' + 89 'attempting to send a Request that has already been sent.'); 90 } 91 if (this._finished) { 92 JX.$E( 93 'JX.Request.send(): ' + 94 'attempting to send a Request that has finished or aborted.'); 95 } 96 } 97 return; 98 } 99 100 // Fire the "start" event before doing anything. A listener may 101 // perform pre-processing or validation on this request 102 this.invoke('start', this); 103 if (this._finished) { 104 return; 105 } 106 107 var xport = this.getTransport(); 108 xport.onreadystatechange = JX.bind(this, this._onreadystatechange); 109 if (xport.upload) { 110 xport.upload.onprogress = JX.bind(this, this._onuploadprogress); 111 } 112 113 var method = this.getMethod().toUpperCase(); 114 115 if (__DEV__) { 116 if (this.getRawData()) { 117 if (method != 'POST') { 118 JX.$E( 119 'JX.Request.send(): ' + 120 'attempting to send post data over GET. You must use POST.'); 121 } 122 } 123 } 124 125 var list_of_pairs = this._data || []; 126 list_of_pairs.push(['__ajax__', true]); 127 128 this._block = JX.Stratcom.allocateMetadataBlock(); 129 list_of_pairs.push(['__metablock__', this._block]); 130 131 var q = (this.getDataSerializer() || 132 JX.Request.defaultDataSerializer)(list_of_pairs); 133 var uri = this.getURI(); 134 135 // If we're sending a file, submit the metadata via the URI instead of 136 // via the request body, because the entire post body will be consumed by 137 // the file content. 138 if (method == 'GET' || this.getRawData()) { 139 uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q; 140 } 141 142 if (this.getTimeout()) { 143 this._timer = setTimeout( 144 JX.bind( 145 this, 146 this._fail, 147 JX.Request.ERROR_TIMEOUT), 148 this.getTimeout()); 149 } 150 151 xport.open(method, uri, true); 152 153 // Must happen after xport.open so that listeners can modify the transport 154 // Some transport properties can only be set after the transport is open 155 this.invoke('open', this); 156 if (this._finished) { 157 return; 158 } 159 160 this.invoke('send', this); 161 if (this._finished) { 162 return; 163 } 164 165 if (method == 'POST') { 166 if (this.getRawData()) { 167 xport.send(this.getRawData()); 168 } else { 169 xport.setRequestHeader( 170 'Content-Type', 171 'application/x-www-form-urlencoded'); 172 xport.send(q); 173 } 174 } else { 175 xport.send(null); 176 } 177 178 this._sent = true; 179 }, 180 181 abort : function() { 182 this._cleanup(); 183 }, 184 185 _onuploadprogress : function(progress) { 186 this.invoke('uploadprogress', progress); 187 }, 188 189 _onreadystatechange : function() { 190 var xport = this.getTransport(); 191 var response; 192 try { 193 this.invoke('statechange', this); 194 if (this._finished) { 195 return; 196 } 197 if (xport.readyState != 4) { 198 return; 199 } 200 // XHR requests to 'file:///' domains return 0 for success, which is why 201 // we treat it as a good result in addition to HTTP 2XX responses. 202 if (xport.status !== 0 && (xport.status < 200 || xport.status >= 300)) { 203 this._fail(); 204 return; 205 } 206 207 if (__DEV__) { 208 var expect_guard = this.getExpectCSRFGuard(); 209 210 if (!xport.responseText.length) { 211 JX.$E( 212 'JX.Request("'+this.getURI()+'", ...): '+ 213 'server returned an empty response.'); 214 } 215 if (expect_guard && xport.responseText.indexOf('for (;;);') !== 0) { 216 JX.$E( 217 'JX.Request("'+this.getURI()+'", ...): '+ 218 'server returned an invalid response.'); 219 } 220 if (expect_guard && xport.responseText == 'for (;;);') { 221 JX.$E( 222 'JX.Request("'+this.getURI()+'", ...): '+ 223 'server returned an empty response.'); 224 } 225 } 226 227 response = this._extractResponse(xport); 228 if (!response) { 229 JX.$E( 230 'JX.Request("'+this.getURI()+'", ...): '+ 231 'server returned an invalid response.'); 232 } 233 } catch (exception) { 234 235 if (__DEV__) { 236 JX.log( 237 'JX.Request("'+this.getURI()+'", ...): '+ 238 'caught exception processing response: '+exception); 239 } 240 this._fail(); 241 return; 242 } 243 244 try { 245 this._handleResponse(response); 246 this._cleanup(); 247 } catch (exception) { 248 // In Firefox+Firebug, at least, something eats these. :/ 249 setTimeout(function() { 250 throw exception; 251 }, 0); 252 } 253 }, 254 255 _extractResponse : function(xport) { 256 var text = xport.responseText; 257 258 if (this.getExpectCSRFGuard()) { 259 text = text.substring('for (;;);'.length); 260 } 261 262 var type = this.getResponseType().toUpperCase(); 263 if (type == 'TEXT') { 264 return text; 265 } else if (type == 'JSON' || type == 'JAVELIN') { 266 return JX.JSON.parse(text); 267 } else if (type == 'XML') { 268 var doc; 269 try { 270 if (typeof DOMParser != 'undefined') { 271 var parser = new DOMParser(); 272 doc = parser.parseFromString(text, "text/xml"); 273 } else { // IE 274 // an XDomainRequest 275 doc = new ActiveXObject("Microsoft.XMLDOM"); 276 doc.async = false; 277 doc.loadXML(xport.responseText); 278 } 279 280 return doc.documentElement; 281 } catch (exception) { 282 if (__DEV__) { 283 JX.log( 284 'JX.Request("'+this.getURI()+'", ...): '+ 285 'caught exception extracting response: '+exception); 286 } 287 this._fail(); 288 return null; 289 } 290 } 291 292 if (__DEV__) { 293 JX.$E( 294 'JX.Request("'+this.getURI()+'", ...): '+ 295 'unrecognized response type.'); 296 } 297 return null; 298 }, 299 300 _fail : function(error) { 301 this._cleanup(); 302 303 this.invoke('error', error, this); 304 this.invoke('finally'); 305 }, 306 307 _done : function(response) { 308 this._cleanup(); 309 310 if (response.onload) { 311 for (var ii = 0; ii < response.onload.length; ii++) { 312 (new Function(response.onload[ii]))(); 313 } 314 } 315 316 var payload; 317 if (this.getRaw()) { 318 payload = response; 319 } else { 320 payload = response.payload; 321 JX.Request._parseResponsePayload(payload); 322 } 323 324 this.invoke('done', payload, this); 325 this.invoke('finally'); 326 }, 327 328 _cleanup : function() { 329 this._finished = true; 330 clearTimeout(this._timer); 331 this._timer = null; 332 333 // Should not abort the transport request if it has already completed 334 // Otherwise, we may see an "HTTP request aborted" error in the console 335 // despite it possibly having succeeded. 336 if (this._transport && this._transport.readyState != 4) { 337 this._transport.abort(); 338 } 339 }, 340 341 setData : function(dictionary) { 342 this._data = null; 343 this.addData(dictionary); 344 return this; 345 }, 346 347 addData : function(dictionary) { 348 if (!this._data) { 349 this._data = []; 350 } 351 for (var k in dictionary) { 352 this._data.push([k, dictionary[k]]); 353 } 354 return this; 355 }, 356 357 setDataWithListOfPairs : function(list_of_pairs) { 358 this._data = list_of_pairs; 359 return this; 360 }, 361 362 _handleResponse : function(response) { 363 if (this.getResponseType().toUpperCase() == 'JAVELIN') { 364 if (response.error) { 365 this._fail(response.error); 366 } else { 367 JX.Stratcom.mergeData( 368 this._block, 369 response.javelin_metadata || {}); 370 371 var when_complete = JX.bind(this, function() { 372 this._done(response); 373 JX.initBehaviors(response.javelin_behaviors || {}); 374 }); 375 376 if (response.javelin_resources) { 377 JX.Resource.load(response.javelin_resources, when_complete); 378 } else { 379 when_complete(); 380 } 381 } 382 } else { 383 this._cleanup(); 384 this.invoke('done', response, this); 385 this.invoke('finally'); 386 } 387 } 388 }, 389 390 statics : { 391 ERROR_TIMEOUT : -9000, 392 defaultDataSerializer : function(list_of_pairs) { 393 var uri = []; 394 for (var ii = 0; ii < list_of_pairs.length; ii++) { 395 var pair = list_of_pairs[ii]; 396 var name = encodeURIComponent(pair[0]); 397 var value = encodeURIComponent(pair[1]); 398 uri.push(name + '=' + value); 399 } 400 return uri.join('&'); 401 }, 402 403 /** 404 * When we receive a JSON blob, parse it to introduce meaningful objects 405 * where there are magic keys for placeholders. 406 * 407 * Objects with the magic key '__html' are translated into JX.HTML objects. 408 * 409 * This function destructively modifies its input. 410 */ 411 _parseResponsePayload: function(parent, index) { 412 var recurse = JX.Request._parseResponsePayload; 413 var obj = (typeof index !== 'undefined') ? parent[index] : parent; 414 if (JX.isArray(obj)) { 415 for (var ii = 0; ii < obj.length; ii++) { 416 recurse(obj, ii); 417 } 418 } else if (obj && typeof obj == 'object') { 419 if (('__html' in obj) && (obj.__html !== null)) { 420 parent[index] = JX.$H(obj.__html); 421 } else { 422 for (var key in obj) { 423 recurse(obj, key); 424 } 425 } 426 } 427 } 428 }, 429 430 properties : { 431 URI : null, 432 dataSerializer : null, 433 /** 434 * Configure which HTTP method to use for the request. Permissible values 435 * are "POST" (default) or "GET". 436 * 437 * @param string HTTP method, one of "POST" or "GET". 438 */ 439 method : 'POST', 440 /** 441 * Set the data parameter of transport.send. Useful if you want to send a 442 * file or FormData. Not that you cannot send raw data and data at the same 443 * time. 444 * 445 * @param Data, argument to transport.send 446 */ 447 rawData: null, 448 raw : false, 449 450 /** 451 * Configure a timeout, in milliseconds. If the request has not resolved 452 * (either with success or with an error) within the provided timeframe, 453 * it will automatically fail with error JX.Request.ERROR_TIMEOUT. 454 * 455 * @param int Timeout, in milliseconds (e.g. 3000 = 3 seconds). 456 */ 457 timeout : null, 458 459 /** 460 * Whether or not we should expect the CSRF guard in the response. 461 * 462 * @param bool 463 */ 464 expectCSRFGuard : true, 465 466 /** 467 * Whether it should be a CORS (Cross-Origin Resource Sharing) request to 468 * a third party domain other than the current site. 469 * 470 * @param bool 471 */ 472 CORS : false, 473 474 /** 475 * Type of the response. 476 * 477 * @param enum 'JAVELIN', 'JSON', 'XML', 'TEXT' 478 */ 479 responseType : 'JAVELIN' 480 } 481 482 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |