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;
+  }
+}