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