From 6763d5284bbafbbbef237c0a1187879b75f904ff Mon Sep 17 00:00:00 2001 From: Mas0nShi Date: Sat, 25 Feb 2023 18:04:18 +0800 Subject: [PATCH] =?UTF-8?q?#791@patch:=20Fix=20=F0=9F=94=A7=20#788=20#789?= =?UTF-8?q?=20and=20#791.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/xml-http-request/XMLHttpRequest.ts | 60 +++++++++---- .../utilities/XMLHttpRequestURLUtility.ts | 85 ++++++++++++++++++- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 700ea1354..c8c65f5e6 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -73,7 +73,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Private properties private readonly _ownerDocument: IDocument = null; private _state: { - incommingMessage: HTTP.IncomingMessage | { headers: string[]; statusCode: number }; + incommingMessage: HTTP.IncomingMessage | { headers: object; statusCode: number }; response: ArrayBuffer | Blob | IDocument | object | string; responseType: XMLHttpResponseTypeEnum | ''; responseText: string; @@ -146,6 +146,15 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return this._state.statusText; } + /** + * Returns the response. + * + * @returns Response. + */ + public get response(): ArrayBuffer | Blob | IDocument | object | string { + return this._state.response; + } + /** * Returns the response URL. * @@ -597,7 +606,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { }; this._state.status = response.statusCode; this._state.statusText = response.statusMessage; + // Although it will immediately be set to loading, + // According to the spec, the state should be headersRecieved first. + this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); // Sync responseType === '' + this._setState(XMLHttpRequestReadyStateEnum.loading); this._state.response = this._decodeResponseText(Buffer.from(response.data, 'base64')); this._state.responseText = this._state.response; this._state.responseXML = null; @@ -758,9 +771,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return; } - this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); this._state.status = this._state.incommingMessage.statusCode; this._state.statusText = this._state.incommingMessage.statusMessage; + this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); // Initialize response. let tempResponse = Buffer.from(new Uint8Array(0)); @@ -835,6 +848,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const dataLength = data.length; + // @TODO: set state headersRecieved first. this._setState(XMLHttpRequestReadyStateEnum.loading); this.dispatchEvent( new ProgressEvent('progress', { @@ -845,9 +859,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); if (data) { - this._parseLocalRequestData(data); + this._parseLocalRequestData(url, data); } + this._setState(XMLHttpRequestReadyStateEnum.done); this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID); } @@ -863,19 +878,33 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } catch (error) { this._onError(error); } - + // @TODO: set state headersRecieved first. + this._setState(XMLHttpRequestReadyStateEnum.loading); if (data) { - this._parseLocalRequestData(data); + this._parseLocalRequestData(url, data); } + + this._setState(XMLHttpRequestReadyStateEnum.done); } /** * Parses local request data. * + * @param url URL. * @param data Data. */ - private _parseLocalRequestData(data: Buffer): void { - this._state.status = 200; + private _parseLocalRequestData(url: UrlObject, data: Buffer): void { + // Manually set the response headers. + this._state.incommingMessage = { + statusCode: 200, + headers: { + 'content-length': data.length, + 'content-type': XMLHttpRequestURLUtility.getMimeTypeFromExt(url) + // @TODO: 'last-modified': + } + }; + + this._state.status = this._state.incommingMessage.statusCode; this._state.statusText = 'OK'; const { response, responseXML, responseText } = this._parseResponseData(data); @@ -941,7 +970,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const domParser = new window.DOMParser(); try { - response = domParser.parseFromString(data.toString(), 'text/xml'); + response = domParser.parseFromString(this._decodeResponseText(data), 'text/xml'); } catch (e) { return { response: null, responseText: null, responseXML: null }; } @@ -975,9 +1004,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { /** * Set Cookies from response headers. * - * @param headers String array. + * @param headers Headers. */ - private _setCookies(headers: string[] | HTTP.IncomingHttpHeaders): void { + private _setCookies(headers: object | HTTP.IncomingHttpHeaders): void { for (const cookie of [...(headers['set-cookie'] ?? []), ...(headers['set-cookie2'] ?? [])]) { this._ownerDocument.defaultView.document._cookie.setCookiesString(cookie); } @@ -1004,14 +1033,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { **/ private _decodeResponseText(data: Buffer): string { const contextTypeEncodingRegexp = new RegExp(CONTENT_TYPE_ENCODING_REGEXP, 'gi'); - let contentType; - if (this._state.incommingMessage && this._state.incommingMessage.headers) { - contentType = this._state.incommingMessage.headers['content-type']; // For remote requests (http/https). - } else { - contentType = this._state.requestHeaders['content-type']; // For local requests or unpredictable remote requests. - } + // Compatibility with file:// protocol or unpredictable http request. + const contentType = + this.getResponseHeader('content-type') ?? this._state.requestHeaders['content-type']; const charset = contextTypeEncodingRegexp.exec(contentType); - // Default utf-8 + // Default encoding: utf-8. return iconv.decode(data, charset ? charset[1] : 'utf-8'); } } diff --git a/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestURLUtility.ts b/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestURLUtility.ts index 551283d35..c5d1a2682 100644 --- a/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestURLUtility.ts +++ b/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestURLUtility.ts @@ -1,4 +1,78 @@ -import { URL } from 'url'; +import { URL, UrlObject } from 'url'; +import Path from 'path'; + +// MIME type. +const LOCAL_MIME_TYPES = { + json: 'application/json', + xml: 'application/xml', + html: 'text/html', + text: 'text/plain', + xhtml: 'application/xhtml+xml', + xht: 'application/xhtml+xml', + xsl: 'application/xml', + xslt: 'application/xml', + rss: 'application/rss+xml', + atom: 'application/atom+xml', + yaml: 'application/x-yaml', + pdf: 'application/pdf', + zip: 'application/zip', + gzip: 'application/gzip', + rar: 'application/x-rar-compressed', + '7z': 'application/x-7z-compressed', + exe: 'application/x-msdownload', + csv: 'text/csv', + ics: 'text/calendar', + rtf: 'text/rtf', + js: 'application/javascript', + css: 'text/css', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + bmp: 'image/bmp', + ico: 'image/x-icon', + tiff: 'image/tiff', + tif: 'image/tiff', + svg: 'image/svg+xml', + svgz: 'image/svg+xml', + webp: 'image/webp', + wav: 'audio/wav', + webm: 'video/webm', + mp4: 'video/mp4', + mpeg: 'video/mpeg', + mpg: 'video/mpeg', + mov: 'video/quicktime', + avi: 'video/x-msvideo', + flv: 'video/x-flv', + mkv: 'video/x-matroska', + mka: 'audio/x-matroska', + m3u: 'audio/x-mpegurl', + m3u8: 'application/x-mpegURL', + pls: 'audio/x-scpls', + flac: 'audio/x-flac', + ogg: 'audio/ogg', + oga: 'audio/ogg', + ogv: 'video/ogg', + ogx: 'application/ogg', + opus: 'audio/opus', + spx: 'audio/ogg', + swf: 'application/x-shockwave-flash', + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + eot: 'application/vnd.ms-fontobject', + otf: 'font/otf', + sfnt: 'application/font-sfnt', + bin: 'application/octet-stream', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ppt: 'application/vnd.ms-powerpoint', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + odt: 'application/vnd.oasis.opendocument.text', + ods: 'application/vnd.oasis.opendocument.spreadsheet' +}; /** * URL utility. @@ -61,4 +135,13 @@ export default class XMLHttpRequestURLUtility { return null; } } + + /** + * + * @param url + */ + public static getMimeTypeFromExt(url: UrlObject): string { + const ext = Path.extname(url.pathname); + return LOCAL_MIME_TYPES[ext] || 'application/octet-stream'; + } }