Move XHRProxy code to the xhrInterceptor folder
Change-Id: I2d2e9a4b4c43b1ed75ed500ba202283e8f3b16b7
diff --git a/src/xhrInterceptor/XHRProxy.js b/src/xhrInterceptor/XHRProxy.js
new file mode 100644
index 0000000..6254fbf
--- /dev/null
+++ b/src/xhrInterceptor/XHRProxy.js
@@ -0,0 +1,203 @@
+import {waitFor} from 'poll-until-promise';
+
+import {correctArrayKeys} from '../common/protojs';
+import ResponseModifier from '../xhrInterceptor/responseModifiers/index.js';
+import * as utils from '../xhrInterceptor/utils.js';
+
+const kSpecialEvents = ['load', 'loadend'];
+const kErrorEvents = ['error', 'timeout', 'abort'];
+
+const kCheckInterceptionOptions = {
+ interval: 50,
+ timeout: 100 * 1000,
+};
+
+function flattenOptions(options) {
+ if (typeof options === 'boolean') return options;
+ if (options) return options['capture'];
+ return undefined;
+}
+
+// Slightly based in https://stackoverflow.com/a/24561614.
+export default class XHRProxy {
+ constructor() {
+ this.originalXMLHttpRequest = window.XMLHttpRequest;
+ const classThis = this;
+
+ this.messageID = 0;
+ this.responseModifier = new ResponseModifier();
+
+ window.XMLHttpRequest = function() {
+ this.xhr = new classThis.originalXMLHttpRequest();
+ this.$TWPTID = classThis.messageID++;
+ this.$responseModified = false;
+ this.$responseIntercepted = false;
+ this.specialHandlers = {
+ load: new Set(),
+ loadend: new Set(),
+ };
+
+ const proxyThis = this;
+ kSpecialEvents.forEach(eventName => {
+ this.xhr.addEventListener(eventName, function() {
+ let p;
+ if (eventName === 'load') {
+ p = classThis.responseModifier.intercept(proxyThis, this.response).then(() => {
+ proxyThis.$responseIntercepted = true;
+ });
+ } else {
+ p = waitFor(() => {
+ if (proxyThis.$responseIntercepted) return Promise.resolve();
+ return Promise.reject();
+ }, kCheckInterceptionOptions);
+ }
+
+ p.then(() => {
+ for (const e of proxyThis.specialHandlers[eventName]) {
+ e[1](arguments);
+ }
+ });
+ });
+ });
+ kErrorEvents.forEach(eventName => {
+ this.xhr.addEventListener(eventName, function() {
+ proxyThis.$responseIntercepted = true;
+ });
+ });
+ };
+
+ const methods = [
+ 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
+ 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
+ ];
+ methods.forEach(method => {
+ window.XMLHttpRequest.prototype[method] = function() {
+ const proxyThis = this;
+
+ switch (method) {
+ case 'open':
+ this.$TWPTRequestURL = arguments[1] || location.href;
+
+ var interceptors =
+ utils.matchInterceptors('response', this.$TWPTRequestURL);
+ if (interceptors.length > 0) {
+ this.xhr.addEventListener('load', function() {
+ var body = utils.getResponseJSON(proxyThis);
+ if (body !== undefined)
+ interceptors.forEach(i => {
+ utils.triggerEvent(i.eventName, body, proxyThis.$TWPTID);
+ });
+ });
+ }
+ break;
+
+ case 'setRequestHeader':
+ let header = arguments[0];
+ let value = arguments[1];
+ if ('Content-Type'.localeCompare(
+ header, undefined, {sensitivity: 'accent'}) == 0)
+ this.$isArrayProto = (value == 'application/json+protobuf');
+ break;
+
+ case 'send':
+ var interceptors = utils.matchInterceptors(
+ 'request', this.$TWPTRequestURL || location.href);
+ if (interceptors.length > 0) {
+ let rawBody = arguments[0];
+ let body;
+ if (typeof (rawBody) === 'object' &&
+ (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
+ let dec = new TextDecoder('utf-8');
+ body = dec.decode(rawBody);
+ } else if (typeof (rawBody) === 'string') {
+ body = rawBody;
+ } else {
+ console.error(
+ 'Unexpected type of request body (' + typeof (rawBody) +
+ ').',
+ this.$TWPTRequestURL);
+ return;
+ }
+
+ let JSONBody = JSON.parse(body);
+ if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
+
+ interceptors.forEach(i => {
+ utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
+ });
+ }
+ break;
+ }
+ return this.xhr[method].apply(this.xhr, arguments);
+ };
+ });
+
+ window.XMLHttpRequest.prototype.addEventListener = function() {
+ if (!kSpecialEvents.includes(arguments[0]))
+ return this.xhr.addEventListener.apply(this.xhr, arguments);
+
+ this.specialHandlers[arguments[0]].add(arguments);
+ };
+
+ window.XMLHttpRequest.prototype.removeEventListener = function(
+ type, callback, options) {
+ if (!kSpecialEvents.includes(type))
+ return this.xhr.removeEventListener.apply(this.xhr, arguments);
+
+ const flattenedOptions = flattenOptions(options);
+ for (const e of this.specialHandlers[type]) {
+ if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
+ return this.specialHandlers[type].delete(e);
+ }
+ }
+ };
+
+ const scalars = [
+ 'onabort',
+ 'onerror',
+ 'onload',
+ 'onloadstart',
+ 'onloadend',
+ 'onprogress',
+ 'onreadystatechange',
+ 'readyState',
+ 'responseText',
+ 'responseType',
+ 'responseXML',
+ 'status',
+ 'statusText',
+ 'upload',
+ 'withCredentials',
+ 'DONE',
+ 'UNSENT',
+ 'HEADERS_RECEIVED',
+ 'LOADING',
+ 'OPENED'
+ ];
+ scalars.forEach(scalar => {
+ Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
+ get: function() {
+ return this.xhr[scalar];
+ },
+ set: function(val) {
+ this.xhr[scalar] = val;
+ },
+ });
+ });
+
+ Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
+ get: function() {
+ if (!this.$responseIntercepted) return undefined;
+ if (this.$responseModified) return this.$newResponse;
+ return this.xhr.response;
+ },
+ });
+ Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
+ get: function() {
+ return this.xhr.response;
+ },
+ });
+
+ return this;
+ }
+}