blob: 95529d8d76db02c614be8622233c93743914a958 [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 = [
Adrià Vilanova Martínez48dbe7c2024-02-26 04:40:34 +010015 'onabort',
16 'onerror',
17 'onload',
18 'onloadstart',
19 'onloadend',
20 'onprogress',
21 'onreadystatechange',
22 'readyState',
23 'responseType',
24 'responseURL',
25 'responseXML',
26 'status',
27 'statusText',
28 'upload',
29 'withCredentials',
30 'DONE',
31 'UNSENT',
32 'HEADERS_RECEIVED',
33 'LOADING',
34 'OPENED',
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010035];
36
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010037const kCheckInterceptionOptions = {
38 interval: 50,
39 timeout: 100 * 1000,
40};
avm999631f50f6f2021-08-12 23:04:41 +020041
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010042/**
43 * Implements the flatten options concept of the DOM spec.
44 *
45 * @see {@link https://dom.spec.whatwg.org/#concept-flatten-options}
46 */
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010047function flattenOptions(options) {
48 if (typeof options === 'boolean') return options;
49 if (options) return options['capture'];
50 return undefined;
51}
52
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010053/**
54 * Class which, when instantiated, overrides window.XMLHttpRequest to proxy the
55 * requests through our internal interceptors to read/modify requests/responses.
56 *
57 * Slightly based in https://stackoverflow.com/a/24561614.
58 */
Adrià Vilanova Martínez4757ae82022-12-28 16:41:37 +010059export default class XHRProxy {
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010060 constructor() {
61 this.originalXMLHttpRequest = window.XMLHttpRequest;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +020062
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010063 this.messageID = 0;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010064 this.responseModifier = new ResponseModifier();
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010065
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010066 this.#overrideXHRObject();
67 }
68
69 #overrideXHRObject() {
70 this.#overrideConstructor();
71 this.#addProxyEventsMethods();
72 this.#overrideMethods();
73 this.#addMethodInterceptors();
74 this.#overrideScalars();
75 this.#setOriginalResponseScalar();
76 }
77
78 #overrideConstructor() {
79 const XHRProxyInstance = this;
80
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010081 window.XMLHttpRequest = function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010082 this.xhr = new XHRProxyInstance.originalXMLHttpRequest();
83 this.$TWPTID = XHRProxyInstance.messageID++;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010084 this.$responseModified = false;
85 this.$responseIntercepted = false;
86 this.specialHandlers = {
87 load: new Set(),
88 loadend: new Set(),
89 };
90
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +010091 this.$proxyEvents();
92 };
93 }
94
95 #addProxyEventsMethods() {
96 window.XMLHttpRequest.prototype.$proxyEvents = function() {
97 this.$proxySpecialEvents();
98 this.$proxyErrorEvents();
99 };
100 this.#addProxySpecialEvents();
101 this.#addProxyErrorEvents();
102 }
103
104 #overrideMethods() {
105 this.#overrideStandardMethods();
106 this.#overrideEventsMethods();
107 }
108
109 #addMethodInterceptors() {
110 this.#addOpenInterceptor();
111 this.#addSetRequestHeaderInterceptor();
112 this.#addSendInterceptor();
113 }
114
115 #overrideScalars() {
116 this.#overrideStandardScalars();
117 this.#overrideResponseScalars();
118 }
119
120 #overrideStandardMethods() {
121 kStandardMethods.forEach(method => {
122 this.#overrideStandardMethod(method);
123 });
124 }
125
126 #overrideEventsMethods() {
127 this.#overrideAddEventListener();
128 this.#overrideRemoveEventListener();
129 }
130
131 #overrideStandardScalars() {
132 kStandardScalars.forEach(scalar => {
133 this.#overrideStandardScalar(scalar);
134 });
135 }
136
137 #overrideResponseScalars() {
138 this.#overrideResponse();
139 this.#overrideResponseText();
140 }
141
142 #addProxySpecialEvents() {
143 const XHRProxyInstance = this;
144 window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
145 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100146 kSpecialEvents.forEach(eventName => {
147 this.xhr.addEventListener(eventName, function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100148 let interceptedPromise;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100149 if (eventName === 'load') {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100150 interceptedPromise =
151 XHRProxyInstance.responseModifier.intercept(proxyInstance)
152 .then(() => {
153 proxyInstance.$responseIntercepted = true;
154 });
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100155 } else {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100156 interceptedPromise = waitFor(() => {
157 if (proxyInstance.$responseIntercepted) return Promise.resolve();
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100158 return Promise.reject();
159 }, kCheckInterceptionOptions);
160 }
161
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100162 interceptedPromise.then(() => {
163 for (const e of proxyInstance.specialHandlers[eventName]) {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100164 e[1](arguments);
165 }
166 });
167 });
168 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100169 };
170 }
171
172 #addProxyErrorEvents() {
173 window.XMLHttpRequest.prototype.$proxyErrorEvents = function() {
174 const proxyInstance = this;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100175 kErrorEvents.forEach(eventName => {
176 this.xhr.addEventListener(eventName, function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100177 proxyInstance.$responseIntercepted = true;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100178 });
179 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100180 }
181 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100182
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100183 #overrideStandardMethod(method) {
184 window.XMLHttpRequest.prototype[method] = function() {
185 if (method == 'open')
186 this.$interceptOpen(...arguments);
187 else if (method == 'setRequestHeader')
188 this.$interceptSetRequestHeader(...arguments);
189 else if (method == 'send')
190 this.$interceptSend(...arguments);
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100191
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100192 return this.xhr[method].apply(this.xhr, arguments);
193 }
194 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100195
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100196 #overrideStandardScalar(scalar) {
197 Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
198 get: function() {
199 return this.xhr[scalar];
200 },
201 set: function(val) {
202 this.xhr[scalar] = val;
203 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200204 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100205 }
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200206
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100207 #overrideAddEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100208 window.XMLHttpRequest.prototype.addEventListener = function() {
209 if (!kSpecialEvents.includes(arguments[0]))
210 return this.xhr.addEventListener.apply(this.xhr, arguments);
211
212 this.specialHandlers[arguments[0]].add(arguments);
213 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100214 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100215
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100216 #overrideRemoveEventListener() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100217 window.XMLHttpRequest.prototype.removeEventListener = function(
218 type, callback, options) {
219 if (!kSpecialEvents.includes(type))
220 return this.xhr.removeEventListener.apply(this.xhr, arguments);
221
222 const flattenedOptions = flattenOptions(options);
223 for (const e of this.specialHandlers[type]) {
224 if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
225 return this.specialHandlers[type].delete(e);
226 }
227 }
228 };
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100229 }
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100230
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100231 #addOpenInterceptor() {
232 window.XMLHttpRequest.prototype.$interceptOpen = function() {
233 const proxyInstance = this;
234
235 this.$TWPTRequestURL = arguments[1] || location.href;
236
237 var interceptors =
238 utils.matchInterceptors('response', this.$TWPTRequestURL);
239 if (interceptors.length > 0) {
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100240 this.addEventListener('load', function() {
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100241 var body = utils.getResponseJSON({
Adrià Vilanova Martínez88e5ef22023-11-15 22:45:13 +0100242 responseType: proxyInstance.responseType,
243 response: proxyInstance.response,
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100244 $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
245 $isArrayProto: proxyInstance.$isArrayProto,
246 });
247 if (body !== undefined)
248 interceptors.forEach(i => {
249 utils.triggerEvent(i.eventName, body, proxyInstance.$TWPTID);
250 });
251 });
252 }
253 };
254 }
255
256 #addSetRequestHeaderInterceptor() {
257 window.XMLHttpRequest.prototype.$interceptSetRequestHeader = function() {
258 let header = arguments[0];
259 let value = arguments[1];
260 if ('Content-Type'.localeCompare(
261 header, undefined, {sensitivity: 'accent'}) == 0)
262 this.$isArrayProto = (value == 'application/json+protobuf');
263 };
264 }
265
266 #addSendInterceptor() {
267 window.XMLHttpRequest.prototype.$interceptSend = function() {
268 var interceptors = utils.matchInterceptors(
269 'request', this.$TWPTRequestURL || location.href);
270 if (interceptors.length > 0) {
271 let rawBody = arguments[0];
272 let body;
273 if (typeof (rawBody) === 'object' &&
274 (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
275 let dec = new TextDecoder('utf-8');
276 body = dec.decode(rawBody);
277 } else if (typeof (rawBody) === 'string') {
278 body = rawBody;
279 } else {
280 console.error(
281 'Unexpected type of request body (' + typeof (rawBody) + ').',
282 this.$TWPTRequestURL);
283 return;
284 }
285
286 let JSONBody = JSON.parse(body);
287 if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
288
289 interceptors.forEach(i => {
290 utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
291 });
292 }
293 }
294 }
295
296 #setOriginalResponseScalar() {
297 Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
298 get: function() {
299 return this.xhr.response;
300 },
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200301 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100302 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100303
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100304 #overrideResponse() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100305 Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
306 get: function() {
307 if (!this.$responseIntercepted) return undefined;
308 if (this.$responseModified) return this.$newResponse;
309 return this.xhr.response;
310 },
311 });
Adrià Vilanova Martínez810383b2023-11-15 22:07:57 +0100312 }
313
314 #overrideResponseText() {
Adrià Vilanova Martínez8a17fa82023-02-04 19:19:27 +0100315 Object.defineProperty(window.XMLHttpRequest.prototype, 'responseText', {
316 get: function() {
317 if (!this.$responseIntercepted) return undefined;
318 if (this.$responseModified) return this.$newResponseText;
319 return this.xhr.responseText;
320 },
321 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200322 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100323}