版博士V2.0程序
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

797 строки
34 KiB

  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. const iconv_lite_1 = __importDefault(require("iconv-lite"));
  23. // These headers are not user setable.
  24. // The following are allowed but banned in the spec:
  25. // * User-agent
  26. const FORBIDDEN_REQUEST_HEADERS = [
  27. 'accept-charset',
  28. 'accept-encoding',
  29. 'access-control-request-headers',
  30. 'access-control-request-method',
  31. 'connection',
  32. 'content-length',
  33. 'content-transfer-encoding',
  34. 'cookie',
  35. 'cookie2',
  36. 'date',
  37. 'expect',
  38. 'host',
  39. 'keep-alive',
  40. 'origin',
  41. 'referer',
  42. 'te',
  43. 'trailer',
  44. 'transfer-encoding',
  45. 'upgrade',
  46. 'via'
  47. ];
  48. // These request methods are not allowed
  49. const FORBIDDEN_REQUEST_METHODS = ['TRACE', 'TRACK', 'CONNECT'];
  50. // Match Content-Type header charset
  51. const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i;
  52. /**
  53. * XMLHttpRequest.
  54. *
  55. * Based on:
  56. * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js
  57. */
  58. class XMLHttpRequest extends XMLHttpRequestEventTarget_1.default {
  59. /**
  60. * Constructor.
  61. */
  62. constructor() {
  63. super();
  64. // Public properties
  65. this.upload = new XMLHttpRequestUpload_1.default();
  66. // Private properties
  67. this._ownerDocument = null;
  68. this._state = {
  69. incommingMessage: null,
  70. response: null,
  71. responseType: '',
  72. responseText: '',
  73. responseXML: null,
  74. responseURL: '',
  75. readyState: XMLHttpRequestReadyStateEnum_1.default.unsent,
  76. asyncRequest: null,
  77. asyncTaskID: null,
  78. requestHeaders: {},
  79. status: null,
  80. statusText: null,
  81. send: false,
  82. error: false,
  83. aborted: false
  84. };
  85. this._settings = {
  86. method: null,
  87. url: null,
  88. async: true,
  89. user: null,
  90. password: null
  91. };
  92. this._ownerDocument = XMLHttpRequest._ownerDocument;
  93. }
  94. /**
  95. * Returns the status.
  96. *
  97. * @returns Status.
  98. */
  99. get status() {
  100. return this._state.status;
  101. }
  102. /**
  103. * Returns the status text.
  104. *
  105. * @returns Status text.
  106. */
  107. get statusText() {
  108. return this._state.statusText;
  109. }
  110. /**
  111. * Returns the response URL.
  112. *
  113. * @returns Response URL.
  114. */
  115. get responseURL() {
  116. return this._state.responseURL;
  117. }
  118. /**
  119. * Returns the ready state.
  120. *
  121. * @returns Ready state.
  122. */
  123. get readyState() {
  124. return this._state.readyState;
  125. }
  126. /**
  127. * Get the response text.
  128. *
  129. * @throws {DOMException} If the response type is not text or empty.
  130. * @returns The response text.
  131. */
  132. get responseText() {
  133. if (this.responseType === XMLHttpResponseTypeEnum_1.default.text || this.responseType === '') {
  134. return this._state.responseText;
  135. }
  136. 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);
  137. }
  138. /**
  139. * Get the responseXML.
  140. *
  141. * @throws {DOMException} If the response type is not text or empty.
  142. * @returns Response XML.
  143. */
  144. get responseXML() {
  145. if (this.responseType === XMLHttpResponseTypeEnum_1.default.document || this.responseType === '') {
  146. return this._state.responseXML;
  147. }
  148. 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);
  149. }
  150. /**
  151. * Set response type.
  152. *
  153. * @param type Response type.
  154. * @throws {DOMException} If the state is not unsent or opened.
  155. * @throws {DOMException} If the request is synchronous.
  156. */
  157. set responseType(type) {
  158. // ResponseType can only be set when the state is unsent or opened.
  159. if (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.opened &&
  160. this.readyState !== XMLHttpRequestReadyStateEnum_1.default.unsent) {
  161. 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);
  162. }
  163. // Sync requests can only have empty string or 'text' as response type.
  164. if (!this._settings.async) {
  165. 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);
  166. }
  167. this._state.responseType = type;
  168. }
  169. /**
  170. * Get response Type.
  171. *
  172. * @returns Response type.
  173. */
  174. get responseType() {
  175. return this._state.responseType;
  176. }
  177. /**
  178. * Opens the connection.
  179. *
  180. * @param method Connection method (eg GET, POST).
  181. * @param url URL for the connection.
  182. * @param [async=true] Asynchronous connection.
  183. * @param [user] Username for basic authentication (optional).
  184. * @param [password] Password for basic authentication (optional).
  185. */
  186. open(method, url, async = true, user, password) {
  187. this.abort();
  188. this._state.aborted = false;
  189. this._state.error = false;
  190. const upperMethod = method.toUpperCase();
  191. // Check for valid request method
  192. if (FORBIDDEN_REQUEST_METHODS.includes(upperMethod)) {
  193. throw new DOMException_1.default('Request method not allowed', DOMExceptionNameEnum_1.default.securityError);
  194. }
  195. // Check responseType.
  196. if (!async && !!this.responseType && this.responseType !== XMLHttpResponseTypeEnum_1.default.text) {
  197. 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);
  198. }
  199. this._settings = {
  200. method: upperMethod,
  201. url: url,
  202. async: async,
  203. user: user || null,
  204. password: password || null
  205. };
  206. this._setState(XMLHttpRequestReadyStateEnum_1.default.opened);
  207. }
  208. /**
  209. * Sets a header for the request.
  210. *
  211. * @param header Header name
  212. * @param value Header value
  213. * @returns Header added.
  214. */
  215. setRequestHeader(header, value) {
  216. if (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.opened) {
  217. throw new DOMException_1.default(`Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.`, DOMExceptionNameEnum_1.default.invalidStateError);
  218. }
  219. const lowerHeader = header.toLowerCase();
  220. if (FORBIDDEN_REQUEST_HEADERS.includes(lowerHeader)) {
  221. return false;
  222. }
  223. if (this._state.send) {
  224. throw new DOMException_1.default(`Failed to execute 'setRequestHeader' on 'XMLHttpRequest': Request is in progress.`, DOMExceptionNameEnum_1.default.invalidStateError);
  225. }
  226. this._state.requestHeaders[lowerHeader] = value;
  227. return true;
  228. }
  229. /**
  230. * Gets a header from the server response.
  231. *
  232. * @param header header Name of header to get.
  233. * @returns string Text of the header or null if it doesn't exist.
  234. */
  235. getResponseHeader(header) {
  236. const lowerHeader = header.toLowerCase();
  237. // Cookie headers are excluded for security reasons as per spec.
  238. if (typeof header === 'string' &&
  239. header !== 'set-cookie' &&
  240. header !== 'set-cookie2' &&
  241. this.readyState > XMLHttpRequestReadyStateEnum_1.default.opened &&
  242. this._state.incommingMessage.headers[lowerHeader] &&
  243. !this._state.error) {
  244. return this._state.incommingMessage.headers[lowerHeader];
  245. }
  246. return null;
  247. }
  248. /**
  249. * Gets all the response headers.
  250. *
  251. * @returns A string with all response headers separated by CR+LF.
  252. */
  253. getAllResponseHeaders() {
  254. if (this.readyState < XMLHttpRequestReadyStateEnum_1.default.headersRecieved || this._state.error) {
  255. return '';
  256. }
  257. const result = [];
  258. for (const name of Object.keys(this._state.incommingMessage.headers)) {
  259. // Cookie headers are excluded for security reasons as per spec.
  260. if (name !== 'set-cookie' && name !== 'set-cookie2') {
  261. result.push(`${name}: ${this._state.incommingMessage.headers[name]}`);
  262. }
  263. }
  264. return result.join('\r\n');
  265. }
  266. /**
  267. * Sends the request to the server.
  268. *
  269. * @param data Optional data to send as request body.
  270. */
  271. send(data) {
  272. var _a, _b;
  273. if (this.readyState != XMLHttpRequestReadyStateEnum_1.default.opened) {
  274. throw new DOMException_1.default(`Failed to execute 'send' on 'XMLHttpRequest': Connection must be opened before send() is called.`, DOMExceptionNameEnum_1.default.invalidStateError);
  275. }
  276. if (this._state.send) {
  277. throw new DOMException_1.default(`Failed to execute 'send' on 'XMLHttpRequest': Send has already been called.`, DOMExceptionNameEnum_1.default.invalidStateError);
  278. }
  279. const { location } = this._ownerDocument.defaultView;
  280. const url = RelativeURL_1.default.getAbsoluteURL(location, this._settings.url);
  281. // Security check.
  282. if (url.protocol === 'http:' && location.protocol === 'https:') {
  283. 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);
  284. }
  285. // Load files off the local filesystem (file://)
  286. if (XMLHttpRequestURLUtility_1.default.isLocal(url)) {
  287. if (!this._ownerDocument.defaultView.happyDOM.settings.enableFileSystemHttpRequests) {
  288. 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);
  289. }
  290. if (this._settings.method !== 'GET') {
  291. 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);
  292. }
  293. if (this._settings.async) {
  294. this._sendLocalAsyncRequest(url).catch((error) => this._onError(error));
  295. }
  296. else {
  297. this._sendLocalSyncRequest(url);
  298. }
  299. return;
  300. }
  301. // TODO: CORS check.
  302. const host = XMLHttpRequestURLUtility_1.default.getHost(url);
  303. const ssl = XMLHttpRequestURLUtility_1.default.isSSL(url);
  304. // Default to port 80. If accessing localhost on another port be sure
  305. // To use http://localhost:port/path
  306. const port = Number(url.port) || (ssl ? 443 : 80);
  307. // Add query string if one is used
  308. const uri = url.pathname + (url.search ? url.search : '');
  309. // Set the Host header or the server may reject the request
  310. this._state.requestHeaders['host'] = host;
  311. if (!((ssl && port === 443) || port === 80)) {
  312. this._state.requestHeaders['host'] += ':' + url.port;
  313. }
  314. // Set Basic Auth if necessary
  315. if (this._settings.user) {
  316. (_a = this._settings).password ?? (_a.password = '');
  317. const authBuffer = Buffer.from(this._settings.user + ':' + this._settings.password);
  318. this._state.requestHeaders['authorization'] = 'Basic ' + authBuffer.toString('base64');
  319. }
  320. // Set the Content-Length header if method is POST
  321. switch (this._settings.method) {
  322. case 'GET':
  323. case 'HEAD':
  324. data = null;
  325. break;
  326. case 'POST':
  327. (_b = this._state.requestHeaders)['content-type'] ?? (_b['content-type'] = 'text/plain;charset=UTF-8');
  328. if (data) {
  329. this._state.requestHeaders['content-length'] = Buffer.isBuffer(data)
  330. ? data.length
  331. : Buffer.byteLength(data);
  332. }
  333. else {
  334. this._state.requestHeaders['content-length'] = 0;
  335. }
  336. break;
  337. default:
  338. break;
  339. }
  340. const options = {
  341. host: host,
  342. port: port,
  343. path: uri,
  344. method: this._settings.method,
  345. headers: { ...this._getDefaultRequestHeaders(), ...this._state.requestHeaders },
  346. agent: false,
  347. rejectUnauthorized: true,
  348. key: ssl ? XMLHttpRequestCertificate_1.default.key : null,
  349. cert: ssl ? XMLHttpRequestCertificate_1.default.cert : null
  350. };
  351. // Reset error flag
  352. this._state.error = false;
  353. // Handle async requests
  354. if (this._settings.async) {
  355. this._sendAsyncRequest(options, ssl, data).catch((error) => this._onError(error));
  356. }
  357. else {
  358. this._sendSyncRequest(options, ssl, data);
  359. }
  360. }
  361. /**
  362. * Aborts a request.
  363. */
  364. abort() {
  365. if (this._state.asyncRequest) {
  366. this._state.asyncRequest.destroy();
  367. this._state.asyncRequest = null;
  368. }
  369. this._state.status = null;
  370. this._state.statusText = null;
  371. this._state.requestHeaders = {};
  372. this._state.responseText = '';
  373. this._state.responseXML = null;
  374. this._state.aborted = true;
  375. this._state.error = true;
  376. if (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.unsent &&
  377. (this.readyState !== XMLHttpRequestReadyStateEnum_1.default.opened || this._state.send) &&
  378. this.readyState !== XMLHttpRequestReadyStateEnum_1.default.done) {
  379. this._state.send = false;
  380. this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
  381. }
  382. this._state.readyState = XMLHttpRequestReadyStateEnum_1.default.unsent;
  383. if (this._state.asyncTaskID !== null) {
  384. this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
  385. }
  386. }
  387. /**
  388. * Changes readyState and calls onreadystatechange.
  389. *
  390. * @param state
  391. */
  392. _setState(state) {
  393. if (this.readyState === state ||
  394. (this.readyState === XMLHttpRequestReadyStateEnum_1.default.unsent && this._state.aborted)) {
  395. return;
  396. }
  397. this._state.readyState = state;
  398. if (this._settings.async ||
  399. this.readyState < XMLHttpRequestReadyStateEnum_1.default.opened ||
  400. this.readyState === XMLHttpRequestReadyStateEnum_1.default.done) {
  401. this.dispatchEvent(new Event_1.default('readystatechange'));
  402. }
  403. if (this.readyState === XMLHttpRequestReadyStateEnum_1.default.done) {
  404. let fire;
  405. if (this._state.aborted) {
  406. fire = new Event_1.default('abort');
  407. }
  408. else if (this._state.error) {
  409. fire = new Event_1.default('error');
  410. }
  411. else {
  412. fire = new Event_1.default('load');
  413. }
  414. this.dispatchEvent(fire);
  415. this.dispatchEvent(new Event_1.default('loadend'));
  416. }
  417. }
  418. /**
  419. * Default request headers.
  420. *
  421. * @returns Default request headers.
  422. */
  423. _getDefaultRequestHeaders() {
  424. const { location, navigator, document } = this._ownerDocument.defaultView;
  425. return {
  426. accept: '*/*',
  427. referer: location.href,
  428. 'user-agent': navigator.userAgent,
  429. cookie: document._cookie.getCookiesString(location, false)
  430. };
  431. }
  432. /**
  433. * Sends a synchronous request.
  434. *
  435. * @param options
  436. * @param ssl
  437. * @param data
  438. */
  439. _sendSyncRequest(options, ssl, data) {
  440. const scriptString = XMLHttpRequestSyncRequestScriptBuilder_1.default.getScript(options, ssl, data);
  441. // Start the other Node Process, executing this string
  442. const content = child_process_1.default.execFileSync(process.argv[0], ['-e', scriptString], {
  443. encoding: 'buffer',
  444. maxBuffer: 1024 * 1024 * 1024 // TODO: Consistent buffer size: 1GB.
  445. });
  446. // If content length is 0, then there was an error
  447. if (!content.length) {
  448. throw new DOMException_1.default('Synchronous request failed', DOMExceptionNameEnum_1.default.networkError);
  449. }
  450. const { error, data: response } = JSON.parse(content.toString());
  451. if (error) {
  452. this._onError(error);
  453. }
  454. if (response) {
  455. this._state.incommingMessage = {
  456. statusCode: response.statusCode,
  457. headers: response.headers
  458. };
  459. this._state.status = response.statusCode;
  460. this._state.statusText = response.statusMessage;
  461. // Sync responseType === ''
  462. this._state.response = this._decodeResponseText(Buffer.from(response.data, 'base64'));
  463. this._state.responseText = this._state.response;
  464. this._state.responseXML = null;
  465. this._state.responseURL = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._settings.url).href;
  466. // Set Cookies.
  467. this._setCookies(this._state.incommingMessage.headers);
  468. // Redirect.
  469. if (this._state.incommingMessage.statusCode === 301 ||
  470. this._state.incommingMessage.statusCode === 302 ||
  471. this._state.incommingMessage.statusCode === 303 ||
  472. this._state.incommingMessage.statusCode === 307) {
  473. const redirectUrl = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._state.incommingMessage.headers['location']);
  474. ssl = redirectUrl.protocol === 'https:';
  475. this._settings.url = redirectUrl.href;
  476. // Recursive call.
  477. this._sendSyncRequest(Object.assign(options, {
  478. host: redirectUrl.host,
  479. path: redirectUrl.pathname + (redirectUrl.search ?? ''),
  480. port: redirectUrl.port || (ssl ? 443 : 80),
  481. method: this._state.incommingMessage.statusCode === 303 ? 'GET' : this._settings.method,
  482. headers: Object.assign(options.headers, {
  483. referer: redirectUrl.origin,
  484. host: redirectUrl.host
  485. })
  486. }), ssl, data);
  487. }
  488. this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
  489. }
  490. }
  491. /**
  492. * Sends an async request.
  493. *
  494. * @param options
  495. * @param ssl
  496. * @param data
  497. */
  498. _sendAsyncRequest(options, ssl, data) {
  499. return new Promise((resolve) => {
  500. // Starts async task in Happy DOM
  501. this._state.asyncTaskID = this._ownerDocument.defaultView.happyDOM.asyncTaskManager.startTask(this.abort.bind(this));
  502. // Use the proper protocol
  503. const sendRequest = ssl ? https_1.default.request : http_1.default.request;
  504. // Request is being sent, set send flag
  505. this._state.send = true;
  506. // As per spec, this is called here for historical reasons.
  507. this.dispatchEvent(new Event_1.default('readystatechange'));
  508. // Create the request
  509. this._state.asyncRequest = sendRequest(options, async (response) => {
  510. await this._onAsyncResponse(response, options, ssl, data);
  511. resolve();
  512. // Ends async task in Happy DOM
  513. this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
  514. }).on('error', (error) => {
  515. this._onError(error);
  516. resolve();
  517. // Ends async task in Happy DOM
  518. this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID);
  519. });
  520. // Node 0.4 and later won't accept empty data. Make sure it's needed.
  521. if (data) {
  522. this._state.asyncRequest.write(data);
  523. }
  524. this._state.asyncRequest.end();
  525. this.dispatchEvent(new Event_1.default('loadstart'));
  526. });
  527. }
  528. /**
  529. * Handles an async response.
  530. *
  531. * @param options Options.
  532. * @param ssl SSL.
  533. * @param data Data.
  534. * @param response Response.
  535. * @returns Promise.
  536. */
  537. _onAsyncResponse(response, options, ssl, data) {
  538. return new Promise((resolve) => {
  539. // Set response var to the response we got back
  540. // This is so it remains accessable outside this scope
  541. this._state.incommingMessage = response;
  542. // Set Cookies
  543. this._setCookies(this._state.incommingMessage.headers);
  544. // Check for redirect
  545. // @TODO Prevent looped redirects
  546. if (this._state.incommingMessage.statusCode === 301 ||
  547. this._state.incommingMessage.statusCode === 302 ||
  548. this._state.incommingMessage.statusCode === 303 ||
  549. this._state.incommingMessage.statusCode === 307) {
  550. // TODO: redirect url protocol change.
  551. // Change URL to the redirect location
  552. this._settings.url = this._state.incommingMessage.headers.location;
  553. // Parse the new URL.
  554. const redirectUrl = RelativeURL_1.default.getAbsoluteURL(this._ownerDocument.defaultView.location, this._settings.url);
  555. this._settings.url = redirectUrl.href;
  556. ssl = redirectUrl.protocol === 'https:';
  557. // Issue the new request
  558. this._sendAsyncRequest({
  559. ...options,
  560. host: redirectUrl.hostname,
  561. port: redirectUrl.port,
  562. path: redirectUrl.pathname + (redirectUrl.search ?? ''),
  563. method: this._state.incommingMessage.statusCode === 303 ? 'GET' : this._settings.method,
  564. headers: { ...options.headers, referer: redirectUrl.origin, host: redirectUrl.host }
  565. }, ssl, data);
  566. // @TODO Check if an XHR event needs to be fired here
  567. return;
  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),
  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(this._decodeResponseText(data)),
  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. const responseText = this._decodeResponseText(data);
  739. return {
  740. response: responseText,
  741. responseText: responseText,
  742. responseXML: null
  743. };
  744. }
  745. }
  746. /**
  747. * Set Cookies from response headers.
  748. *
  749. * @param headers String array.
  750. */
  751. _setCookies(headers) {
  752. for (const cookie of [...(headers['set-cookie'] ?? []), ...(headers['set-cookie2'] ?? [])]) {
  753. this._ownerDocument.defaultView.document._cookie.setCookiesString(cookie);
  754. }
  755. }
  756. /**
  757. * Called when an error is encountered to deal with it.
  758. *
  759. * @param error Error.
  760. */
  761. _onError(error) {
  762. this._state.status = 0;
  763. this._state.statusText = error.toString();
  764. this._state.responseText = error instanceof Error ? error.stack : '';
  765. this._state.error = true;
  766. this._setState(XMLHttpRequestReadyStateEnum_1.default.done);
  767. }
  768. /**
  769. * Decodes response text.
  770. *
  771. * @param data Data.
  772. * @returns Decoded text.
  773. **/
  774. _decodeResponseText(data) {
  775. const contextTypeEncodingRegexp = new RegExp(CONTENT_TYPE_ENCODING_REGEXP, 'gi');
  776. let contentType;
  777. if (this._state.incommingMessage && this._state.incommingMessage.headers) {
  778. contentType = this._state.incommingMessage.headers['content-type']; // For remote requests (http/https).
  779. }
  780. else {
  781. contentType = this._state.requestHeaders['content-type']; // For local requests or unpredictable remote requests.
  782. }
  783. const charset = contextTypeEncodingRegexp.exec(contentType);
  784. // Default utf-8
  785. return iconv_lite_1.default.decode(data, charset ? charset[1] : 'utf-8');
  786. }
  787. }
  788. exports.default = XMLHttpRequest;
  789. // Owner document is set by a sub-class in the Window constructor
  790. XMLHttpRequest._ownerDocument = null;
  791. // Constants
  792. XMLHttpRequest.UNSENT = XMLHttpRequestReadyStateEnum_1.default.unsent;
  793. XMLHttpRequest.OPENED = XMLHttpRequestReadyStateEnum_1.default.opened;
  794. XMLHttpRequest.HEADERS_RECEIVED = XMLHttpRequestReadyStateEnum_1.default.headersRecieved;
  795. XMLHttpRequest.LOADING = XMLHttpRequestReadyStateEnum_1.default.loading;
  796. XMLHttpRequest.DONE = XMLHttpRequestReadyStateEnum_1.default.done;
  797. //# sourceMappingURL=XMLHttpRequest.js.map