UNPKG

@fastly/as-url

Version:

![npm version](https://img.shields.io/npm/v/@fastly/as-url) ![npm downloads per month](https://img.shields.io/npm/dm/@fastly/as-url)

345 lines (281 loc) 8.43 kB
// Copyright 2021 Fastly, Inc. import { CHARCODE, isCZeroControlPercentEncodeSet, isFragmentPercentEncodeSet, isQueryPercentEncodeSet, isPathPercentEncodeSet, isUserInfoPercentEncodeSet, } from "./charcode"; import { SPECIAL_SCHEMES, throwInvalidUrlError } from "./util"; import { URLParser } from "./url-parse"; import { URLProperties } from "./url-properties"; // A URL cannot have a username/password/port // if its host is null or the empty string, // its cannot-be-a-base-URL flag is set, or its scheme is "file". // https://url.spec.whatwg.org/#url-miscellaneous function urlCannotHaveUsernamePasswordPort(urlProps: URLProperties): boolean { if ( urlProps.hostname.length == 0 || !SPECIAL_SCHEMES.includes(urlProps.protocol) || urlProps.protocol == "file:" ) { return true; } return false; } // If this’s URL’s cannot-be-a-base-URL flag is set, then return. // https://url.spec.whatwg.org/#url-cannot-be-a-base-url-flag function urlCannotBeBaseUrl(urlProps: URLProperties): boolean { if (!SPECIAL_SCHEMES.includes(urlProps.protocol)) { return true; } return false; } export class URL { private _urlProps: URLProperties; constructor(url: string, baseUrl: string | null = null) { // Create our url properties this._urlProps = new URLProperties(); // Replace Forward Slashes with Back Slashes url = url.replaceAll("\\", "/"); let baseUrlString = ""; if (baseUrl !== null) { baseUrlString = (baseUrl as string).replaceAll("\\", "/"); } // First apply our baseUrl if it is absolute if (baseUrlString.length > 0) { URLParser.parseAbsoluteUrl(baseUrl as string, this._urlProps); // Check if our relative URL has a protocol let relativeUrlProtocol = ""; if ( !url.startsWith("//") && !url.includes("file:") && url.indexOf(":") > 0 ) { relativeUrlProtocol = url.slice(0, url.indexOf(":") + 1); } if ( relativeUrlProtocol != "" && !SPECIAL_SCHEMES.includes(relativeUrlProtocol) ) { // treat it as it's own absolute url URLParser.parseAbsoluteUrl(url, this._urlProps); } else { let relativeUrl = url; if (relativeUrlProtocol != "") { // Remove the protocol and pass a relative url relativeUrl = relativeUrl.slice(relativeUrl.indexOf(":") + 1); } URLParser.applySchemeOrPathRelativeUrl(relativeUrl, this._urlProps); } } else { // If we didn't have a baseUrl, the url has to be absolute if (!URLParser.isAbsoluteUrl(url)) { throw new Error( "The URL: " + url + " is a relative URL. You must also pass a baseUrl for relative URLs." ); return; } URLParser.parseAbsoluteUrl(url, this._urlProps); } // Run our URL validation // This will throw an error if the URL is invalid URLParser.validateUrl(this._urlProps); } // Getters and Setters for properties get protocol(): string { return this._urlProps.protocol; } set protocol(protocol: string) { if (!protocol.endsWith(":")) { protocol += ":"; } // Check if we are changing from a non-special protocol, // to a special protocol // https://nodejs.org/api/url.html#url_special_schemes if ( SPECIAL_SCHEMES.includes(this._urlProps.protocol) && !SPECIAL_SCHEMES.includes(protocol) ) { // Do Nothing } else if ( !SPECIAL_SCHEMES.includes(this._urlProps.protocol) && SPECIAL_SCHEMES.includes(protocol) ) { // Do Nothing } else { this._urlProps.protocol = protocol; } } get username(): string { return this._urlProps.username; } set username(username: string) { if (urlCannotHaveUsernamePasswordPort(this._urlProps)) { return; } this._urlProps.username = username; } get password(): string { return this._urlProps.password; } set password(password: string) { if (urlCannotHaveUsernamePasswordPort(this._urlProps)) { return; } this._urlProps.password = password; } get hostname(): string { // The hostname must be all lowercase let lowercaseHostname = ""; for (let i = 0; i < this._urlProps.hostname.length; i++) { let charcode = this._urlProps.hostname.charCodeAt(i); if ( charcode >= CHARCODE.UPPERCASE_A && charcode <= CHARCODE.UPPERCASE_Z ) { lowercaseHostname += String.fromCharCode(charcode + 32); } else { lowercaseHostname += this._urlProps.hostname.charAt(i); } } // We must also percent encode the hostname let encodedHostname = ""; for (let i = 0; i < lowercaseHostname.length; i++) { let charcode = lowercaseHostname.charCodeAt(i); if (isPathPercentEncodeSet(charcode)) { encodedHostname += "%" + charcode.toString(16); } else { encodedHostname += lowercaseHostname.charAt(i); } } return encodedHostname; } set hostname(hostname: string) { if (urlCannotBeBaseUrl(this._urlProps)) { return; } this._urlProps.hostname = hostname; } get port(): string { return this._urlProps.port; } set port(port: string) { if (urlCannotHaveUsernamePasswordPort(this._urlProps)) { return; } this._urlProps.port = port; } get pathname(): string { if (this._urlProps.pathname.length === 0) { return "/"; } // Percent encode out pathname let encodedPathname = ""; for (let i = 0; i < this._urlProps.pathname.length; i++) { let charcode = this._urlProps.pathname.charCodeAt(i); if (isPathPercentEncodeSet(charcode)) { encodedPathname += "%" + charcode.toString(16); } else { encodedPathname += this._urlProps.pathname.charAt(i); } } return encodedPathname; } set pathname(pathname: string) { if (urlCannotBeBaseUrl(this._urlProps)) { return; } this._urlProps.pathname = pathname; // Remove any trailing slash if (this._urlProps.pathname.endsWith("/")) { this._urlProps.pathname = this._urlProps.pathname.slice( 0, this._urlProps.pathname.length - 1 ); } } get search(): string { return this._urlProps.search; } set search(search: string) { if (!search.startsWith("?")) { this._urlProps.search = "?" + search; } else { this._urlProps.search = search; } } get hash(): string { return this._urlProps.hash; } set hash(hash: string) { if (!hash.startsWith("#")) { this._urlProps.hash = "#" + hash; } else { this._urlProps.hash = hash; } } get host(): string { if (this._urlProps.port.length > 0) { return this.hostname + ":" + this._urlProps.port; } return this.hostname; } set host(host: string) { if (urlCannotBeBaseUrl(this._urlProps)) { return; } if (host.includes(":")) { let splitHost = host.split(":"); this._urlProps.hostname = splitHost[0]; this._urlProps.port = splitHost[1]; } else { this._urlProps.hostname = host; } } get origin(): string { return this._urlProps.protocol + "//" + this.host; } get href(): string { let href = ""; // Add the protocol href += this._urlProps.protocol; if (SPECIAL_SCHEMES.includes(this._urlProps.protocol)) { href += "//"; } // Add the username and password if they exist if (this._urlProps.username.length > 0) { href += this._urlProps.username; if (this._urlProps.password.length > 0) { href += ":" + this._urlProps.password; } href += "@"; } // Add the host href += this.host; // Add the pathname href += this.pathname; if (!SPECIAL_SCHEMES.includes(this._urlProps.protocol)) { // Remove the trailing slash href = href.slice(0, href.length - 1); } // Add the search href += this._urlProps.search; // Add the hash href += this._urlProps.hash; return href; } set href(absoluteUrl: string) { URLParser.parseAbsoluteUrl(absoluteUrl, this._urlProps); } // Synonyms for href: https://developer.mozilla.org/en-US/docs/Web/API/URL#Methods toString(): string { return this.href; } toJSON(): string { return this.href; } }