summaryrefslogtreecommitdiff
path: root/cli/js/url.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/url.ts')
-rw-r--r--cli/js/url.ts376
1 files changed, 376 insertions, 0 deletions
diff --git a/cli/js/url.ts b/cli/js/url.ts
new file mode 100644
index 000000000..f22198da4
--- /dev/null
+++ b/cli/js/url.ts
@@ -0,0 +1,376 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as urlSearchParams from "./url_search_params.ts";
+import * as domTypes from "./dom_types.ts";
+import { getRandomValues } from "./get_random_values.ts";
+import { window } from "./window.ts";
+
+interface URLParts {
+ protocol: string;
+ username: string;
+ password: string;
+ hostname: string;
+ port: string;
+ path: string;
+ query: string | null;
+ hash: string;
+}
+
+const patterns = {
+ protocol: "(?:([^:/?#]+):)",
+ authority: "(?://([^/?#]*))",
+ path: "([^?#]*)",
+ query: "(\\?[^#]*)",
+ hash: "(#.*)",
+
+ authentication: "(?:([^:]*)(?::([^@]*))?@)",
+ hostname: "([^:]+)",
+ port: "(?::(\\d+))"
+};
+
+const urlRegExp = new RegExp(
+ `^${patterns.protocol}?${patterns.authority}?${patterns.path}${
+ patterns.query
+ }?${patterns.hash}?`
+);
+
+const authorityRegExp = new RegExp(
+ `^${patterns.authentication}?${patterns.hostname}${patterns.port}?$`
+);
+
+const searchParamsMethods: Array<keyof urlSearchParams.URLSearchParams> = [
+ "append",
+ "delete",
+ "set"
+];
+
+function parse(url: string): URLParts | undefined {
+ const urlMatch = urlRegExp.exec(url);
+ if (urlMatch) {
+ const [, , authority] = urlMatch;
+ const authorityMatch = authority
+ ? authorityRegExp.exec(authority)
+ : [null, null, null, null, null];
+ if (authorityMatch) {
+ return {
+ protocol: urlMatch[1] || "",
+ username: authorityMatch[1] || "",
+ password: authorityMatch[2] || "",
+ hostname: authorityMatch[3] || "",
+ port: authorityMatch[4] || "",
+ path: urlMatch[3] || "",
+ query: urlMatch[4] || "",
+ hash: urlMatch[5] || ""
+ };
+ }
+ }
+ return undefined;
+}
+
+// Based on https://github.com/kelektiv/node-uuid
+// TODO(kevinkassimo): Use deno_std version once possible.
+function generateUUID(): string {
+ return "00000000-0000-4000-8000-000000000000".replace(
+ /[0]/g,
+ (): string =>
+ // random integer from 0 to 15 as a hex digit.
+ (getRandomValues(new Uint8Array(1))[0] % 16).toString(16)
+ );
+}
+
+// Keep it outside of URL to avoid any attempts of access.
+export const blobURLMap = new Map<string, domTypes.Blob>();
+
+function isAbsolutePath(path: string): boolean {
+ return path.startsWith("/");
+}
+
+// Resolves `.`s and `..`s where possible.
+// Preserves repeating and trailing `/`s by design.
+function normalizePath(path: string): string {
+ const isAbsolute = isAbsolutePath(path);
+ path = path.replace(/^\//, "");
+ const pathSegments = path.split("/");
+
+ const newPathSegments: string[] = [];
+ for (let i = 0; i < pathSegments.length; i++) {
+ const previous = newPathSegments[newPathSegments.length - 1];
+ if (
+ pathSegments[i] == ".." &&
+ previous != ".." &&
+ (previous != undefined || isAbsolute)
+ ) {
+ newPathSegments.pop();
+ } else if (pathSegments[i] != ".") {
+ newPathSegments.push(pathSegments[i]);
+ }
+ }
+
+ let newPath = newPathSegments.join("/");
+ if (!isAbsolute) {
+ if (newPathSegments.length == 0) {
+ newPath = ".";
+ }
+ } else {
+ newPath = `/${newPath}`;
+ }
+ return newPath;
+}
+
+// Standard URL basing logic, applied to paths.
+function resolvePathFromBase(path: string, basePath: string): string {
+ const normalizedPath = normalizePath(path);
+ if (isAbsolutePath(normalizedPath)) {
+ return normalizedPath;
+ }
+ const normalizedBasePath = normalizePath(basePath);
+ if (!isAbsolutePath(normalizedBasePath)) {
+ throw new TypeError("Base path must be absolute.");
+ }
+
+ // Special case.
+ if (path == "") {
+ return normalizedBasePath;
+ }
+
+ // Remove everything after the last `/` in `normalizedBasePath`.
+ const prefix = normalizedBasePath.replace(/[^\/]*$/, "");
+ // If `normalizedPath` ends with `.` or `..`, add a trailing space.
+ const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/");
+
+ return normalizePath(prefix + suffix);
+}
+
+export class URL {
+ private _parts: URLParts;
+ private _searchParams!: urlSearchParams.URLSearchParams;
+
+ private _updateSearchParams(): void {
+ const searchParams = new urlSearchParams.URLSearchParams(this.search);
+
+ for (const methodName of searchParamsMethods) {
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ const method: (...args: any[]) => any = searchParams[methodName];
+ searchParams[methodName] = (...args: unknown[]): any => {
+ method.apply(searchParams, args);
+ this.search = searchParams.toString();
+ };
+ /* eslint-enable */
+ }
+ this._searchParams = searchParams;
+
+ // convert to `any` that has avoided the private limit
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (this._searchParams as any).url = this;
+ }
+
+ get hash(): string {
+ return this._parts.hash;
+ }
+
+ set hash(value: string) {
+ value = unescape(String(value));
+ if (!value) {
+ this._parts.hash = "";
+ } else {
+ if (value.charAt(0) !== "#") {
+ value = `#${value}`;
+ }
+ // hashes can contain % and # unescaped
+ this._parts.hash = escape(value)
+ .replace(/%25/g, "%")
+ .replace(/%23/g, "#");
+ }
+ }
+
+ get host(): string {
+ return `${this.hostname}${this.port ? `:${this.port}` : ""}`;
+ }
+
+ set host(value: string) {
+ value = String(value);
+ const url = new URL(`http://${value}`);
+ this._parts.hostname = url.hostname;
+ this._parts.port = url.port;
+ }
+
+ get hostname(): string {
+ return this._parts.hostname;
+ }
+
+ set hostname(value: string) {
+ value = String(value);
+ this._parts.hostname = encodeURIComponent(value);
+ }
+
+ get href(): string {
+ const authentication =
+ this.username || this.password
+ ? `${this.username}${this.password ? ":" + this.password : ""}@`
+ : "";
+
+ return `${this.protocol}//${authentication}${this.host}${this.pathname}${
+ this.search
+ }${this.hash}`;
+ }
+
+ set href(value: string) {
+ value = String(value);
+ if (value !== this.href) {
+ const url = new URL(value);
+ this._parts = { ...url._parts };
+ this._updateSearchParams();
+ }
+ }
+
+ get origin(): string {
+ return `${this.protocol}//${this.host}`;
+ }
+
+ get password(): string {
+ return this._parts.password;
+ }
+
+ set password(value: string) {
+ value = String(value);
+ this._parts.password = encodeURIComponent(value);
+ }
+
+ get pathname(): string {
+ return this._parts.path ? this._parts.path : "/";
+ }
+
+ set pathname(value: string) {
+ value = unescape(String(value));
+ if (!value || value.charAt(0) !== "/") {
+ value = `/${value}`;
+ }
+ // paths can contain % unescaped
+ this._parts.path = escape(value).replace(/%25/g, "%");
+ }
+
+ get port(): string {
+ return this._parts.port;
+ }
+
+ set port(value: string) {
+ const port = parseInt(String(value), 10);
+ this._parts.port = isNaN(port)
+ ? ""
+ : Math.max(0, port % 2 ** 16).toString();
+ }
+
+ get protocol(): string {
+ return `${this._parts.protocol}:`;
+ }
+
+ set protocol(value: string) {
+ value = String(value);
+ if (value) {
+ if (value.charAt(value.length - 1) === ":") {
+ value = value.slice(0, -1);
+ }
+ this._parts.protocol = encodeURIComponent(value);
+ }
+ }
+
+ get search(): string {
+ if (this._parts.query === null || this._parts.query === "") {
+ return "";
+ }
+
+ return this._parts.query;
+ }
+
+ set search(value: string) {
+ value = String(value);
+ let query: string | null;
+
+ if (value === "") {
+ query = null;
+ } else if (value.charAt(0) !== "?") {
+ query = `?${value}`;
+ } else {
+ query = value;
+ }
+
+ this._parts.query = query;
+ this._updateSearchParams();
+ }
+
+ get username(): string {
+ return this._parts.username;
+ }
+
+ set username(value: string) {
+ value = String(value);
+ this._parts.username = encodeURIComponent(value);
+ }
+
+ get searchParams(): urlSearchParams.URLSearchParams {
+ return this._searchParams;
+ }
+
+ constructor(url: string, base?: string | URL) {
+ let baseParts: URLParts | undefined;
+ if (base) {
+ baseParts = typeof base === "string" ? parse(base) : base._parts;
+ if (!baseParts || baseParts.protocol == "") {
+ throw new TypeError("Invalid base URL.");
+ }
+ }
+
+ const urlParts = parse(url);
+ if (!urlParts) {
+ throw new TypeError("Invalid URL.");
+ }
+
+ if (urlParts.protocol) {
+ this._parts = urlParts;
+ } else if (baseParts) {
+ this._parts = {
+ protocol: baseParts.protocol,
+ username: baseParts.username,
+ password: baseParts.password,
+ hostname: baseParts.hostname,
+ port: baseParts.port,
+ path: resolvePathFromBase(urlParts.path, baseParts.path || "/"),
+ query: urlParts.query,
+ hash: urlParts.hash
+ };
+ } else {
+ throw new TypeError("URL requires a base URL.");
+ }
+ this._updateSearchParams();
+ }
+
+ toString(): string {
+ return this.href;
+ }
+
+ toJSON(): string {
+ return this.href;
+ }
+
+ // TODO(kevinkassimo): implement MediaSource version in the future.
+ static createObjectURL(b: domTypes.Blob): string {
+ const origin = window.location.origin || "http://deno-opaque-origin";
+ const key = `blob:${origin}/${generateUUID()}`;
+ blobURLMap.set(key, b);
+ return key;
+ }
+
+ static revokeObjectURL(url: string): void {
+ let urlObject;
+ try {
+ urlObject = new URL(url);
+ } catch {
+ throw new TypeError("Provided URL string is not valid");
+ }
+ if (urlObject.protocol !== "blob:") {
+ return;
+ }
+ // Origin match check seems irrelevant for now, unless we implement
+ // persisten storage for per window.location.origin at some point.
+ blobURLMap.delete(url);
+ }
+}