@fastly/as-url
Version:
 
345 lines (281 loc) • 8.43 kB
text/typescript
// 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;
}
}