Refactor XHR interceptor

In preparation for future work on the XHR interceptor.

Bug: twpowertools:153
Change-Id: Id8df1486c033ba02429a17d161e2bcc87a0f1de5
diff --git a/src/common/contentScriptsUtils.js b/src/common/contentScriptsUtils.js
index cb7a67d..51257c0 100644
--- a/src/common/contentScriptsUtils.js
+++ b/src/common/contentScriptsUtils.js
@@ -10,8 +10,12 @@
   injectStylesheet('data:text/css;charset=UTF-8,' + encodeURIComponent(css));
 }
 
-export function injectScript(scriptName) {
+export function injectScript(scriptName, prepend = false) {
   var script = document.createElement('script');
   script.src = scriptName;
-  (document.head || document.documentElement).append(script);
+  const root = (document.head || document.documentElement);
+  if (prepend)
+    root.prepend(script);
+  else
+    root.append(script);
 }
diff --git a/src/common/xhrInterceptorUtils.js b/src/common/xhrInterceptorUtils.js
deleted file mode 100644
index 526296c..0000000
--- a/src/common/xhrInterceptorUtils.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {correctArrayKeys} from '../common/protojs';
-
-import xhrInterceptors from './xhrInterceptors.json5';
-
-export {xhrInterceptors};
-
-export function matchInterceptors(interceptFilter, url) {
-  return xhrInterceptors.interceptors.filter(interceptor => {
-    var regex = new RegExp(interceptor.urlRegex);
-    return interceptor.intercepts == interceptFilter && regex.test(url);
-  });
-}
-
-export function getResponseJSON(xhr) {
-  let response;
-  if (xhr.responseType === 'arraybuffer') {
-    var arrBuffer = xhr.response;
-    if (!arrBuffer) {
-      console.error('No array buffer.');
-      return undefined;
-    }
-    let byteArray = new Uint8Array(arrBuffer);
-    let dec = new TextDecoder('utf-8');
-    let rawResponse = dec.decode(byteArray);
-    response = JSON.parse(rawResponse);
-  } else if (xhr.responseType === 'text' || xhr.responseType === '') {
-    response = JSON.parse(xhr.responseText);
-  } else if (xhr.responseType === 'json') {
-    response = xhr.response;
-  } else {
-    console.error(
-        'Unexpected responseType ' + xhr.responseType + '. Request url: ',
-        xhr.$TWPTRequestURL);
-    return undefined;
-  }
-
-  if (xhr.$isArrayProto) response = correctArrayKeys(response);
-  return response;
-}
-
-export function triggerEvent(eventName, body, id) {
-  var evt = new CustomEvent('TWPT_' + eventName, {
-    detail: {
-      body,
-      id,
-    }
-  });
-  window.dispatchEvent(evt);
-}
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index b3417fe..a6e171d 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -9,7 +9,9 @@
 const SMEI_NESTED_REPLIES = 15;
 const SMEI_RCE_THREAD_INTEROP = 22;
 
-injectScript(chrome.runtime.getURL('xhrInterceptorInject.bundle.js'));
+injectScript(
+    chrome.runtime.getURL('xhrInterceptorInject.bundle.js'),
+    /* prepend = */ true);
 injectScript(chrome.runtime.getURL('extraInfoInject.bundle.js'));
 
 getOptions(null).then(options => {
diff --git a/src/injections/xhrInterceptor.js b/src/injections/xhrInterceptor.js
index 071daf5..ef2efe3 100644
--- a/src/injections/xhrInterceptor.js
+++ b/src/injections/xhrInterceptor.js
@@ -1,5 +1,5 @@
 import {correctArrayKeys} from '../common/protojs';
-import * as utils from '../common/xhrInterceptorUtils.js';
+import * as utils from '../xhrInterceptor/utils.js';
 
 const originalOpen = window.XMLHttpRequest.prototype.open;
 const originalSetRequestHeader =
@@ -8,60 +8,121 @@
 
 let messageID = 0;
 
-window.XMLHttpRequest.prototype.open = function() {
-  this.$TWPTRequestURL = arguments[1] || location.href;
-  this.$TWPTID = messageID++;
+class XHRProxy {
+  constructor() {
+    this.originalXMLHttpRequest = window.XMLHttpRequest;
+    const originalXMLHttpRequest = this.originalXMLHttpRequest;
 
-  let interceptors = utils.matchInterceptors('response', this.$TWPTRequestURL);
-  if (interceptors.length > 0) {
-    this.addEventListener('load', function() {
-      var body = utils.getResponseJSON(this);
-      if (body !== undefined)
-        interceptors.forEach(i => {
-          utils.triggerEvent(i.eventName, body, this.$TWPTID);
-        });
+    this.messageID = 0;
+
+    window.XMLHttpRequest = function() {
+      this.xhr = new originalXMLHttpRequest();
+      this.$TWPTID = messageID++;
+    };
+
+    const methods = [
+      'open', 'abort', 'setRequestHeader', 'send', 'addEventListener',
+      'removeEventListener', '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(this);
+                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);
+      };
     });
-  }
 
-  originalOpen.apply(this, arguments);
-};
-
-window.XMLHttpRequest.prototype.setRequestHeader = function() {
-  originalSetRequestHeader.apply(this, arguments);
-
-  let header = arguments[0];
-  let value = arguments[1];
-  if ('Content-Type'.localeCompare(
-          header, undefined, {sensitivity: 'accent'}) == 0)
-    this.$isArrayProto = (value == 'application/json+protobuf');
-};
-
-window.XMLHttpRequest.prototype.send = function() {
-  originalSend.apply(this, arguments);
-
-  let 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);
+    const scalars = [
+      'onabort',
+      'onerror',
+      'onload',
+      'onloadstart',
+      'onloadend',
+      'onprogress',
+      'onreadystatechange',
+      'readyState',
+      'response',
+      '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;
+        },
+      });
     });
+
+    return this;
   }
-};
+}
+
+new XHRProxy();
diff --git a/src/common/xhrInterceptors.json5 b/src/xhrInterceptor/interceptors.json5
similarity index 100%
rename from src/common/xhrInterceptors.json5
rename to src/xhrInterceptor/interceptors.json5
diff --git a/src/xhrInterceptor/utils.js b/src/xhrInterceptor/utils.js
new file mode 100644
index 0000000..909549b
--- /dev/null
+++ b/src/xhrInterceptor/utils.js
@@ -0,0 +1,77 @@
+import {correctArrayKeys} from '../common/protojs';
+
+import xhrInterceptors from './interceptors.json5';
+
+export {xhrInterceptors};
+
+export function matchInterceptors(interceptFilter, url) {
+  return xhrInterceptors.interceptors.filter(interceptor => {
+    var regex = new RegExp(interceptor.urlRegex);
+    return interceptor.intercepts == interceptFilter && regex.test(url);
+  });
+}
+
+export function getResponseText(xhr, transformArrayPb = true) {
+  let response;
+  if (xhr.responseType === 'arraybuffer') {
+    var arrBuffer = xhr.response;
+    if (!arrBuffer) {
+      console.error('No array buffer.');
+      return undefined;
+    }
+    let byteArray = new Uint8Array(arrBuffer);
+    let dec = new TextDecoder('utf-8');
+    response = dec.decode(byteArray);
+  } else if (xhr.responseType === 'text' || xhr.responseType === '') {
+    response = xhr.responseText;
+  } else if (xhr.responseType === 'json') {
+    response = JSON.stringify(xhr.response);
+  } else {
+    console.error(
+        'Unexpected responseType ' + xhr.responseType + '. Request url: ',
+        xhr.$TWPTRequestURL);
+    return undefined;
+  }
+
+  if (xhr.$isArrayProto && transformArrayPb)
+    response = correctArrayKeys(response);
+
+  return response;
+}
+
+export function getResponseJSON(xhr) {
+  let response;
+  if (xhr.responseType === 'arraybuffer') {
+    var arrBuffer = xhr.response;
+    if (!arrBuffer) {
+      console.error('No array buffer.');
+      return undefined;
+    }
+    let byteArray = new Uint8Array(arrBuffer);
+    let dec = new TextDecoder('utf-8');
+    let rawResponse = dec.decode(byteArray);
+    response = JSON.parse(rawResponse);
+  } else if (xhr.responseType === 'text' || xhr.responseType === '') {
+    response = JSON.parse(xhr.responseText);
+  } else if (xhr.responseType === 'json') {
+    response = xhr.response;
+  } else {
+    console.error(
+        'Unexpected responseType ' + xhr.responseType + '. Request url: ',
+        xhr.$TWPTRequestURL);
+    return undefined;
+  }
+
+  if (xhr.$isArrayProto) response = correctArrayKeys(response);
+  return response;
+}
+
+export function triggerEvent(eventName, body, id) {
+  var evt = new CustomEvent('TWPT_' + eventName, {
+    detail: {
+      body,
+      id,
+    }
+  });
+  window.dispatchEvent(evt);
+}