From d1f8256d41e004b7525ffef3cbf2c1bcde33f354 Mon Sep 17 00:00:00 2001 From: Mas0nShi Date: Fri, 1 Jul 2022 12:55:41 +0800 Subject: [PATCH] #521@patch: Replace URL to native module URL. --- packages/happy-dom/src/fetch/FetchHandler.ts | 2 +- .../src/fetch/ResourceFetchHandler.ts | 5 +- packages/happy-dom/src/location/Location.ts | 4 +- .../happy-dom/src/location/RelativeURL.ts | 33 +----- packages/happy-dom/src/location/URL.ts | 100 +----------------- .../happy-dom/src/location/URLSearchParams.ts | 6 ++ packages/happy-dom/src/window/IWindow.ts | 4 +- packages/happy-dom/src/window/Window.ts | 11 +- .../src/xml-http-request/XMLHttpRequest.ts | 100 ++++++++++-------- .../xml-http-request/XMLHttpRequestUpload.ts | 8 +- .../test/location/RelativeURL.test.ts | 12 +-- 11 files changed, 88 insertions(+), 197 deletions(-) create mode 100644 packages/happy-dom/src/location/URLSearchParams.ts diff --git a/packages/happy-dom/src/fetch/FetchHandler.ts b/packages/happy-dom/src/fetch/FetchHandler.ts index 81fe86b57..755b20292 100644 --- a/packages/happy-dom/src/fetch/FetchHandler.ts +++ b/packages/happy-dom/src/fetch/FetchHandler.ts @@ -24,7 +24,7 @@ export default class FetchHandler { return new Promise((resolve, reject) => { const taskID = taskManager.startTask(); - NodeFetch(RelativeURL.getAbsoluteURL(document.defaultView.location, url), init) + NodeFetch(RelativeURL.getAbsoluteURL(document.defaultView.location, url).href, init) .then((response) => { if (taskManager.getTaskCount() === 0) { reject(new Error('Failed to complete fetch request. Task was canceled.')); diff --git a/packages/happy-dom/src/fetch/ResourceFetchHandler.ts b/packages/happy-dom/src/fetch/ResourceFetchHandler.ts index 79f3d8d0c..176880555 100644 --- a/packages/happy-dom/src/fetch/ResourceFetchHandler.ts +++ b/packages/happy-dom/src/fetch/ResourceFetchHandler.ts @@ -32,14 +32,13 @@ export default class ResourceFetchHandler { */ public static fetchSync(document: IDocument, url: string): string { // We want to only load SyncRequest when it is needed to improve performance and not have direct dependencies to server side packages. - const absoluteURL = RelativeURL.getAbsoluteURL(document.defaultView.location, url); + const absoluteURL = RelativeURL.getAbsoluteURL(document.defaultView.location, url).href; const syncRequest = require('sync-request'); const response = syncRequest('GET', absoluteURL, { headers: { 'user-agent': document.defaultView.navigator.userAgent, cookie: document.defaultView.document.cookie, - referer: document.defaultView.location.href, - pragma: 'no-cache' + referer: document.defaultView.location.origin } }); diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 4d6599e8d..0e16a3671 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -8,7 +8,7 @@ export default class Location extends URL { * Constructor. */ constructor() { - super(''); + super('about:blank'); } /** @@ -17,7 +17,7 @@ export default class Location extends URL { * @param url URL. */ public replace(url: string): void { - this.parse(url); + this.href = url; } /** diff --git a/packages/happy-dom/src/location/RelativeURL.ts b/packages/happy-dom/src/location/RelativeURL.ts index 1ae92557d..53a04ab5f 100644 --- a/packages/happy-dom/src/location/RelativeURL.ts +++ b/packages/happy-dom/src/location/RelativeURL.ts @@ -1,4 +1,5 @@ import Location from './Location'; +import URL from './URL'; /** * Helper class for getting the URL relative to a Location object. @@ -10,35 +11,7 @@ export default class RelativeURL { * @param location Location. * @param url URL. */ - public static getAbsoluteURL(location: Location, url: string): string { - // If the URL starts with '//' then it is a Protocol relative URL. - // Reference: https://url.spec.whatwg.org/#protocol-relative-urls. - // E.g. '//example.com/' needs to be converted to 'http://example.com/'. - if (url.startsWith('//')) { - return location.protocol + url; - } - // If the URL starts with '/' then it is a Path relative URL. - // E.g. '/example.com/' needs to be converted to 'http://example.com/'. - if (url.startsWith('/')) { - return location.origin + url; - } - // If the URL starts with 'https://' or 'http://' then it is a Absolute URL. - // E.g. 'https://example.com' needs to be converted to 'https://example.com/'. - // E.g. 'http://example.com' needs to be converted to 'http://example.com/'. - if (!url.startsWith('https://') && !url.startsWith('http://')) { - let pathname = location.pathname; - if (pathname.endsWith('/')) { - pathname = pathname.slice(0, -1); - } - - return ( - location.origin + - (/(.*)\/.*/.test(pathname) ? pathname.match(/(.*)\/.*/)[1] : '') + - '/' + - url - ); - } - - return url; + public static getAbsoluteURL(location: Location, url: string): URL { + return new URL(url, location.href); } } diff --git a/packages/happy-dom/src/location/URL.ts b/packages/happy-dom/src/location/URL.ts index e66e4317f..2d52a7c47 100644 --- a/packages/happy-dom/src/location/URL.ts +++ b/packages/happy-dom/src/location/URL.ts @@ -1,102 +1,6 @@ -const URL_REGEXP = - /(https?:)\/\/([-a-zA-Z0-9@:%._\+~#=]{2,256}[a-z]{2,6})(:[0-9]*)?([-a-zA-Z0-9@:%_\+.~c&//=]*)(\?[^#]*)?(#.*)?/; -const PATH_REGEXP = /([-a-zA-Z0-9@:%_\+.~c&//=]*)(\?[^#]*)?(#.*)?/; +import { URL as Url } from 'url'; /** * */ -export default class URL { - public protocol = ''; - public hostname = ''; - public port = ''; - public pathname = ''; - public search = ''; - public hash = ''; - public username = ''; - public password = ''; - - /** - * Constructor. - * - * @param [url] URL. - */ - constructor(url?: string) { - if (url) { - this.parse(url); - } - } - - /** - * Returns the entire URL as a string. - * - * @returns Href. - */ - public get href(): string { - const credentials = this.username ? `${this.username}:${this.password}@` : ''; - return this.protocol + '//' + credentials + this.host + this.pathname + this.search + this.hash; - } - - /** - * Sets the href. - * - * @param url URL. - */ - public set href(url: string) { - this.parse(url); - } - - /** - * Returns the origin. - * - * @returns HREF. - */ - public get origin(): string { - return this.protocol + '//' + this.host; - } - - /** - * Returns the entire URL as a string. - * - * @returns Host. - */ - public get host(): string { - return this.hostname + this.port; - } - - /** - * Returns the entire URL as a string. - */ - public toString(): string { - return this.href; - } - - /** - * Parses an URL. - * - * @param url URL. - */ - protected parse(url: string): void { - const match = url.match(URL_REGEXP); - - if (match) { - const hostnamePart = match[2] ? match[2].split('@') : ''; - const credentialsPart = hostnamePart.length > 1 ? hostnamePart[0].split(':') : null; - - this.protocol = match[1] || ''; - this.hostname = hostnamePart.length > 1 ? hostnamePart[1] : hostnamePart[0]; - this.port = match[3] || ''; - this.pathname = match[4] || '/'; - this.search = match[5] || ''; - this.hash = match[6] || ''; - this.username = credentialsPart ? credentialsPart[0] : ''; - this.password = credentialsPart ? credentialsPart[1] : ''; - } else { - const pathMatch = url.match(PATH_REGEXP); - if (pathMatch) { - this.pathname = pathMatch[1] || ''; - this.search = pathMatch[2] || ''; - this.hash = pathMatch[3] || ''; - } - } - } -} +export default class URL extends Url {} diff --git a/packages/happy-dom/src/location/URLSearchParams.ts b/packages/happy-dom/src/location/URLSearchParams.ts new file mode 100644 index 000000000..fbfd6dbba --- /dev/null +++ b/packages/happy-dom/src/location/URLSearchParams.ts @@ -0,0 +1,6 @@ +import { URLSearchParams as UrlSearchParams } from 'url'; + +/** + * + */ +export default class URLSearchParams extends UrlSearchParams {} diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index b2d23151f..1574bcbf3 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -37,6 +37,7 @@ import KeyboardEvent from '../event/events/KeyboardEvent'; import ProgressEvent from '../event/events/ProgressEvent'; import EventTarget from '../event/EventTarget'; import URL from '../location/URL'; +import URLSearchParams from '../location/URLSearchParams'; import Location from '../location/Location'; import MutationObserver from '../mutation-observer/MutationObserver'; import DOMParser from '../dom-parser/DOMParser'; @@ -84,7 +85,6 @@ import Range from '../range/Range'; import MediaQueryList from '../match-media/MediaQueryList'; import DOMRect from '../nodes/element/DOMRect'; import Window from './Window'; -import { URLSearchParams } from 'url'; import { Performance } from 'perf_hooks'; /** @@ -150,6 +150,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { readonly DataTransferItem: typeof DataTransferItem; readonly DataTransferItemList: typeof DataTransferItemList; readonly URL: typeof URL; + readonly URLSearchParams: typeof URLSearchParams; readonly Location: typeof Location; readonly CustomElementRegistry: typeof CustomElementRegistry; readonly Window: typeof Window; @@ -163,7 +164,6 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { readonly History: typeof History; readonly Screen: typeof Screen; readonly Storage: typeof Storage; - readonly URLSearchParams: typeof URLSearchParams; readonly HTMLCollection: typeof HTMLCollection; readonly NodeList: typeof NodeList; readonly CSSUnitValue: typeof CSSUnitValue; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index bbc969756..46aa1fc4f 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -37,6 +37,7 @@ import KeyboardEvent from '../event/events/KeyboardEvent'; import ProgressEvent from '../event/events/ProgressEvent'; import EventTarget from '../event/EventTarget'; import URL from '../location/URL'; +import URLSearchParams from '../location/URLSearchParams'; import Location from '../location/Location'; import NonImplementedEventTypes from '../event/NonImplementedEventTypes'; import MutationObserver from '../mutation-observer/MutationObserver'; @@ -86,7 +87,6 @@ import MimeType from '../navigator/MimeType'; import MimeTypeArray from '../navigator/MimeTypeArray'; import Plugin from '../navigator/Plugin'; import PluginArray from '../navigator/PluginArray'; -import { URLSearchParams } from 'url'; import FetchHandler from '../fetch/FetchHandler'; import { default as RangeImplementation } from '../range/Range'; import DOMRect from '../nodes/element/DOMRect'; @@ -209,11 +209,6 @@ export default class Window extends EventTarget implements IWindow { public readonly FileReader; public readonly Image; - // XMLHttpRequest - public XMLHttpRequest = XMLHttpRequest; - public XMLHttpRequestUpload = XMLHttpRequestUpload; - public XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; - // Events public onload: (event: Event) => void = null; public onerror: (event: ErrorEvent) => void = null; @@ -238,6 +233,10 @@ export default class Window extends EventTarget implements IWindow { public readonly localStorage = new Storage(); public readonly performance = PerfHooks.performance; + public XMLHttpRequest = XMLHttpRequest; + public XMLHttpRequestUpload = XMLHttpRequestUpload; + public XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; + // Node.js Globals public ArrayBuffer; public Boolean; diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 810d8fed5..ea28b8fba 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -15,12 +15,13 @@ import DOMException from '../exception/DOMException'; import IWindow from '../window/IWindow'; import URL from '../location/URL'; import RelativeURL from '../location/RelativeURL'; +import Blob from '../file/Blob'; +import { + copyToArrayBuffer, + MajorNodeVersion, + IXMLHttpRequestOptions +} from './XMLHttpReqeustUtility'; -interface IXMLHttpRequestOptions { - anon?: boolean; -} -const NodeVersion = process.version.replace('v', '').split('.'); -const MajorNodeVersion = Number.parseInt(NodeVersion[0]); /** * References: https://github.com/souldreamer/xhr2-cookies. */ @@ -112,6 +113,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param password The password to use for authentication purposes. */ public open(method: string, url: string, async = true, user?: string, password?: string): void { + const { _defaultView } = XMLHttpRequest; + // If _defaultView is not defined, then we can't set the URL. + if (!_defaultView) { + throw new Error('need set defaultView'); + } method = method.toUpperCase(); if (this.restrictedMethods[method]) { throw new DOMException( @@ -119,14 +125,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { DOMExceptionNameEnum.securityError ); } - // If _defaultView is not defined, then we can't set the URL. - if (!XMLHttpRequest._defaultView) { - throw new Error('need set defaultView'); - } + // Get and Parse the URL relative to the given Location object. - const xhrUrl = new XMLHttpRequest._defaultView.URL( - RelativeURL.getAbsoluteURL(XMLHttpRequest._defaultView.location, url) - ); + const xhrUrl = RelativeURL.getAbsoluteURL(XMLHttpRequest._defaultView.location, url); // Set username and password if given. xhrUrl.username = user ? user : xhrUrl.username; xhrUrl.password = password ? password : xhrUrl.password; @@ -163,6 +164,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param value The value to set as the body of the header. */ public setRequestHeader(name: string, value: unknown): void { + const { _defaultView } = XMLHttpRequest; if (this.readyState !== XMLHttpRequest.OPENED) { throw new DOMException( 'XHR readyState must be OPENED', @@ -176,7 +178,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { /^sec-/.test(loweredName) || /^proxy-/.test(loweredName) ) { - XMLHttpRequest._defaultView.console.warn(`Refused to set unsafe header "${name}"`); + _defaultView.console.warn(`Refused to set unsafe header "${name}"`); return; } @@ -196,14 +198,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param data The data to send with the request. */ public send(data?: string | Buffer | ArrayBuffer | ArrayBufferView): void { + const { invalidStateError, networkError } = DOMExceptionNameEnum; if (this.readyState !== XMLHttpRequest.OPENED) { - throw new DOMException( - 'XHR readyState must be OPENED', - DOMExceptionNameEnum.invalidStateError - ); + throw new DOMException('XHR readyState must be OPENED', invalidStateError); } if (this._request) { - throw new DOMException('send() already called', DOMExceptionNameEnum.invalidStateError); + throw new DOMException('send() already called', invalidStateError); } switch (this.url.protocol) { case 'file:': @@ -212,10 +212,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case 'https:': return this.sendHttp(data); default: - throw new DOMException( - `Unsupported protocol ${this.url.protocol}`, - DOMExceptionNameEnum.networkError - ); + throw new DOMException(`Unsupported protocol ${this.url.protocol}`, networkError); } } @@ -233,7 +230,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } else { this._request.abort(); } - + this.setReadyState(XMLHttpRequest.DONE); this.setError(); this.dispatchProgress('abort'); @@ -246,7 +243,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param name The name of the header. */ public getResponseHeader(name: string): string { - if (this.responseHeaders == null || name == null) { + if ( + this.responseHeaders == null || + name == null || + this.readyState in [XMLHttpRequest.OPENED, XMLHttpRequest.UNSENT] + ) { return null; } const loweredName = name.toLowerCase(); @@ -260,7 +261,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * */ public getAllResponseHeaders(): string { - if (this.responseHeaders == null) { + if ( + this.responseHeaders == null || + this.readyState in [XMLHttpRequest.OPENED, XMLHttpRequest.UNSENT] + ) { return ''; } return Object.keys(this.responseHeaders) @@ -274,7 +278,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param mimeType The MIME type to use. */ public overrideMimeType(mimeType: string): void { - if (this.readyState === XMLHttpRequest.LOADING || this.readyState === XMLHttpRequest.DONE) { + if (this.readyState in [XMLHttpRequest.LOADING, XMLHttpRequest.DONE]) { throw new DOMException( 'overrideMimeType() not allowed in LOADING or DONE', DOMExceptionNameEnum.invalidStateError @@ -296,6 +300,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { /** * Send request with file. * + * @todo Not implemented. * @param _data File body to send. */ private sendFile(_data: unknown): void { @@ -309,13 +314,13 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param data Data to send. */ private sendHttp(data?: string | Buffer | ArrayBuffer | ArrayBufferView): void { + const { _defaultView } = XMLHttpRequest; if (this.sync) { + // TODO: sync not implemented. throw new Error('Synchronous XHR processing not implemented'); } if (data && (this.method === 'GET' || this.method === 'HEAD')) { - XMLHttpRequest._defaultView.console.warn( - `Discarding entity body for ${this.method} requests` - ); + _defaultView.console.warn(`Discarding entity body for ${this.method} requests`); data = null; } else { data = data || ''; @@ -331,10 +336,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * */ private sendHxxpRequest(): void { + const { _defaultView } = XMLHttpRequest; if (this.withCredentials) { // Set cookie if URL is same-origin. - if (XMLHttpRequest._defaultView.location.origin === this.url.origin) { - this.headers.cookie = XMLHttpRequest._defaultView.document.cookie; + if (_defaultView.location.origin === this.url.origin) { + this.headers.cookie = _defaultView.document.cookie; } } @@ -369,11 +375,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * */ private finalizeHeaders(): void { + const { _defaultView } = XMLHttpRequest; this.headers = { ...this.headers, Connection: 'keep-alive', Host: this.url.host, - 'User-Agent': XMLHttpRequest._defaultView.navigator.userAgent, + 'User-Agent': _defaultView.navigator.userAgent, ...(this.anonymous ? { Referer: 'about:blank' } : {}) }; this.upload.finalizeHeaders(this.headers, this.loweredHeaders); @@ -389,13 +396,14 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { if (this._request !== request) { return; } + const { _defaultView } = XMLHttpRequest; if (this.withCredentials && response.headers['set-cookie']) { - XMLHttpRequest._defaultView.document.cookie = response.headers['set-cookie'].join('; '); + _defaultView.document.cookie = response.headers['set-cookie'].join('; '); } if ([301, 302, 303, 307, 308].indexOf(response.statusCode) >= 0) { - this.url = new XMLHttpRequest._defaultView.URL(response.headers.location); + this.url = new _defaultView.URL(response.headers.location); this.method = 'GET'; if (this.loweredHeaders['content-type']) { delete this.headers[this.loweredHeaders['content-type']]; @@ -604,20 +612,24 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } catch { this.response = null; } - return; + break; case 'buffer': this.responseText = null; this.response = buffer; - return; + break; case 'arraybuffer': this.responseText = null; - const arrayBuffer = new ArrayBuffer(buffer.length); - const view = new Uint8Array(arrayBuffer); - for (let i = 0; i < buffer.length; i++) { - view[i] = buffer[i]; - } - this.response = arrayBuffer; - return; + this.response = copyToArrayBuffer(buffer); + break; + case 'blob': + this.responseText = null; + this.response = new Blob([new Uint8Array(buffer)], { + type: this.mimeOverride || this.responseHeaders['content-type'] || '' + }); + break; + case 'document': + // TODO: MimeType parse not yet supported. + break; case 'text': default: try { @@ -626,7 +638,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.responseText = buffer.toString('binary'); } this.response = this.responseText; + break; } + return; } /** diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequestUpload.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequestUpload.ts index 0a77e2443..da9c8c710 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequestUpload.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequestUpload.ts @@ -44,17 +44,13 @@ export default class XMLHttpRequestUpload extends XMLHttpRequestEventTarget { } else if (data instanceof ArrayBuffer) { const body = Buffer.alloc(data.byteLength); const view = new Uint8Array(data); - for (let i = 0; i < data.byteLength; i++) { - body[i] = view[i]; - } + body.set(view); this._body = body; } else if (data.buffer && data.buffer instanceof ArrayBuffer) { const body = Buffer.alloc(data.byteLength); const offset = data.byteOffset; const view = new Uint8Array(data.buffer); - for (let i = 0; i < data.byteLength; i++) { - body[i] = view[i + offset]; - } + body.set(view, offset); this._body = body; } else { throw new Error(`Unsupported send() data ${data}`); diff --git a/packages/happy-dom/test/location/RelativeURL.test.ts b/packages/happy-dom/test/location/RelativeURL.test.ts index 29ec0a990..010341279 100644 --- a/packages/happy-dom/test/location/RelativeURL.test.ts +++ b/packages/happy-dom/test/location/RelativeURL.test.ts @@ -11,22 +11,22 @@ describe('RelativeURL', () => { describe('getAbsoluteURL()', () => { it('Returns absolute URL when location is "https://localhost:8080/base/" and URL is "path/to/resource/".', () => { location.href = 'https://localhost:8080/base/'; - expect(RelativeURL.getAbsoluteURL(location, 'path/to/resource/')).toBe( - 'https://localhost:8080/path/to/resource/' + expect(RelativeURL.getAbsoluteURL(location, 'path/to/resource/').href).toBe( + 'https://localhost:8080/base/path/to/resource/' ); }); it('Returns absolute URL when location is "https://localhost:8080" and URL is "path/to/resource/".', () => { location.href = 'https://localhost:8080'; - expect(RelativeURL.getAbsoluteURL(location, 'path/to/resource/')).toBe( + expect(RelativeURL.getAbsoluteURL(location, 'path/to/resource/').href).toBe( 'https://localhost:8080/path/to/resource/' ); }); it('Returns absolute URL when URL is "https://localhost:8080/path/to/resource/".', () => { - expect(RelativeURL.getAbsoluteURL(location, 'https://localhost:8080/path/to/resource/')).toBe( - 'https://localhost:8080/path/to/resource/' - ); + expect( + RelativeURL.getAbsoluteURL(location, 'https://localhost:8080/path/to/resource/').href + ).toBe('https://localhost:8080/path/to/resource/'); }); }); });