| /* |
| * Copyright 2014 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| * Author: jmarantz@google.com (Joshua Marantz) |
| */ |
| |
| |
| goog.provide('mob.XhrHijack'); |
| |
| |
| |
| /** |
| * @fileoverview Hijacks the construction of XHR objects, for two reasons: |
| * |
| * 1. Provides a notification callback whenever any XHR is sent or completes |
| * on a page. |
| * |
| * 2. Provides an opportunity to alter the XHR request URL to correct a domain, |
| * which will likely be needed when using a ProxySuffix for sites that form |
| * XHR requests using absolute domains. |
| * |
| * So far we only need this for the notification callback. |
| * |
| * This JS file should be loaded ahead of any other scripts, preferably inline. |
| * Post-compile it's 1366 bytes uncompressed, 521 bytes compressed as of now. |
| * Its interface is that another JavaScript file can call |
| * window.pagespeedXhrHijackSetListener(listener) |
| * where listener is a map that has two function properties: xhrSendHook() and |
| * xhrResponseHook(http_status_code). No other external API is provided. |
| * |
| * This file will hijack all XHR requests starting immediately when it's loaded. |
| * When a listener is registered, even if it's much later in the page load, |
| * it will receive any pending callback notifications in order. |
| */ |
| |
| |
| |
| /** |
| * Object to hijack XHR XMLHttpRequests. We use this for tracking active |
| * XHR requests. |
| * @param {?XMLHttpRequest} xhr The XHR request. |
| * @constructor |
| * |
| * TODO(jmarantz): considering creating a subclass of goog.net.XhrLike (see |
| * goog.net.IeCorsXhrAdapter in |
| * http://docs.closure-library.googlecode.com/git/local_closure_goog_net_corsxmlhttpfactory.js.source.html |
| * That will at least give us type annotations to copy. |
| */ |
| mob.XhrHijack = function(xhr) { |
| this.xhr = xhr || new this.XMLHttpRequestConstructor_(); |
| this.onreadystatechange = null; |
| this.readyState = 0; |
| this.responseText = ''; |
| this.statusText = ''; |
| this.xhr.onreadystatechange = this.readyCallback.bind(this); |
| this.xhr.onload = this.onloadCallback.bind(this); |
| }; |
| |
| |
| /** |
| * Array of objects representing any send/response events that transpired |
| * prior to the listener being established. Send events are represented with |
| * an 's' and response events are represented with the numeric http status |
| * code. When a listener is established, any pending send/response events are |
| * immediately sent to it and queuedHooks is cleared. |
| * |
| * @private |
| */ |
| mob.XhrHijack.queuedEvents_ = []; |
| |
| |
| /** |
| * Object to get notified whenever XHR activity occurs. This is set globally |
| * for the class because the listener doesn't have control of when XHRs are |
| * constructed. |
| * |
| * @private |
| */ |
| mob.XhrHijack.listener_ = {}; |
| |
| |
| /** |
| * @const @private {string} |
| */ |
| mob.XhrHijack.SEND_EVENT_CHAR_ = 's'; |
| |
| |
| /** |
| * Default handler for xhrSendHook. |
| */ |
| mob.XhrHijack.listener_['xhrSendHook'] = function() { |
| mob.XhrHijack.queuedEvents_.push(mob.XhrHijack.SEND_EVENT_CHAR_); |
| }; |
| |
| |
| /** |
| * Default handler for xhrResponseHook. |
| * @param {number} http_status |
| */ |
| mob.XhrHijack.listener_['xhrResponseHook'] = function(http_status) { |
| mob.XhrHijack.queuedEvents_.push(http_status); |
| }; |
| |
| |
| /** |
| * Establishes a listener instance for XHR events. If any XHR events were |
| * initiated before the listener is established, it immediately receives |
| * the queued events. |
| * |
| * The listener must be a class instance that defines these methods: |
| * listener_.xhrSendHook() |
| * listener_.xhrResponseHook(http_status_code) |
| * |
| * @param {!Object} listener |
| * |
| * TODO(jmarantz): Use an at-interface with the above methods defined, and |
| * then use the type annotation to ensure they are defined correctly. |
| */ |
| mob.XhrHijack.setListener = function(listener) { |
| mob.XhrHijack.listener_ = listener; |
| for (var i = 0; i < mob.XhrHijack.queuedEvents_.length; ++i) { |
| var event = mob.XhrHijack.queuedEvents_[i]; |
| if (event == mob.XhrHijack.SEND_EVENT_CHAR_) { |
| mob.XhrHijack.listener_['xhrSendHook'](); |
| } else { |
| mob.XhrHijack.listener_['xhrResponseHook'](event); |
| } |
| } |
| mob.XhrHijack.queuedEvents_ = null; // Will not be used again. |
| }; |
| window['pagespeedXhrHijackSetListener'] = mob.XhrHijack.setListener; |
| |
| |
| /** |
| * Captures the native XMLHttpRequest constructor, so we can insert |
| * our own hooks to run after the normal callback runs. |
| * @private |
| */ |
| mob.XhrHijack.prototype.XMLHttpRequestConstructor_ = XMLHttpRequest; |
| |
| |
| /** |
| * Callback to run onload, transferring the responseText, and calling |
| * client onload function. |
| */ |
| mob.XhrHijack.prototype.onloadCallback = function() { |
| if (this.xhr.responseText) { |
| this.responseText = this.xhr.responseText; |
| } |
| if (this.onload) { |
| this.onload(); |
| } |
| }; |
| |
| |
| /** |
| * Wrapped onreadystatechange callback. |
| */ |
| mob.XhrHijack.prototype.readyCallback = function() { |
| // hijack more: http://www.w3.org/TR/2006/WD-XMLHttpRequest-20060405/ |
| this.readyState = this.xhr.readyState; |
| this.status = this.xhr.status; |
| this.responseText = this.xhr.responseText; |
| if (this.xhr.statusText) { |
| this.statusText = this.xhr.statusText; |
| } |
| if (this.onreadystatechange) { |
| this.onreadystatechange(); |
| } |
| if (this.readyState == 4) { |
| mob.XhrHijack.listener_['xhrResponseHook'](this.status); |
| } |
| }; |
| |
| |
| /** |
| * Wrapped abort callback. |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['abort'] = function() { |
| this.xhr.abort(); |
| }; |
| |
| |
| /** |
| * Hijacked open call. Strictly delegates for now, but this |
| * gives us an opportunity to domain-correct for proxy-suffix |
| * as needed. |
| * @param {string} a |
| * @param {string} b |
| * @param {?boolean} c |
| * @param {?string} d |
| * @param {?string} e |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['open'] = function(a, b, c, d, e) { |
| this.xhr.open(a, b, c, d, e); |
| }; |
| |
| |
| /** |
| * Hijacked send call, helping us track outstanding XHRs. |
| * @param {!ArrayBuffer|!ArrayBufferView|!Blob|!Document|!FormData|string=} |
| * opt_x |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['send'] = function(opt_x) { |
| mob.XhrHijack.listener_['xhrSendHook'](); |
| this.xhr.send(opt_x); |
| }; |
| |
| |
| /** |
| * Hijacked. |
| * @param {string} x |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['overrideMimeType'] = function(x) { |
| this.xhr.overrideMimeType(x); |
| }; |
| |
| |
| /** |
| * Hijacked. |
| * @param {string} name The name of the request header. |
| * @param {string} value The value of the requets header. |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['setRequestHeader'] = function(name, value) { |
| this.xhr.setRequestHeader(name, value); |
| }; |
| |
| |
| /** |
| * Hijacked. |
| * @return {string} |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['getAllResponseHeaders'] = function() { |
| return this.xhr.getAllResponseHeaders(); |
| }; |
| |
| |
| /** |
| * Hijacked. |
| * @param {string} name |
| * @return {string} |
| * @this {mob.XhrHijack} |
| * |
| * Note: this usage of at-this is deployed in lieu of at-export because of |
| * much smaller output file sizes for this module, which is loaded blocking |
| * in head. |
| */ |
| mob.XhrHijack.prototype['getResponseHeader'] = function(name) { |
| return this.xhr.getResponseHeader(name); |
| }; |
| |
| |
| /** |
| * Hijack XMLHttpRequest with mob.XhrHijack. |
| */ |
| window.XMLHttpRequest = /** @type {function (new:XMLHttpRequest)} */ |
| (mob.XhrHijack); |