happy-dom 7.7.2 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of happy-dom might be problematic. Click here for more details.

Files changed (97) hide show
  1. package/README.md +62 -27
  2. package/lib/async-task-manager/AsyncTaskManager.d.ts +3 -8
  3. package/lib/async-task-manager/AsyncTaskManager.js +21 -24
  4. package/lib/async-task-manager/AsyncTaskManager.js.map +1 -1
  5. package/lib/event/IEventListener.d.ts +1 -1
  6. package/lib/exception/DOMExceptionNameEnum.d.ts +4 -1
  7. package/lib/exception/DOMExceptionNameEnum.js +3 -0
  8. package/lib/exception/DOMExceptionNameEnum.js.map +1 -1
  9. package/lib/fetch/FetchHandler.d.ts +3 -2
  10. package/lib/fetch/FetchHandler.js +31 -3
  11. package/lib/fetch/FetchHandler.js.map +1 -1
  12. package/lib/fetch/RequestInfo.d.ts +5 -0
  13. package/lib/fetch/RequestInfo.js +3 -0
  14. package/lib/fetch/RequestInfo.js.map +1 -0
  15. package/lib/fetch/ResourceFetchHandler.d.ts +2 -2
  16. package/lib/fetch/ResourceFetchHandler.js +9 -8
  17. package/lib/fetch/ResourceFetchHandler.js.map +1 -1
  18. package/lib/index.d.ts +1 -2
  19. package/lib/index.js +3 -4
  20. package/lib/index.js.map +1 -1
  21. package/lib/location/Location.d.ts +2 -1
  22. package/lib/location/Location.js +4 -7
  23. package/lib/location/Location.js.map +1 -1
  24. package/lib/location/RelativeURL.d.ts +3 -1
  25. package/lib/location/RelativeURL.js +2 -11
  26. package/lib/location/RelativeURL.js.map +1 -1
  27. package/lib/nodes/document/Document.d.ts +12 -2
  28. package/lib/nodes/document/Document.js +16 -2
  29. package/lib/nodes/document/Document.js.map +1 -1
  30. package/lib/nodes/document/IDocument.d.ts +2 -0
  31. package/lib/nodes/html-link-element/HTMLLinkElement.js +5 -2
  32. package/lib/nodes/html-link-element/HTMLLinkElement.js.map +1 -1
  33. package/lib/nodes/html-script-element/HTMLScriptElement.js +1 -1
  34. package/lib/nodes/html-script-element/HTMLScriptElement.js.map +1 -1
  35. package/lib/nodes/html-script-element/ScriptUtility.js +4 -0
  36. package/lib/nodes/html-script-element/ScriptUtility.js.map +1 -1
  37. package/lib/window/IHappyDOMSettings.d.ts +9 -0
  38. package/lib/window/IHappyDOMSettings.js +3 -0
  39. package/lib/window/IHappyDOMSettings.js.map +1 -0
  40. package/lib/window/IWindow.d.ts +15 -6
  41. package/lib/window/Window.d.ts +18 -3
  42. package/lib/window/Window.js +26 -4
  43. package/lib/window/Window.js.map +1 -1
  44. package/lib/xml-http-request/XMLHttpRequest.d.ts +196 -0
  45. package/lib/xml-http-request/XMLHttpRequest.js +777 -0
  46. package/lib/xml-http-request/XMLHttpRequest.js.map +1 -0
  47. package/lib/xml-http-request/XMLHttpRequestCertificate.d.ts +5 -0
  48. package/lib/xml-http-request/XMLHttpRequestCertificate.js +55 -0
  49. package/lib/xml-http-request/XMLHttpRequestCertificate.js.map +1 -0
  50. package/lib/xml-http-request/XMLHttpRequestEventTarget.d.ts +15 -0
  51. package/lib/xml-http-request/XMLHttpRequestEventTarget.js +23 -0
  52. package/lib/xml-http-request/XMLHttpRequestEventTarget.js.map +1 -0
  53. package/lib/xml-http-request/XMLHttpRequestReadyStateEnum.d.ts +8 -0
  54. package/lib/xml-http-request/XMLHttpRequestReadyStateEnum.js +12 -0
  55. package/lib/xml-http-request/XMLHttpRequestReadyStateEnum.js.map +1 -0
  56. package/lib/xml-http-request/XMLHttpRequestUpload.d.ts +6 -0
  57. package/lib/xml-http-request/XMLHttpRequestUpload.js +13 -0
  58. package/lib/xml-http-request/XMLHttpRequestUpload.js.map +1 -0
  59. package/lib/xml-http-request/XMLHttpResponseTypeEnum.d.ts +8 -0
  60. package/lib/xml-http-request/XMLHttpResponseTypeEnum.js +12 -0
  61. package/lib/xml-http-request/XMLHttpResponseTypeEnum.js.map +1 -0
  62. package/lib/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.d.ts +15 -0
  63. package/lib/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.js +55 -0
  64. package/lib/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.js.map +1 -0
  65. package/lib/xml-http-request/utilities/XMLHttpRequestURLUtility.d.ts +35 -0
  66. package/lib/xml-http-request/utilities/XMLHttpRequestURLUtility.js +62 -0
  67. package/lib/xml-http-request/utilities/XMLHttpRequestURLUtility.js.map +1 -0
  68. package/package.json +2 -2
  69. package/src/async-task-manager/AsyncTaskManager.ts +24 -26
  70. package/src/event/IEventListener.ts +1 -1
  71. package/src/exception/DOMExceptionNameEnum.ts +4 -1
  72. package/src/fetch/FetchHandler.ts +40 -4
  73. package/src/fetch/RequestInfo.ts +6 -0
  74. package/src/fetch/ResourceFetchHandler.ts +10 -8
  75. package/src/index.ts +1 -2
  76. package/src/location/Location.ts +3 -3
  77. package/src/location/RelativeURL.ts +3 -14
  78. package/src/nodes/document/Document.ts +18 -2
  79. package/src/nodes/document/IDocument.ts +2 -0
  80. package/src/nodes/html-link-element/HTMLLinkElement.ts +7 -2
  81. package/src/nodes/html-script-element/HTMLScriptElement.ts +1 -1
  82. package/src/nodes/html-script-element/ScriptUtility.ts +8 -0
  83. package/src/window/IHappyDOMSettings.ts +9 -0
  84. package/src/window/IWindow.ts +15 -6
  85. package/src/window/Window.ts +36 -5
  86. package/src/xml-http-request/XMLHttpRequest.ts +998 -0
  87. package/src/xml-http-request/XMLHttpRequestCertificate.ts +52 -0
  88. package/src/xml-http-request/XMLHttpRequestEventTarget.ts +17 -0
  89. package/src/xml-http-request/XMLHttpRequestReadyStateEnum.ts +9 -0
  90. package/src/xml-http-request/XMLHttpRequestUpload.ts +6 -0
  91. package/src/xml-http-request/XMLHttpResponseTypeEnum.ts +9 -0
  92. package/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts +53 -0
  93. package/src/xml-http-request/utilities/XMLHttpRequestURLUtility.ts +64 -0
  94. package/lib/location/URL.d.ts +0 -53
  95. package/lib/location/URL.js +0 -96
  96. package/lib/location/URL.js.map +0 -1
  97. package/src/location/URL.ts +0 -102
@@ -0,0 +1,777 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const child_process_1 = __importDefault(require("child_process"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const https_1 = __importDefault(require("https"));
10
+ const XMLHttpRequestEventTarget_1 = __importDefault(require("./XMLHttpRequestEventTarget"));
11
+ const XMLHttpRequestReadyStateEnum_1 = __importDefault(require("./XMLHttpRequestReadyStateEnum"));
12
+ const Event_1 = __importDefault(require("../event/Event"));
13
+ const RelativeURL_1 = __importDefault(require("../location/RelativeURL"));
14
+ const XMLHttpRequestUpload_1 = __importDefault(require("./XMLHttpRequestUpload"));
15
+ const DOMException_1 = __importDefault(require("../exception/DOMException"));
16
+ const DOMExceptionNameEnum_1 = __importDefault(require("../exception/DOMExceptionNameEnum"));
17
+ const XMLHttpRequestURLUtility_1 = __importDefault(require("./utilities/XMLHttpRequestURLUtility"));
18
+ const ProgressEvent_1 = __importDefault(require("../event/events/ProgressEvent"));
19
+ const XMLHttpResponseTypeEnum_1 = __importDefault(require("./XMLHttpResponseTypeEnum"));
20
+ const XMLHttpRequestCertificate_1 = __importDefault(require("./XMLHttpRequestCertificate"));
21
+ const XMLHttpRequestSyncRequestScriptBuilder_1 = __importDefault(require("./utilities/XMLHttpRequestSyncRequestScriptBuilder"));
22
+ // These headers are not user setable.
23
+ // The following are allowed but banned in the spec:
24
+ // * User-agent
25
+ const FORBIDDEN_REQUEST_HEADERS = [
26
+ 'accept-charset',
27
+ 'accept-encoding',
28
+ 'access-control-request-headers',
29
+ 'access-control-request-method',
30
+ 'connection',
31
+ 'content-length',
32
+ 'content-transfer-encoding',
33
+ 'cookie',
34
+ 'cookie2',
35
+ 'date',
36
+ 'expect',
37
+ 'host',
38
+ 'keep-alive',
39
+ 'origin',
40
+ 'referer',
41
+ 'te',
42
+ 'trailer',
43
+ 'transfer-encoding',
44
+ 'upgrade',
45
+ 'via'
46
+ ];
47
+ // These request methods are not allowed
48
+ const FORBIDDEN_REQUEST_METHODS = ['TRACE', 'TRACK', 'CONNECT'];
49
+ /**
50
+ * XMLHttpRequest.
51
+ *
52
+ * Based on:
53
+ * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js
54
+ */
55
+ class XMLHttpRequest extends XMLHttpRequestEventTarget_1.default {
56
+ /**
57
+ * Constructor.
58
+ */
59
+ constructor() {
60
+ super();
61
+ // Public properties
62
+ this.upload = new XMLHttpRequestUpload_1.default();
63
+ // Private properties
64
+ this._ownerDocument = null;
65
+ this._state = {
66
+ incommingMessage: null,
67
+ response: null,
68
+ responseType: '',
69
+ responseText: '',
70
+ responseXML: null,
71
+ responseURL: '',
72
+ readyState: XMLHttpRequestReadyStateEnum_1.default.unsent,
73
+ asyncRequest: null,
74
+ asyncTaskID: null,
75
+ requestHeaders: {},
76
+ status: null,
77
+ statusText: null,
78
+ send: false,
79
+ error: false,
80
+ aborted: false
81
+ };
82
+ this._settings = {
83
+ method: null,
84
+ url: null,
85
+ async: true,
86
+ user: null,
87
+ password: null
88
+ };
89
+ this._ownerDocument = XMLHttpRequest._ownerDocument;
90
+ }
91
+ /**
92
+ * Returns the status.
93
+ *
94
+ * @returns Status.
95
+ */
96
+ get status() {
97
+ return this._state.status;
98
+ }
99
+ /**
100
+ * Returns the status text.
101
+ *
102
+ * @returns Status text.
103
+ */
104
+ get statusText() {
105
+ return this._state.statusText;
106
+ }
107
+ /**
108
+ * Returns the response URL.
109
+ *
110
+ * @returns Response URL.
111
+ */
112
+ get responseURL() {
113
+ return this._state.responseURL;
114
+ }
115
+ /**
116
+ * Returns the ready state.
117
+ *
118
+ * @returns Ready state.
119
+ */
120
+ get readyState() {
121
+ return this._state.readyState;
122
+ }
123
+ /**
124
+ * Get the response text.
125
+ *
126
+ * @throws {DOMException} If the response type is not text or empty.
127
+ * @returns The response text.
128
+ */
129
+ get responseText() {
130
+ if (this.responseType === XMLHttpResponseTypeEnum_1.default.text || this.responseType === '') {
131
+ return this._state.responseText;
132
+ }
133
+ throw new DOMException_1.default(`Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '${this.responseType}').`, DOMExceptionNameEnum_1.default.invalidStateError);
134
+ }
135
+ /**
136
+ * Get the responseXML.
137
+ *
138
+ * @throws {DOMException} If the response type is not text or empty.
139
+ * @returns Response XML.
140
+ */
141
+ get responseXML() {
142
+ if (this.responseType === XMLHttpResponseTypeEnum_1.default.document || this.responseType === '') {
143
+ return this._state.responseXML;
144
+ }
145
+ throw new DOMException_1.default(`Failed to read the 'responseXML' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'document' (was '${this.responseType}').`, DOMExceptionNameEnum_1.default.invalidStateError);
146
+ }
147
+ /**
148
+ * Set response type.
149
+ *
150
+ * @param type Response type.
151
+ * @throws {DOMException} If the state is not unsent or opened.
152
+ * @throws {DOMException} If the request is synchronous.
153
+ */
154
+ set responseType(type) {
155
+ // ResponseType can only be set when the state is unsent or opened.
156
+ if (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.opened &&
157
+ this.readyState !== XMLHttpRequestReadyStateEnum_1.default.unsent) {
158
+ throw new DOMException_1.default(`Failed to set the 'responseType' property on 'XMLHttpRequest': The object's state must be OPENED or UNSENT.`, DOMExceptionNameEnum_1.default.invalidStateError);
159
+ }
160
+ // Sync requests can only have empty string or 'text' as response type.
161
+ if (!this._settings.async) {
162
+ throw new DOMException_1.default(`Failed to set the 'responseType' property on 'XMLHttpRequest': The response type cannot be changed for synchronous requests made from a document.`, DOMExceptionNameEnum_1.default.invalidStateError);
163
+ }
164
+ this._state.responseType = type;
165
+ }
166
+ /**
167
+ * Get response Type.
168
+ *
169
+ * @returns Response type.
170
+ */
171
+ get responseType() {
172
+ return this._state.responseType;
173
+ }
174
+ /**
175
+ * Opens the connection.
176
+ *
177
+ * @param method Connection method (eg GET, POST).
178
+ * @param url URL for the connection.
179
+ * @param [async=true] Asynchronous connection.
180
+ * @param [user] Username for basic authentication (optional).
181
+ * @param [password] Password for basic authentication (optional).
182
+ */
183
+ open(method, url, async = true, user, password) {
184
+ this.abort();
185
+ this._state.aborted = false;
186
+ this._state.error = false;
187
+ const upperMethod = method.toUpperCase();
188
+ // Check for valid request method
189
+ if (FORBIDDEN_REQUEST_METHODS.includes(upperMethod)) {
190
+ throw new DOMException_1.default('Request method not allowed', DOMExceptionNameEnum_1.default.securityError);
191
+ }
192
+ // Check responseType.
193
+ if (!async && !!this.responseType && this.responseType !== XMLHttpResponseTypeEnum_1.default.text) {
194
+ throw new DOMException_1.default(`Failed to execute 'open' on 'XMLHttpRequest': Synchronous requests from a document must not set a response type.`, DOMExceptionNameEnum_1.default.invalidAccessError);
195
+ }
196
+ this._settings = {
197
+ method: upperMethod,
198
+ url: url,
199
+ async: async,
200
+ user: user || null,
201
+ password: password || null
202
+ };
203
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.opened);
204
+ }
205
+ /**
206
+ * Sets a header for the request.
207
+ *
208
+ * @param header Header name
209
+ * @param value Header value
210
+ * @returns Header added.
211
+ */
212
+ setRequestHeader(header, value) {
213
+ if (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.opened) {
214
+ throw new DOMException_1.default(`Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.`, DOMExceptionNameEnum_1.default.invalidStateError);
215
+ }
216
+ const lowerHeader = header.toLowerCase();
217
+ if (FORBIDDEN_REQUEST_HEADERS.includes(lowerHeader)) {
218
+ return false;
219
+ }
220
+ if (this._state.send) {
221
+ throw new DOMException_1.default(`Failed to execute 'setRequestHeader' on 'XMLHttpRequest': Request is in progress.`, DOMExceptionNameEnum_1.default.invalidStateError);
222
+ }
223
+ this._state.requestHeaders[lowerHeader] = value;
224
+ return true;
225
+ }
226
+ /**
227
+ * Gets a header from the server response.
228
+ *
229
+ * @param header header Name of header to get.
230
+ * @returns string Text of the header or null if it doesn't exist.
231
+ */
232
+ getResponseHeader(header) {
233
+ const lowerHeader = header.toLowerCase();
234
+ // Cookie headers are excluded for security reasons as per spec.
235
+ if (typeof header === 'string' &&
236
+ header !== 'set-cookie' &&
237
+ header !== 'set-cookie2' &&
238
+ this.readyState > XMLHttpRequestReadyStateEnum_1.default.opened &&
239
+ this._state.incommingMessage.headers[lowerHeader] &&
240
+ !this._state.error) {
241
+ return this._state.incommingMessage.headers[lowerHeader];
242
+ }
243
+ return null;
244
+ }
245
+ /**
246
+ * Gets all the response headers.
247
+ *
248
+ * @returns A string with all response headers separated by CR+LF.
249
+ */
250
+ getAllResponseHeaders() {
251
+ if (this.readyState < XMLHttpRequestReadyStateEnum_1.default.headersRecieved || this._state.error) {
252
+ return '';
253
+ }
254
+ const result = [];
255
+ for (const name of Object.keys(this._state.incommingMessage.headers)) {
256
+ // Cookie headers are excluded for security reasons as per spec.
257
+ if (name !== 'set-cookie' && name !== 'set-cookie2') {
258
+ result.push(`${name}: ${this._state.incommingMessage.headers[name]}`);
259
+ }
260
+ }
261
+ return result.join('\r\n');
262
+ }
263
+ /**
264
+ * Sends the request to the server.
265
+ *
266
+ * @param data Optional data to send as request body.
267
+ */
268
+ send(data) {
269
+ var _a, _b;
270
+ if (this.readyState != XMLHttpRequestReadyStateEnum_1.default.opened) {
271
+ throw new DOMException_1.default(`Failed to execute 'send' on 'XMLHttpRequest': Connection must be opened before send() is called.`, DOMExceptionNameEnum_1.default.invalidStateError);
272
+ }
273
+ if (this._state.send) {
274
+ throw new DOMException_1.default(`Failed to execute 'send' on 'XMLHttpRequest': Send has already been called.`, DOMExceptionNameEnum_1.default.invalidStateError);
275
+ }
276
+ const { location } = this._ownerDocument.defaultView;
277
+ const url = RelativeURL_1.default.getAbsoluteURL(location, this._settings.url);
278
+ // Security check.
279
+ if (url.protocol === 'http:' && location.protocol === 'https:') {
280
+ throw new DOMException_1.default(`Mixed Content: The page at '${location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${url.href}'. This request has been blocked; the content must be served over HTTPS.`, DOMExceptionNameEnum_1.default.securityError);
281
+ }
282
+ // Load files off the local filesystem (file://)
283
+ if (XMLHttpRequestURLUtility_1.default.isLocal(url)) {
284
+ if (!this._ownerDocument.defaultView.happyDOM.settings.enableFileSystemHttpRequests) {
285
+ throw new DOMException_1.default('File system is disabled by default for security reasons. To enable it, set the "window.happyDOM.settings.enableFileSystemHttpRequests" option to true.', DOMExceptionNameEnum_1.default.securityError);
286
+ }
287
+ if (this._settings.method !== 'GET') {
288
+ throw new DOMException_1.default('Failed to send local file system request. Only "GET" method is supported for local file system requests.', DOMExceptionNameEnum_1.default.notSupportedError);
289
+ }
290
+ if (this._settings.async) {
291
+ this._sendLocalAsyncRequest(url).catch((error) => this._onError(error));
292
+ }
293
+ else {
294
+ this._sendLocalSyncRequest(url);
295
+ }
296
+ return;
297
+ }
298
+ // TODO: CORS check.
299
+ const host = XMLHttpRequestURLUtility_1.default.getHost(url);
300
+ const ssl = XMLHttpRequestURLUtility_1.default.isSSL(url);
301
+ // Default to port 80. If accessing localhost on another port be sure
302
+ // To use http://localhost:port/path
303
+ const port = Number(url.port) || (ssl ? 443 : 80);
304
+ // Add query string if one is used
305
+ const uri = url.pathname + (url.search ? url.search : '');
306
+ // Set the Host header or the server may reject the request
307
+ this._state.requestHeaders['host'] = host;
308
+ if (!((ssl && port === 443) || port === 80)) {
309
+ this._state.requestHeaders['host'] += ':' + url.port;
310
+ }
311
+ // Set Basic Auth if necessary
312
+ if (this._settings.user) {
313
+ (_a = this._settings).password ?? (_a.password = '');
314
+ const authBuffer = Buffer.from(this._settings.user + ':' + this._settings.password);
315
+ this._state.requestHeaders['authorization'] = 'Basic ' + authBuffer.toString('base64');
316
+ }
317
+ // Set the Content-Length header if method is POST
318
+ switch (this._settings.method) {
319
+ case 'GET':
320
+ case 'HEAD':
321
+ data = null;
322
+ break;
323
+ case 'POST':
324
+ (_b = this._state.requestHeaders)['content-type'] ?? (_b['content-type'] = 'text/plain;charset=UTF-8');
325
+ if (data) {
326
+ this._state.requestHeaders['content-length'] = Buffer.isBuffer(data)
327
+ ? data.length
328
+ : Buffer.byteLength(data);
329
+ }
330
+ else {
331
+ this._state.requestHeaders['content-length'] = 0;
332
+ }
333
+ break;
334
+ default:
335
+ break;
336
+ }
337
+ const options = {
338
+ host: host,
339
+ port: port,
340
+ path: uri,
341
+ method: this._settings.method,
342
+ headers: { ...this._getDefaultRequestHeaders(), ...this._state.requestHeaders },
343
+ agent: false,
344
+ rejectUnauthorized: true,
345
+ key: ssl ? XMLHttpRequestCertificate_1.default.key : null,
346
+ cert: ssl ? XMLHttpRequestCertificate_1.default.cert : null
347
+ };
348
+ // Reset error flag
349
+ this._state.error = false;
350
+ // Handle async requests
351
+ if (this._settings.async) {
352
+ this._sendAsyncRequest(options, ssl, data).catch((error) => this._onError(error));
353
+ }
354
+ else {
355
+ this._sendSyncRequest(options, ssl, data);
356
+ }
357
+ }
358
+ /**
359
+ * Aborts a request.
360
+ */
361
+ abort() {
362
+ if (this._state.asyncRequest) {
363
+ this._state.asyncRequest.destroy();
364
+ this._state.asyncRequest = null;
365
+ }
366
+ this._state.status = null;
367
+ this._state.statusText = null;
368
+ this._state.requestHeaders = {};
369
+ this._state.responseText = '';
370
+ this._state.responseXML = null;
371
+ this._state.aborted = true;
372
+ this._state.error = true;
373
+ if (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.unsent &&
374
+ (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.opened || this._state.send) &&
375
+ this.readyState !== XMLHttpRequestReadyStateEnum_1.default.done) {
376
+ this._state.send = false;
377
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
378
+ }
379
+ this._state.readyState = XMLHttpRequestReadyStateEnum_1.default.unsent;
380
+ if (this._state.asyncTaskID !== null) {
381
+ this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
382
+ }
383
+ }
384
+ /**
385
+ * Changes readyState and calls onreadystatechange.
386
+ *
387
+ * @param state
388
+ */
389
+ _setState(state) {
390
+ if (this.readyState === state ||
391
+ (this.readyState === XMLHttpRequestReadyStateEnum_1.default.unsent && this._state.aborted)) {
392
+ return;
393
+ }
394
+ this._state.readyState = state;
395
+ if (this._settings.async ||
396
+ this.readyState < XMLHttpRequestReadyStateEnum_1.default.opened ||
397
+ this.readyState === XMLHttpRequestReadyStateEnum_1.default.done) {
398
+ this.dispatchEvent(new Event_1.default('readystatechange'));
399
+ }
400
+ if (this.readyState === XMLHttpRequestReadyStateEnum_1.default.done) {
401
+ let fire;
402
+ if (this._state.aborted) {
403
+ fire = new Event_1.default('abort');
404
+ }
405
+ else if (this._state.error) {
406
+ fire = new Event_1.default('error');
407
+ }
408
+ else {
409
+ fire = new Event_1.default('load');
410
+ }
411
+ this.dispatchEvent(fire);
412
+ this.dispatchEvent(new Event_1.default('loadend'));
413
+ }
414
+ }
415
+ /**
416
+ * Default request headers.
417
+ *
418
+ * @returns Default request headers.
419
+ */
420
+ _getDefaultRequestHeaders() {
421
+ const { location, navigator, document } = this._ownerDocument.defaultView;
422
+ return {
423
+ accept: '*/*',
424
+ referer: location.href,
425
+ 'user-agent': navigator.userAgent,
426
+ cookie: document._cookie.getCookiesString(location, false)
427
+ };
428
+ }
429
+ /**
430
+ * Sends a synchronous request.
431
+ *
432
+ * @param options
433
+ * @param ssl
434
+ * @param data
435
+ */
436
+ _sendSyncRequest(options, ssl, data) {
437
+ const scriptString = XMLHttpRequestSyncRequestScriptBuilder_1.default.getScript(options, ssl, data);
438
+ // Start the other Node Process, executing this string
439
+ const content = child_process_1.default.execFileSync(process.argv[0], ['-e', scriptString], {
440
+ encoding: 'buffer',
441
+ maxBuffer: 1024 * 1024 * 1024 // TODO: Consistent buffer size: 1GB.
442
+ });
443
+ // If content length is 0, then there was an error
444
+ if (!content.length) {
445
+ throw new DOMException_1.default('Synchronous request failed', DOMExceptionNameEnum_1.default.networkError);
446
+ }
447
+ const { error, data: response } = JSON.parse(content.toString());
448
+ if (error) {
449
+ this._onError(error);
450
+ }
451
+ if (response) {
452
+ this._state.incommingMessage = {
453
+ statusCode: response.statusCode,
454
+ headers: response.headers
455
+ };
456
+ this._state.status = response.statusCode;
457
+ this._state.statusText = response.statusMessage;
458
+ // Sync responseType === ''
459
+ this._state.response = response.text;
460
+ this._state.responseText = response.text;
461
+ this._state.responseXML = null;
462
+ this._state.responseURL = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._settings.url).href;
463
+ // Set Cookies.
464
+ this._setCookies(this._state.incommingMessage.headers);
465
+ // Redirect.
466
+ if (this._state.incommingMessage.statusCode === 301 ||
467
+ this._state.incommingMessage.statusCode === 302 ||
468
+ this._state.incommingMessage.statusCode === 303 ||
469
+ this._state.incommingMessage.statusCode === 307) {
470
+ const redirectUrl = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._state.incommingMessage.headers['location']);
471
+ ssl = redirectUrl.protocol === 'https:';
472
+ this._settings.url = redirectUrl.href;
473
+ // Recursive call.
474
+ this._sendSyncRequest(Object.assign(options, {
475
+ host: redirectUrl.host,
476
+ path: redirectUrl.pathname + (redirectUrl.search ?? ''),
477
+ port: redirectUrl.port || (ssl ? 443 : 80),
478
+ method: this._state.incommingMessage.statusCode === 303 ? 'GET' : this._settings.method,
479
+ headers: Object.assign(options.headers, {
480
+ referer: redirectUrl.origin,
481
+ host: redirectUrl.host
482
+ })
483
+ }), ssl, data);
484
+ }
485
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
486
+ }
487
+ }
488
+ /**
489
+ * Sends an async request.
490
+ *
491
+ * @param options
492
+ * @param ssl
493
+ * @param data
494
+ */
495
+ _sendAsyncRequest(options, ssl, data) {
496
+ return new Promise((resolve) => {
497
+ // Starts async task in Happy DOM
498
+ this._state.asyncTaskID = this._ownerDocument.defaultView.happyDOM.asyncTaskManager.startTask(this.abort.bind(this));
499
+ // Use the proper protocol
500
+ const sendRequest = ssl ? https_1.default.request : http_1.default.request;
501
+ // Request is being sent, set send flag
502
+ this._state.send = true;
503
+ // As per spec, this is called here for historical reasons.
504
+ this.dispatchEvent(new Event_1.default('readystatechange'));
505
+ // Create the request
506
+ this._state.asyncRequest = sendRequest(options, async (response) => {
507
+ await this._onAsyncResponse(response, options, ssl, data);
508
+ resolve();
509
+ // Ends async task in Happy DOM
510
+ this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
511
+ }).on('error', (error) => {
512
+ this._onError(error);
513
+ resolve();
514
+ // Ends async task in Happy DOM
515
+ this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
516
+ });
517
+ // Node 0.4 and later won't accept empty data. Make sure it's needed.
518
+ if (data) {
519
+ this._state.asyncRequest.write(data);
520
+ }
521
+ this._state.asyncRequest.end();
522
+ this.dispatchEvent(new Event_1.default('loadstart'));
523
+ });
524
+ }
525
+ /**
526
+ * Handles an async response.
527
+ *
528
+ * @param options Options.
529
+ * @param ssl SSL.
530
+ * @param data Data.
531
+ * @param response Response.
532
+ * @returns Promise.
533
+ */
534
+ _onAsyncResponse(response, options, ssl, data) {
535
+ return new Promise((resolve) => {
536
+ // Set response var to the response we got back
537
+ // This is so it remains accessable outside this scope
538
+ this._state.incommingMessage = response;
539
+ // Set Cookies
540
+ this._setCookies(this._state.incommingMessage.headers);
541
+ // Check for redirect
542
+ // @TODO Prevent looped redirects
543
+ if (this._state.incommingMessage.statusCode === 301 ||
544
+ this._state.incommingMessage.statusCode === 302 ||
545
+ this._state.incommingMessage.statusCode === 303 ||
546
+ this._state.incommingMessage.statusCode === 307) {
547
+ // TODO: redirect url protocol change.
548
+ // Change URL to the redirect location
549
+ this._settings.url = this._state.incommingMessage.headers.location;
550
+ // Parse the new URL.
551
+ const redirectUrl = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._settings.url);
552
+ this._settings.url = redirectUrl.href;
553
+ ssl = redirectUrl.protocol === 'https:';
554
+ // Issue the new request
555
+ this._sendAsyncRequest({
556
+ ...options,
557
+ host: redirectUrl.hostname,
558
+ port: redirectUrl.port,
559
+ path: redirectUrl.pathname + (redirectUrl.search ?? ''),
560
+ method: this._state.incommingMessage.statusCode === 303 ? 'GET' : this._settings.method,
561
+ headers: { ...options.headers, referer: redirectUrl.origin, host: redirectUrl.host }
562
+ }, ssl, data);
563
+ // @TODO Check if an XHR event needs to be fired here
564
+ return;
565
+ }
566
+ if (this._state.incommingMessage && this._state.incommingMessage.setEncoding) {
567
+ this._state.incommingMessage.setEncoding('utf-8');
568
+ }
569
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.headersRecieved);
570
+ this._state.status = this._state.incommingMessage.statusCode;
571
+ this._state.statusText = this._state.incommingMessage.statusMessage;
572
+ // Initialize response.
573
+ let tempResponse = Buffer.from(new Uint8Array(0));
574
+ this._state.incommingMessage.on('data', (chunk) => {
575
+ // Make sure there's some data
576
+ if (chunk) {
577
+ tempResponse = Buffer.concat([tempResponse, Buffer.from(chunk)]);
578
+ }
579
+ // Don't emit state changes if the connection has been aborted.
580
+ if (this._state.send) {
581
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.loading);
582
+ }
583
+ const contentLength = Number(this._state.incommingMessage.headers['content-length']);
584
+ this.dispatchEvent(new ProgressEvent_1.default('progress', {
585
+ lengthComputable: isNaN(contentLength) ? false : true,
586
+ loaded: tempResponse.length,
587
+ total: isNaN(contentLength) ? 0 : contentLength
588
+ }));
589
+ });
590
+ this._state.incommingMessage.on('end', () => {
591
+ if (this._state.send) {
592
+ // The sendFlag needs to be set before setState is called. Otherwise, if we are chaining callbacks
593
+ // There can be a timing issue (the callback is called and a new call is made before the flag is reset).
594
+ this._state.send = false;
595
+ // Set response according to responseType.
596
+ const { response, responseXML, responseText } = this._parseResponseData(tempResponse);
597
+ this._state.response = response;
598
+ this._state.responseXML = responseXML;
599
+ this._state.responseText = responseText;
600
+ this._state.responseURL = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._settings.url).href;
601
+ // Discard the 'end' event if the connection has been aborted
602
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
603
+ }
604
+ resolve();
605
+ });
606
+ this._state.incommingMessage.on('error', (error) => {
607
+ this._onError(error);
608
+ resolve();
609
+ });
610
+ });
611
+ }
612
+ /**
613
+ * Sends a local file system async request.
614
+ *
615
+ * @param url URL.
616
+ * @returns Promise.
617
+ */
618
+ async _sendLocalAsyncRequest(url) {
619
+ this._state.asyncTaskID = this._ownerDocument.defaultView.happyDOM.asyncTaskManager.startTask(this.abort.bind(this));
620
+ let data;
621
+ try {
622
+ data = await fs_1.default.promises.readFile(decodeURI(url.pathname.slice(1)));
623
+ }
624
+ catch (error) {
625
+ this._onError(error);
626
+ }
627
+ const dataLength = data.length;
628
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.loading);
629
+ this.dispatchEvent(new ProgressEvent_1.default('progress', {
630
+ lengthComputable: true,
631
+ loaded: dataLength,
632
+ total: dataLength
633
+ }));
634
+ if (data) {
635
+ this._parseLocalRequestData(data);
636
+ }
637
+ this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
638
+ }
639
+ /**
640
+ * Sends a local file system synchronous request.
641
+ *
642
+ * @param url URL.
643
+ */
644
+ _sendLocalSyncRequest(url) {
645
+ let data;
646
+ try {
647
+ data = fs_1.default.readFileSync(decodeURI(url.pathname.slice(1)));
648
+ }
649
+ catch (error) {
650
+ this._onError(error);
651
+ }
652
+ if (data) {
653
+ this._parseLocalRequestData(data);
654
+ }
655
+ }
656
+ /**
657
+ * Parses local request data.
658
+ *
659
+ * @param data Data.
660
+ */
661
+ _parseLocalRequestData(data) {
662
+ this._state.status = 200;
663
+ this._state.statusText = 'OK';
664
+ const { response, responseXML, responseText } = this._parseResponseData(data);
665
+ this._state.response = response;
666
+ this._state.responseXML = responseXML;
667
+ this._state.responseText = responseText;
668
+ this._state.responseURL = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._settings.url).href;
669
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
670
+ }
671
+ /**
672
+ * Returns response based to the "responseType" property.
673
+ *
674
+ * @param data Data.
675
+ * @returns Parsed response.
676
+ */
677
+ _parseResponseData(data) {
678
+ switch (this.responseType) {
679
+ case XMLHttpResponseTypeEnum_1.default.arraybuffer:
680
+ // See: https://github.com/jsdom/jsdom/blob/c3c421c364510e053478520500bccafd97f5fa39/lib/jsdom/living/helpers/binary-data.js
681
+ const newAB = new ArrayBuffer(data.length);
682
+ const view = new Uint8Array(newAB);
683
+ view.set(data);
684
+ return {
685
+ response: view,
686
+ responseText: null,
687
+ responseXML: null
688
+ };
689
+ case XMLHttpResponseTypeEnum_1.default.blob:
690
+ try {
691
+ return {
692
+ response: new this._ownerDocument.defaultView.Blob([new Uint8Array(data)], {
693
+ type: this.getResponseHeader('content-type') || ''
694
+ }),
695
+ responseText: null,
696
+ responseXML: null
697
+ };
698
+ }
699
+ catch (e) {
700
+ return { response: null, responseText: null, responseXML: null };
701
+ }
702
+ case XMLHttpResponseTypeEnum_1.default.document:
703
+ const window = this._ownerDocument.defaultView;
704
+ const happyDOMSettings = window.happyDOM.settings;
705
+ let response;
706
+ // Temporary disables unsecure features.
707
+ window.happyDOM.settings = {
708
+ ...happyDOMSettings,
709
+ enableFileSystemHttpRequests: false,
710
+ disableJavaScriptEvaluation: true,
711
+ disableCSSFileLoading: true,
712
+ disableJavaScriptFileLoading: true
713
+ };
714
+ const domParser = new window.DOMParser();
715
+ try {
716
+ response = domParser.parseFromString(data.toString(), 'text/xml');
717
+ }
718
+ catch (e) {
719
+ return { response: null, responseText: null, responseXML: null };
720
+ }
721
+ // Restores unsecure features.
722
+ window.happyDOM.settings = happyDOMSettings;
723
+ return { response, responseText: null, responseXML: response };
724
+ case XMLHttpResponseTypeEnum_1.default.json:
725
+ try {
726
+ return {
727
+ response: JSON.parse(data.toString()),
728
+ responseText: null,
729
+ responseXML: null
730
+ };
731
+ }
732
+ catch (e) {
733
+ return { response: null, responseText: null, responseXML: null };
734
+ }
735
+ case XMLHttpResponseTypeEnum_1.default.text:
736
+ case '':
737
+ default:
738
+ return {
739
+ response: data.toString(),
740
+ responseText: data.toString(),
741
+ responseXML: null
742
+ };
743
+ }
744
+ }
745
+ /**
746
+ * Set Cookies from response headers.
747
+ *
748
+ * @param headers String array.
749
+ */
750
+ _setCookies(headers) {
751
+ for (const cookie of [...(headers['set-cookie'] ?? []), ...(headers['set-cookie2'] ?? [])]) {
752
+ this._ownerDocument.defaultView.document._cookie.setCookiesString(cookie);
753
+ }
754
+ }
755
+ /**
756
+ * Called when an error is encountered to deal with it.
757
+ *
758
+ * @param error Error.
759
+ */
760
+ _onError(error) {
761
+ this._state.status = 0;
762
+ this._state.statusText = error.toString();
763
+ this._state.responseText = error instanceof Error ? error.stack : '';
764
+ this._state.error = true;
765
+ this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
766
+ }
767
+ }
768
+ exports.default = XMLHttpRequest;
769
+ // Owner document is set by a sub-class in the Window constructor
770
+ XMLHttpRequest._ownerDocument = null;
771
+ // Constants
772
+ XMLHttpRequest.UNSENT = XMLHttpRequestReadyStateEnum_1.default.unsent;
773
+ XMLHttpRequest.OPENED = XMLHttpRequestReadyStateEnum_1.default.opened;
774
+ XMLHttpRequest.HEADERS_RECEIVED = XMLHttpRequestReadyStateEnum_1.default.headersRecieved;
775
+ XMLHttpRequest.LOADING = XMLHttpRequestReadyStateEnum_1.default.loading;
776
+ XMLHttpRequest.DONE = XMLHttpRequestReadyStateEnum_1.default.done;
777
+ //# sourceMappingURL=XMLHttpRequest.js.map