fix: add FetchProxy
This will let us intercept fetch requests (until now we're only proxying
XMLHttpRequest), in order to fix the issues we're experiencing with some
features.
Bug: twpowertools:229
Change-Id: I277473c05479ca39bb6183a51855382124890bde
diff --git a/src/xhrInterceptor/ResponseModifier.adapter.ts b/src/xhrInterceptor/ResponseModifier.adapter.ts
new file mode 100644
index 0000000..c6ed999
--- /dev/null
+++ b/src/xhrInterceptor/ResponseModifier.adapter.ts
@@ -0,0 +1,80 @@
+import MWOptionsWatcherClient from '../common/mainWorldOptionsWatcher/Client.js';
+import { OptionCodename } from '../common/options/optionsPrototype.js';
+import {
+ InterceptedResponse,
+ ResponseModifierPort,
+ Result,
+} from './ResponseModifier.port.js';
+
+import { Modifier } from './responseModifiers/types.js';
+
+// 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 ResponseModifierAdapter implements ResponseModifierPort {
+ private optionsWatcher: MWOptionsWatcherClient;
+
+ constructor(private responseModifiers: Modifier[]) {
+ this.optionsWatcher = new MWOptionsWatcherClient(
+ Array.from(this.watchingFeatures(this.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 = this.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,
+ };
+ }
+}