refactor(xhr-proxy): separate logic by levels of abstraction
This CL is an attempt to partially clean the code of the XHRProxy class
by separating logic with several levels of abstraction into their own
methods.
Change-Id: Ifd40d112936f16a467bcd735d751b10ff7f97eae
diff --git a/src/xhrInterceptor/XHRProxy.js b/src/xhrInterceptor/XHRProxy.js
index 2d6d03f..97b9158 100644
--- a/src/xhrInterceptor/XHRProxy.js
+++ b/src/xhrInterceptor/XHRProxy.js
@@ -7,29 +7,64 @@
const kSpecialEvents = ['load', 'loadend'];
const kErrorEvents = ['error', 'timeout', 'abort'];
+const kStandardMethods = [
+ 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
+ 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
+];
+const kStandardScalars = [
+ 'onabort', 'onerror', 'onload', 'onloadstart', 'onloadend', 'onprogress',
+ 'onreadystatechange', 'readyState', 'responseType', 'responseXML', 'status',
+ 'statusText', 'upload', 'withCredentials', 'DONE', 'UNSENT',
+ 'HEADERS_RECEIVED', 'LOADING', 'OPENED'
+];
+
const kCheckInterceptionOptions = {
interval: 50,
timeout: 100 * 1000,
};
+/**
+ * Implements the flatten options concept of the DOM spec.
+ *
+ * @see {@link https://dom.spec.whatwg.org/#concept-flatten-options}
+ */
function flattenOptions(options) {
if (typeof options === 'boolean') return options;
if (options) return options['capture'];
return undefined;
}
-// Slightly based in https://stackoverflow.com/a/24561614.
+/**
+ * Class which, when instantiated, overrides window.XMLHttpRequest to proxy the
+ * requests through our internal interceptors to read/modify requests/responses.
+ *
+ * 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();
+ this.#overrideXHRObject();
+ }
+
+ #overrideXHRObject() {
+ this.#overrideConstructor();
+ this.#addProxyEventsMethods();
+ this.#overrideMethods();
+ this.#addMethodInterceptors();
+ this.#overrideScalars();
+ this.#setOriginalResponseScalar();
+ }
+
+ #overrideConstructor() {
+ const XHRProxyInstance = this;
+
window.XMLHttpRequest = function() {
- this.xhr = new classThis.originalXMLHttpRequest();
- this.$TWPTID = classThis.messageID++;
+ this.xhr = new XHRProxyInstance.originalXMLHttpRequest();
+ this.$TWPTID = XHRProxyInstance.messageID++;
this.$responseModified = false;
this.$responseIntercepted = false;
this.specialHandlers = {
@@ -37,113 +72,132 @@
loadend: new Set(),
};
- const proxyThis = this;
+ this.$proxyEvents();
+ };
+ }
+
+ #addProxyEventsMethods() {
+ window.XMLHttpRequest.prototype.$proxyEvents = function() {
+ this.$proxySpecialEvents();
+ this.$proxyErrorEvents();
+ };
+ this.#addProxySpecialEvents();
+ this.#addProxyErrorEvents();
+ }
+
+ #overrideMethods() {
+ this.#overrideStandardMethods();
+ this.#overrideEventsMethods();
+ }
+
+ #addMethodInterceptors() {
+ this.#addOpenInterceptor();
+ this.#addSetRequestHeaderInterceptor();
+ this.#addSendInterceptor();
+ }
+
+ #overrideScalars() {
+ this.#overrideStandardScalars();
+ this.#overrideResponseScalars();
+ }
+
+ #overrideStandardMethods() {
+ kStandardMethods.forEach(method => {
+ this.#overrideStandardMethod(method);
+ });
+ }
+
+ #overrideEventsMethods() {
+ this.#overrideAddEventListener();
+ this.#overrideRemoveEventListener();
+ }
+
+ #overrideStandardScalars() {
+ kStandardScalars.forEach(scalar => {
+ this.#overrideStandardScalar(scalar);
+ });
+ }
+
+ #overrideResponseScalars() {
+ this.#overrideResponse();
+ this.#overrideResponseText();
+ }
+
+ #addProxySpecialEvents() {
+ const XHRProxyInstance = this;
+ window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
+ const proxyInstance = this;
kSpecialEvents.forEach(eventName => {
this.xhr.addEventListener(eventName, function() {
- let p;
+ let interceptedPromise;
if (eventName === 'load') {
- p = classThis.responseModifier.intercept(proxyThis).then(() => {
- proxyThis.$responseIntercepted = true;
- });
+ interceptedPromise =
+ XHRProxyInstance.responseModifier.intercept(proxyInstance)
+ .then(() => {
+ proxyInstance.$responseIntercepted = true;
+ });
} else {
- p = waitFor(() => {
- if (proxyThis.$responseIntercepted) return Promise.resolve();
+ interceptedPromise = waitFor(() => {
+ if (proxyInstance.$responseIntercepted) return Promise.resolve();
return Promise.reject();
}, kCheckInterceptionOptions);
}
- p.then(() => {
- for (const e of proxyThis.specialHandlers[eventName]) {
+ interceptedPromise.then(() => {
+ for (const e of proxyInstance.specialHandlers[eventName]) {
e[1](arguments);
}
});
});
});
+ };
+ }
+
+ #addProxyErrorEvents() {
+ window.XMLHttpRequest.prototype.$proxyErrorEvents = function() {
+ const proxyInstance = this;
kErrorEvents.forEach(eventName => {
this.xhr.addEventListener(eventName, function() {
- proxyThis.$responseIntercepted = true;
+ proxyInstance.$responseIntercepted = true;
});
});
- };
+ }
+ }
- const methods = [
- 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
- 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
- ];
- methods.forEach(method => {
- window.XMLHttpRequest.prototype[method] = function() {
- const proxyThis = this;
+ #overrideStandardMethod(method) {
+ window.XMLHttpRequest.prototype[method] = function() {
+ if (method == 'open')
+ this.$interceptOpen(...arguments);
+ else if (method == 'setRequestHeader')
+ this.$interceptSetRequestHeader(...arguments);
+ else if (method == 'send')
+ this.$interceptSend(...arguments);
- switch (method) {
- case 'open':
- this.$TWPTRequestURL = arguments[1] || location.href;
+ return this.xhr[method].apply(this.xhr, arguments);
+ }
+ }
- var interceptors =
- utils.matchInterceptors('response', this.$TWPTRequestURL);
- if (interceptors.length > 0) {
- this.xhr.addEventListener('load', function() {
- var body = utils.getResponseJSON({
- responseType: proxyThis.xhr.responseType,
- response: proxyThis.xhr.response,
- $TWPTRequestURL: proxyThis.$TWPTRequestURL,
- $isArrayProto: proxyThis.$isArrayProto,
- });
- 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);
- };
+ #overrideStandardScalar(scalar) {
+ Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
+ get: function() {
+ return this.xhr[scalar];
+ },
+ set: function(val) {
+ this.xhr[scalar] = val;
+ },
});
+ }
+ #overrideAddEventListener() {
window.XMLHttpRequest.prototype.addEventListener = function() {
if (!kSpecialEvents.includes(arguments[0]))
return this.xhr.addEventListener.apply(this.xhr, arguments);
this.specialHandlers[arguments[0]].add(arguments);
};
+ }
+ #overrideRemoveEventListener() {
window.XMLHttpRequest.prototype.removeEventListener = function(
type, callback, options) {
if (!kSpecialEvents.includes(type))
@@ -156,39 +210,82 @@
}
}
};
+ }
- const scalars = [
- 'onabort',
- 'onerror',
- 'onload',
- 'onloadstart',
- 'onloadend',
- 'onprogress',
- 'onreadystatechange',
- 'readyState',
- '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;
- },
- });
+ #addOpenInterceptor() {
+ window.XMLHttpRequest.prototype.$interceptOpen = function() {
+ const proxyInstance = this;
+
+ 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({
+ responseType: proxyInstance.xhr.responseType,
+ response: proxyInstance.xhr.response,
+ $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
+ $isArrayProto: proxyInstance.$isArrayProto,
+ });
+ if (body !== undefined)
+ interceptors.forEach(i => {
+ utils.triggerEvent(i.eventName, body, proxyInstance.$TWPTID);
+ });
+ });
+ }
+ };
+ }
+
+ #addSetRequestHeaderInterceptor() {
+ window.XMLHttpRequest.prototype.$interceptSetRequestHeader = function() {
+ let header = arguments[0];
+ let value = arguments[1];
+ if ('Content-Type'.localeCompare(
+ header, undefined, {sensitivity: 'accent'}) == 0)
+ this.$isArrayProto = (value == 'application/json+protobuf');
+ };
+ }
+
+ #addSendInterceptor() {
+ window.XMLHttpRequest.prototype.$interceptSend = function() {
+ 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);
+ });
+ }
+ }
+ }
+
+ #setOriginalResponseScalar() {
+ Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
+ get: function() {
+ return this.xhr.response;
+ },
});
+ }
+ #overrideResponse() {
Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
get: function() {
if (!this.$responseIntercepted) return undefined;
@@ -196,11 +293,9 @@
return this.xhr.response;
},
});
- Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
- get: function() {
- return this.xhr.response;
- },
- });
+ }
+
+ #overrideResponseText() {
Object.defineProperty(window.XMLHttpRequest.prototype, 'responseText', {
get: function() {
if (!this.$responseIntercepted) return undefined;
@@ -208,7 +303,5 @@
return this.xhr.responseText;
},
});
-
- return this;
}
}