blob: 2d6d03fd2ef83d719fc9f1864dad0f889c127667 [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ínezac2a5612022-12-27 21:51:40 +010010const kCheckInterceptionOptions = {
11 interval: 50,
12 timeout: 100 * 1000,
13};
avm999631f50f6f2021-08-12 23:04:41 +020014
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010015function flattenOptions(options) {
16 if (typeof options === 'boolean') return options;
17 if (options) return options['capture'];
18 return undefined;
19}
20
21// Slightly based in https://stackoverflow.com/a/24561614.
Adrià Vilanova Martínez4757ae82022-12-28 16:41:37 +010022export default class XHRProxy {
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010023 constructor() {
24 this.originalXMLHttpRequest = window.XMLHttpRequest;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010025 const classThis = this;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +020026
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010027 this.messageID = 0;
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010028 this.responseModifier = new ResponseModifier();
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010029
30 window.XMLHttpRequest = function() {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010031 this.xhr = new classThis.originalXMLHttpRequest();
32 this.$TWPTID = classThis.messageID++;
33 this.$responseModified = false;
34 this.$responseIntercepted = false;
35 this.specialHandlers = {
36 load: new Set(),
37 loadend: new Set(),
38 };
39
40 const proxyThis = this;
41 kSpecialEvents.forEach(eventName => {
42 this.xhr.addEventListener(eventName, function() {
43 let p;
44 if (eventName === 'load') {
Adrià Vilanova Martínezb47ec062023-01-15 17:43:26 +010045 p = classThis.responseModifier.intercept(proxyThis).then(() => {
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010046 proxyThis.$responseIntercepted = true;
47 });
48 } else {
49 p = waitFor(() => {
50 if (proxyThis.$responseIntercepted) return Promise.resolve();
51 return Promise.reject();
52 }, kCheckInterceptionOptions);
53 }
54
55 p.then(() => {
56 for (const e of proxyThis.specialHandlers[eventName]) {
57 e[1](arguments);
58 }
59 });
60 });
61 });
62 kErrorEvents.forEach(eventName => {
63 this.xhr.addEventListener(eventName, function() {
64 proxyThis.$responseIntercepted = true;
65 });
66 });
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010067 };
68
69 const methods = [
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +010070 'open', 'abort', 'setRequestHeader', 'send', 'getResponseHeader',
71 'getAllResponseHeaders', 'dispatchEvent', 'overrideMimeType'
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010072 ];
73 methods.forEach(method => {
74 window.XMLHttpRequest.prototype[method] = function() {
75 const proxyThis = this;
76
77 switch (method) {
78 case 'open':
79 this.$TWPTRequestURL = arguments[1] || location.href;
80
81 var interceptors =
82 utils.matchInterceptors('response', this.$TWPTRequestURL);
83 if (interceptors.length > 0) {
84 this.xhr.addEventListener('load', function() {
Adrià Vilanova Martínezb47ec062023-01-15 17:43:26 +010085 var body = utils.getResponseJSON({
86 responseType: proxyThis.xhr.responseType,
87 response: proxyThis.xhr.response,
88 $TWPTRequestURL: proxyThis.$TWPTRequestURL,
89 $isArrayProto: proxyThis.$isArrayProto,
90 });
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010091 if (body !== undefined)
92 interceptors.forEach(i => {
93 utils.triggerEvent(i.eventName, body, proxyThis.$TWPTID);
94 });
95 });
96 }
97 break;
98
99 case 'setRequestHeader':
100 let header = arguments[0];
101 let value = arguments[1];
102 if ('Content-Type'.localeCompare(
103 header, undefined, {sensitivity: 'accent'}) == 0)
104 this.$isArrayProto = (value == 'application/json+protobuf');
105 break;
106
107 case 'send':
108 var interceptors = utils.matchInterceptors(
109 'request', this.$TWPTRequestURL || location.href);
110 if (interceptors.length > 0) {
111 let rawBody = arguments[0];
112 let body;
113 if (typeof (rawBody) === 'object' &&
114 (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
115 let dec = new TextDecoder('utf-8');
116 body = dec.decode(rawBody);
117 } else if (typeof (rawBody) === 'string') {
118 body = rawBody;
119 } else {
120 console.error(
121 'Unexpected type of request body (' + typeof (rawBody) +
122 ').',
123 this.$TWPTRequestURL);
124 return;
125 }
126
127 let JSONBody = JSON.parse(body);
128 if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
129
130 interceptors.forEach(i => {
131 utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
132 });
133 }
134 break;
135 }
136 return this.xhr[method].apply(this.xhr, arguments);
137 };
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200138 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200139
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100140 window.XMLHttpRequest.prototype.addEventListener = function() {
141 if (!kSpecialEvents.includes(arguments[0]))
142 return this.xhr.addEventListener.apply(this.xhr, arguments);
143
144 this.specialHandlers[arguments[0]].add(arguments);
145 };
146
147 window.XMLHttpRequest.prototype.removeEventListener = function(
148 type, callback, options) {
149 if (!kSpecialEvents.includes(type))
150 return this.xhr.removeEventListener.apply(this.xhr, arguments);
151
152 const flattenedOptions = flattenOptions(options);
153 for (const e of this.specialHandlers[type]) {
154 if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
155 return this.specialHandlers[type].delete(e);
156 }
157 }
158 };
159
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100160 const scalars = [
161 'onabort',
162 'onerror',
163 'onload',
164 'onloadstart',
165 'onloadend',
166 'onprogress',
167 'onreadystatechange',
168 'readyState',
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100169 'responseType',
170 'responseXML',
171 'status',
172 'statusText',
173 'upload',
174 'withCredentials',
175 'DONE',
176 'UNSENT',
177 'HEADERS_RECEIVED',
178 'LOADING',
179 'OPENED'
180 ];
181 scalars.forEach(scalar => {
182 Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
183 get: function() {
184 return this.xhr[scalar];
185 },
186 set: function(val) {
187 this.xhr[scalar] = val;
188 },
189 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200190 });
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100191
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100192 Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
193 get: function() {
194 if (!this.$responseIntercepted) return undefined;
195 if (this.$responseModified) return this.$newResponse;
196 return this.xhr.response;
197 },
198 });
199 Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
200 get: function() {
201 return this.xhr.response;
202 },
203 });
Adrià Vilanova Martínez8a17fa82023-02-04 19:19:27 +0100204 Object.defineProperty(window.XMLHttpRequest.prototype, 'responseText', {
205 get: function() {
206 if (!this.$responseIntercepted) return undefined;
207 if (this.$responseModified) return this.$newResponseText;
208 return this.xhr.responseText;
209 },
210 });
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100211
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100212 return this;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200213 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100214}