happy-dom 7.7.2 → 7.8.1

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