blob: 6254fbf630c659d845be9e057c78f6284a0eede0 [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') {
45 p = classThis.responseModifier.intercept(proxyThis, this.response).then(() => {
46 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ínezc252ba02022-12-28 16:37:11 +010085 var body = utils.getResponseJSON(proxyThis);
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +010086 if (body !== undefined)
87 interceptors.forEach(i => {
88 utils.triggerEvent(i.eventName, body, proxyThis.$TWPTID);
89 });
90 });
91 }
92 break;
93
94 case 'setRequestHeader':
95 let header = arguments[0];
96 let value = arguments[1];
97 if ('Content-Type'.localeCompare(
98 header, undefined, {sensitivity: 'accent'}) == 0)
99 this.$isArrayProto = (value == 'application/json+protobuf');
100 break;
101
102 case 'send':
103 var interceptors = utils.matchInterceptors(
104 'request', this.$TWPTRequestURL || location.href);
105 if (interceptors.length > 0) {
106 let rawBody = arguments[0];
107 let body;
108 if (typeof (rawBody) === 'object' &&
109 (rawBody instanceof Object.getPrototypeOf(Uint8Array))) {
110 let dec = new TextDecoder('utf-8');
111 body = dec.decode(rawBody);
112 } else if (typeof (rawBody) === 'string') {
113 body = rawBody;
114 } else {
115 console.error(
116 'Unexpected type of request body (' + typeof (rawBody) +
117 ').',
118 this.$TWPTRequestURL);
119 return;
120 }
121
122 let JSONBody = JSON.parse(body);
123 if (this.$isArrayProto) JSONBody = correctArrayKeys(JSONBody);
124
125 interceptors.forEach(i => {
126 utils.triggerEvent(i.eventName, JSONBody, this.$TWPTID);
127 });
128 }
129 break;
130 }
131 return this.xhr[method].apply(this.xhr, arguments);
132 };
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200133 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200134
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100135 window.XMLHttpRequest.prototype.addEventListener = function() {
136 if (!kSpecialEvents.includes(arguments[0]))
137 return this.xhr.addEventListener.apply(this.xhr, arguments);
138
139 this.specialHandlers[arguments[0]].add(arguments);
140 };
141
142 window.XMLHttpRequest.prototype.removeEventListener = function(
143 type, callback, options) {
144 if (!kSpecialEvents.includes(type))
145 return this.xhr.removeEventListener.apply(this.xhr, arguments);
146
147 const flattenedOptions = flattenOptions(options);
148 for (const e of this.specialHandlers[type]) {
149 if (callback === e[1] && flattenOptions(e[2]) === flattenedOptions) {
150 return this.specialHandlers[type].delete(e);
151 }
152 }
153 };
154
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100155 const scalars = [
156 'onabort',
157 'onerror',
158 'onload',
159 'onloadstart',
160 'onloadend',
161 'onprogress',
162 'onreadystatechange',
163 'readyState',
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100164 'responseText',
165 'responseType',
166 'responseXML',
167 'status',
168 'statusText',
169 'upload',
170 'withCredentials',
171 'DONE',
172 'UNSENT',
173 'HEADERS_RECEIVED',
174 'LOADING',
175 'OPENED'
176 ];
177 scalars.forEach(scalar => {
178 Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
179 get: function() {
180 return this.xhr[scalar];
181 },
182 set: function(val) {
183 this.xhr[scalar] = val;
184 },
185 });
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200186 });
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100187
Adrià Vilanova Martínezac2a5612022-12-27 21:51:40 +0100188 Object.defineProperty(window.XMLHttpRequest.prototype, 'response', {
189 get: function() {
190 if (!this.$responseIntercepted) return undefined;
191 if (this.$responseModified) return this.$newResponse;
192 return this.xhr.response;
193 },
194 });
195 Object.defineProperty(window.XMLHttpRequest.prototype, 'originalResponse', {
196 get: function() {
197 return this.xhr.response;
198 },
199 });
200
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100201 return this;
Adrià Vilanova Martínez43ec2b92021-07-16 18:44:54 +0200202 }
Adrià Vilanova Martínez102d54b2022-12-18 11:12:11 +0100203}