blob: b04ef697cb84bc90a6d34af342db9fbd75ae999b [file] [log] [blame]
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +01001import {waitFor} from 'poll-until-promise';
2
Adrià Vilanova Martínez4e0cb182022-06-26 00:21:50 +02003import {correctArrayKeys} from '../common/protojs';
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +01004import ResponseModifier from '../xhrInterceptor/responseModifiers/index.js';
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +01005import * as utils from '../xhrInterceptor/utils.js';
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +02006
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +01007const kSpecialEvents = ['load', 'loadend'];
8const kErrorEvents = ['error', 'timeout', 'abort'];
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +02009
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010010const kStandardMethods = [
11 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
12 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
13];
14const kStandardScalars = [
15 'onabort', 'onerror', 'onload', 'onloadstart', 'onloadend', 'onprogress',
16 'onreadystatechange', 'readyState', 'responseType', 'responseXML', 'status',
17 'statusText', 'upload', 'withCredentials', 'DONE', 'UNSENT',
18 'HEADERS_RECEIVED', 'LOADING', 'OPENED'
19];
20
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010021const kCheckInterceptionOptions = {
22 interval: 50,
23 timeout: 100 * 1000,
24};
avm999631f50f6f2021-08-12 23:04:41 +020025
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010026/**
27 * Implements the flatten options concept of the DOM spec.
28 *
29 * @see {@link https://dom.spec.whatwg.org/#concept-flatten-options}
30 */
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010031function flattenOptions(options) {
32 if (typeof options === 'boolean') return options;
33 if (options) return options['capture'];
34 return undefined;
35}
36
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010037/**
38 * Class which, when instantiated, overrides window.XMLHttpRequest to proxy the
39 * requests through our internal interceptors to read/modify requests/responses.
40 *
41 * Slightly based in https://stackoverflow.com/a/24561614.
42 */
Adrià Vilanova Martínez4757ae82022-12-28 16:41:37 +010043export default class XHRProxy {
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010044 constructor() {
45 this.originalXMLHttpRequest = window.XMLHttpRequest;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +020046
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010047 this.messageID = 0;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010048 this.responseModifier = new ResponseModifier();
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010049
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010050 this.#overrideXHRObject();
51 }
52
53 #overrideXHRObject() {
54 this.#overrideConstructor();
55 this.#addProxyEventsMethods();
56 this.#overrideMethods();
57 this.#addMethodInterceptors();
58 this.#overrideScalars();
59 this.#setOriginalResponseScalar();
60 }
61
62 #overrideConstructor() {
63 const XHRProxyInstance = this;
64
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010065 window.XMLHttpRequest = function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010066 this.xhr = new XHRProxyInstance.originalXMLHttpRequest();
67 this.$TWPTID = XHRProxyInstance.messageID++;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010068 this.$responseModified = false;
69 this.$responseIntercepted = false;
70 this.specialHandlers = {
71 load: new Set(),
72 loadend: new Set(),
73 };
74
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010075 this.$proxyEvents();
76 };
77 }
78
79 #addProxyEventsMethods() {
80 window.XMLHttpRequest.prototype.$proxyEvents = function() {
81 this.$proxySpecialEvents();
82 this.$proxyErrorEvents();
83 };
84 this.#addProxySpecialEvents();
85 this.#addProxyErrorEvents();
86 }
87
88 #overrideMethods() {
89 this.#overrideStandardMethods();
90 this.#overrideEventsMethods();
91 }
92
93 #addMethodInterceptors() {
94 this.#addOpenInterceptor();
95 this.#addSetRequestHeaderInterceptor();
96 this.#addSendInterceptor();
97 }
98
99 #overrideScalars() {
100 this.#overrideStandardScalars();
101 this.#overrideResponseScalars();
102 }
103
104 #overrideStandardMethods() {
105 kStandardMethods.forEach(method => {
106 this.#overrideStandardMethod(method);
107 });
108 }
109
110 #overrideEventsMethods() {
111 this.#overrideAddEventListener();
112 this.#overrideRemoveEventListener();
113 }
114
115 #overrideStandardScalars() {
116 kStandardScalars.forEach(scalar => {
117 this.#overrideStandardScalar(scalar);
118 });
119 }
120
121 #overrideResponseScalars() {
122 this.#overrideResponse();
123 this.#overrideResponseText();
124 }
125
126 #addProxySpecialEvents() {
127 const XHRProxyInstance = this;
128 window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
129 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100130 kSpecialEvents.forEach(eventName => {
131 this.xhr.addEventListener(eventName, function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100132 let interceptedPromise;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100133 if (eventName === 'load') {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100134 interceptedPromise =
135 XHRProxyInstance.responseModifier.intercept(proxyInstance)
136 .then(() => {
137 proxyInstance.$responseIntercepted = true;
138 });
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100139 } else {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100140 interceptedPromise = waitFor(() => {
141 if (proxyInstance.$responseIntercepted) return Promise.resolve();
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100142 return Promise.reject();
143 }, kCheckInterceptionOptions);
144 }
145
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100146 interceptedPromise.then(() => {
147 for (const e of proxyInstance.specialHandlers[eventName]) {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100148 e[1](arguments);
149 }
150 });
151 });
152 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100153 };
154 }
155
156 #addProxyErrorEvents() {
157 window.XMLHttpRequest.prototype.$proxyErrorEvents = function() {
158 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100159 kErrorEvents.forEach(eventName => {
160 this.xhr.addEventListener(eventName, function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100161 proxyInstance.$responseIntercepted = true;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100162 });
163 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100164 }
165 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100166
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100167 #overrideStandardMethod(method) {
168 window.XMLHttpRequest.prototype[method] = function() {
169 if (method == 'open')
170 this.$interceptOpen(...arguments);
171 else if (method == 'setRequestHeader')
172 this.$interceptSetRequestHeader(...arguments);
173 else if (method == 'send')
174 this.$interceptSend(...arguments);
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100175
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100176 return this.xhr[method].apply(this.xhr, arguments);
177 }
178 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100179
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100180 #overrideStandardScalar(scalar) {
181 Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
182 get: function() {
183 return this.xhr[scalar];
184 },
185 set: function(val) {
186 this.xhr[scalar] = val;
187 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200188 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100189 }
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200190
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100191 #overrideAddEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100192 window.XMLHttpRequest.prototype.addEventListener = function() {
193 if (!kSpecialEvents.includes(arguments[0]))
194 return this.xhr.addEventListener.apply(this.xhr, arguments);
195
196 this.specialHandlers[arguments[0]].add(arguments);
197 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100198 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100199
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100200 #overrideRemoveEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100201 window.XMLHttpRequest.prototype.removeEventListener = function(
202 type, callback, options) {
203 if (!kSpecialEvents.includes(type))
204 return this.xhr.removeEventListener.apply(this.xhr, arguments);
205
206 const flattenedOptions = flattenOptions(options);
207 for (const e of this.specialHandlers[type]) {
208 if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
209 return this.specialHandlers[type].delete(e);
210 }
211 }
212 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100213 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100214
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100215 #addOpenInterceptor() {
216 window.XMLHttpRequest.prototype.$interceptOpen = function() {
217 const proxyInstance = this;
218
219 this.$TWPTRequestURL = arguments[1] || location.href;
220
221 var interceptors =
222 utils.matchInterceptors('response', this.$TWPTRequestURL);
223 if (interceptors.length > 0) {
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100224 this.addEventListener('load', function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100225 var body = utils.getResponseJSON({
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100226 responseType: proxyInstance.responseType,
227 response: proxyInstance.response,
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100228 $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
229 $isArrayProto: proxyInstance.$isArrayProto,
230 });
231 if (body !== undefined)
232 interceptors.forEach(i => {
233 utils.triggerEvent(i.eventName, body, proxyInstance.$TWPTID);
234 });
235 });
236 }
237 };
238 }
239
240 #addSetRequestHeaderInterceptor() {
241 window.XMLHttpRequest.prototype.$interceptSetRequestHeader = function() {
242 let header = arguments[0];
243 let value = arguments[1];
244 if ('Content-Type'.localeCompare(
245 header, undefined, {sensitivity: 'accent'}) == 0)
246 this.$isArrayProto = (value == 'application/json+protobuf');
247 };
248 }
249
250 #addSendInterceptor() {
251 window.XMLHttpRequest.prototype.$interceptSend = function() {
252 var interceptors = utils.matchInterceptors(
253 'request', this.$TWPTRequestURL || location.href);
254 if (interceptors.length > 0) {
255 let rawBody = arguments[0];
256 let body;
257 if (typeof (rawBody) === 'object' &&
258 (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
259 let dec = new TextDecoder('utf-8');
260 body = dec.decode(rawBody);
261 } else if (typeof (rawBody) === 'string') {
262 body = rawBody;
263 } else {
264 console.error(
265 'Unexpected type of request body (' + typeof (rawBody) + ').',
266 this.$TWPTRequestURL);
267 return;
268 }
269
270 let JSONBody = JSON.parse(body);
271 if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
272
273 interceptors.forEach(i => {
274 utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
275 });
276 }
277 }
278 }
279
280 #setOriginalResponseScalar() {
281 Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
282 get: function() {
283 return this.xhr.response;
284 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200285 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100286 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100287
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100288 #overrideResponse() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100289 Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
290 get: function() {
291 if (!this.$responseIntercepted) return undefined;
292 if (this.$responseModified) return this.$newResponse;
293 return this.xhr.response;
294 },
295 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100296 }
297
298 #overrideResponseText() {
Adrià Vilanova Martínez8a17fa82023-02-04 19:19:27 +0100299 Object.defineProperty(window.XMLHttpRequest.prototype, 'responseText', {
300 get: function() {
301 if (!this.$responseIntercepted) return undefined;
302 if (this.$responseModified) return this.$newResponseText;
303 return this.xhr.responseText;
304 },
305 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200306 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100307}