refactor(response-modifier): extract XHR-specific code from the class
The ResponseModifier class had code specific for XMLHttpRequest. Since
in a follow-up commit we will add support for proxying fetch as well,
this commit extracts the XMLHttpRequest-specific code to the XHRProxy
class.
Bug: twpowertools:229
Change-Id: I1b3653ee20f09ec762aafbac2583a96790a27f45
diff --git a/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts b/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
index 464a7fa..c5d5311 100644
--- a/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
+++ b/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
@@ -1,6 +1,6 @@
import Script, { ScriptEnvironment, ScriptPage, ScriptRunPhase } from "../../../common/architecture/scripts/Script"
import MWOptionsWatcherServer from "../../../common/mainWorldOptionsWatcher/Server"
-import { kCSTarget, kMWTarget } from "../../../xhrInterceptor/responseModifiers"
+import { kCSTarget, kMWTarget } from "../../../xhrInterceptor/ResponseModifier"
export default class MWOptionsWatcherServerScript extends Script {
// The server should be available as soon as possible, since e.g. the
diff --git a/src/xhrInterceptor/ResponseModifier.ts b/src/xhrInterceptor/ResponseModifier.ts
new file mode 100644
index 0000000..c2b3a34
--- /dev/null
+++ b/src/xhrInterceptor/ResponseModifier.ts
@@ -0,0 +1,107 @@
+import MWOptionsWatcherClient from '../common/mainWorldOptionsWatcher/Client.js';
+import { OptionCodename } from '../common/options/optionsPrototype.js';
+import { ProtobufObject } from '../common/protojs.types.js';
+
+import createMessageRemoveParentRef from './responseModifiers/createMessageRemoveParentRef';
+import flattenThread from './responseModifiers/flattenThread';
+import loadMoreThread from './responseModifiers/loadMoreThread';
+import { Modifier } from './responseModifiers/types';
+
+export const responseModifiers = [
+ loadMoreThread,
+ flattenThread,
+ createMessageRemoveParentRef,
+] as Modifier[];
+
+// Content script target
+export const kCSTarget = 'TWPT-XHRInterceptorOptionsWatcher-CS';
+// Main world (AKA regular web page) target
+export const kMWTarget = 'TWPT-XHRInterceptorOptionsWatcher-MW';
+
+export default class ResponseModifier {
+ private optionsWatcher: MWOptionsWatcherClient;
+
+ constructor() {
+ this.optionsWatcher = new MWOptionsWatcherClient(
+ Array.from(this.watchingFeatures(responseModifiers)),
+ kCSTarget,
+ kMWTarget,
+ );
+ }
+
+ private watchingFeatures(modifiers: Modifier[]): Set<OptionCodename> {
+ const union = new Set<OptionCodename>();
+ for (const m of modifiers) {
+ if (!m.featureGated) continue;
+ for (const feature of m.features) union.add(feature);
+ }
+ return union;
+ }
+
+ private async getMatchingModifiers(requestUrl: string) {
+ // First filter modifiers which match the request URL regex.
+ const urlModifiers = responseModifiers.filter((modifier) =>
+ requestUrl.match(modifier.urlRegex),
+ );
+
+ // Now filter modifiers which require a certain feature to be enabled
+ // (feature-gated modifiers).
+ const featuresAreEnabled = await this.optionsWatcher.areEnabled(
+ Array.from(this.watchingFeatures(urlModifiers)),
+ );
+
+ // #!if !production
+ if (Object.keys(featuresAreEnabled).length > 0) {
+ console.debug(
+ '[XHR Interceptor - Response Modifier] Requested features',
+ featuresAreEnabled,
+ 'for request',
+ requestUrl,
+ );
+ }
+ // #!endif
+
+ return urlModifiers.filter((modifier) => {
+ return !modifier.featureGated || modifier.isEnabled(featuresAreEnabled);
+ });
+ }
+
+ async intercept(interception: InterceptedResponse): Promise<Result> {
+ const matchingModifiers = await this.getMatchingModifiers(interception.url);
+
+ // If we didn't find any matching modifiers, return the response right away.
+ if (matchingModifiers.length === 0) return { wasModified: false };
+
+ // Otherwise, apply the modifiers sequentially and set the new response.
+ let json = interception.originalResponse;
+ for (const modifier of matchingModifiers) {
+ json = await modifier.interceptor(json, interception.url);
+ }
+ return {
+ wasModified: true,
+ modifiedResponse: json,
+ };
+ }
+}
+
+/**
+ * Represents an intercepted response.
+ */
+export interface InterceptedResponse {
+ /**
+ * URL of the original request.
+ */
+ url: string;
+
+ /**
+ * Object with the response as intercepted without any modification.
+ */
+ originalResponse: ProtobufObject;
+}
+
+export type Result =
+ | { wasModified: false }
+ | {
+ wasModified: true;
+ modifiedResponse: ProtobufObject;
+ };
diff --git a/src/xhrInterceptor/XHRProxy.js b/src/xhrInterceptor/XHRProxy.js
index 95529d8..14d2aae 100644
--- a/src/xhrInterceptor/XHRProxy.js
+++ b/src/xhrInterceptor/XHRProxy.js
@@ -1,7 +1,7 @@
import {waitFor} from 'poll-until-promise';
import {correctArrayKeys} from '../common/protojs';
-import ResponseModifier from '../xhrInterceptor/responseModifiers/index.js';
+import ResponseModifier from '../xhrInterceptor/ResponseModifier';
import * as utils from '../xhrInterceptor/utils.js';
const kSpecialEvents = ['load', 'loadend'];
@@ -144,26 +144,39 @@
window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
const proxyInstance = this;
kSpecialEvents.forEach(eventName => {
- this.xhr.addEventListener(eventName, function() {
- let interceptedPromise;
+ this.xhr.addEventListener(eventName, async function() {
if (eventName === 'load') {
- interceptedPromise =
- XHRProxyInstance.responseModifier.intercept(proxyInstance)
- .then(() => {
- proxyInstance.$responseIntercepted = true;
- });
+ const initialResponse = utils.getResponseJSON({
+ responseType: proxyInstance.xhr.responseType,
+ response: proxyInstance.xhr.response,
+ $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
+ $isArrayProto: proxyInstance.$isArrayProto,
+ });
+
+ const result = await XHRProxyInstance.responseModifier.intercept({
+ url: proxyInstance.$TWPTRequestURL,
+ originalResponse: initialResponse,
+ });
+
+ if (result.wasModified) {
+ proxyInstance.$newResponse = utils.convertJSONToResponse(
+ proxyInstance, result.modifiedResponse);
+ proxyInstance.$newResponseText = utils.convertJSONToResponseText(
+ proxyInstance, result.modifiedResponse);
+ }
+
+ proxyInstance.$responseModified = result.wasModified;
+ proxyInstance.$responseIntercepted = true;
} else {
- interceptedPromise = waitFor(() => {
+ await waitFor(() => {
if (proxyInstance.$responseIntercepted) return Promise.resolve();
return Promise.reject();
}, kCheckInterceptionOptions);
}
- interceptedPromise.then(() => {
- for (const e of proxyInstance.specialHandlers[eventName]) {
- e[1](arguments);
- }
- });
+ for (const e of proxyInstance.specialHandlers[eventName]) {
+ e[1](arguments);
+ }
});
});
};
diff --git a/src/xhrInterceptor/responseModifiers/index.js b/src/xhrInterceptor/responseModifiers/index.js
deleted file mode 100644
index b822153..0000000
--- a/src/xhrInterceptor/responseModifiers/index.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import MWOptionsWatcherClient from '../../common/mainWorldOptionsWatcher/Client.js';
-import {convertJSONToResponse, convertJSONToResponseText, getResponseJSON} from '../utils.js';
-
-import createMessageRemoveParentRef from './createMessageRemoveParentRef';
-import flattenThread from './flattenThread';
-import loadMoreThread from './loadMoreThread';
-
-export const responseModifiers = [
- loadMoreThread,
- flattenThread,
- createMessageRemoveParentRef,
-];
-
-// Content script target
-export const kCSTarget = 'TWPT-XHRInterceptorOptionsWatcher-CS';
-// Main world (AKA regular web page) target
-export const kMWTarget = 'TWPT-XHRInterceptorOptionsWatcher-MW';
-
-export default class ResponseModifier {
- constructor() {
- this.optionsWatcher = new MWOptionsWatcherClient(
- Array.from(this.watchingFeatures()), kCSTarget, kMWTarget);
- }
-
- watchingFeatures(modifiers) {
- if (!modifiers) modifiers = responseModifiers;
-
- const union = new Set();
- for (const m of modifiers) {
- if (!m.featureGated) continue;
- for (const feature of m.features) union.add(feature);
- }
- return union;
- }
-
- async #getMatchingModifiers(request) {
- // First filter modifiers which match the request URL regex.
- const urlModifiers = responseModifiers.filter(
- modifier => request.$TWPTRequestURL.match(modifier.urlRegex));
-
- // Now filter modifiers which require a certain feature to be enabled
- // (feature-gated modifiers).
- const featuresAreEnabled = await this.optionsWatcher.areEnabled(
- Array.from(this.watchingFeatures(urlModifiers)));
-
- // #!if !production
- if (Object.keys(featuresAreEnabled).length > 0) {
- console.debug(
- '[XHR Interceptor - Response Modifier] Requested features',
- featuresAreEnabled, 'for request', request.$TWPTRequestURL);
- }
- // #!endif
-
- return urlModifiers.filter(modifier => {
- return !modifier.featureGated || modifier.isEnabled(featuresAreEnabled);
- });
- }
-
- async intercept(request) {
- const matchingModifiers = await this.#getMatchingModifiers(request);
-
- // If we didn't find any matching modifiers, return the response right away.
- if (matchingModifiers.length === 0) return request.xhr.response;
-
- // Otherwise, apply the modifiers sequentially and set the new response.
- let json = getResponseJSON({
- responseType: request.xhr.responseType,
- response: request.xhr.response,
- $TWPTRequestURL: request.$TWPTRequestURL,
- $isArrayProto: request.$isArrayProto,
- });
- for (const modifier of matchingModifiers) {
- json = await modifier.interceptor(request, json);
- }
- request.$newResponse = convertJSONToResponse(request, json);
- request.$newResponseText = convertJSONToResponseText(request, json);
- request.$responseModified = true;
- }
-}