summaryrefslogtreecommitdiffstats
path: root/remote/shared/webdriver/URLPattern.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /remote/shared/webdriver/URLPattern.sys.mjs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/shared/webdriver/URLPattern.sys.mjs')
-rw-r--r--remote/shared/webdriver/URLPattern.sys.mjs521
1 files changed, 521 insertions, 0 deletions
diff --git a/remote/shared/webdriver/URLPattern.sys.mjs b/remote/shared/webdriver/URLPattern.sys.mjs
new file mode 100644
index 0000000000..0033cced66
--- /dev/null
+++ b/remote/shared/webdriver/URLPattern.sys.mjs
@@ -0,0 +1,521 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+});
+
+/**
+ * Parsed pattern to use for URL matching.
+ *
+ * @typedef {object} ParsedURLPattern
+ * @property {string|null} protocol
+ * The protocol, for instance "https".
+ * @property {string|null} hostname
+ * The hostname, for instance "example.com".
+ * @property {string|null} port
+ * The serialized port. Empty string for default ports of special schemes.
+ * @property {string|null} path
+ * The path, starting with "/".
+ * @property {string|null} search
+ * The search query string, without the leading "?"
+ */
+
+/**
+ * Subset of properties extracted from a parsed URL.
+ *
+ * @typedef {object} ParsedURL
+ * @property {string=} host
+ * @property {string|Array<string>} path
+ * Either a string if the path is an opaque path, or an array of strings
+ * (path segments).
+ * @property {number=} port
+ * @property {string=} query
+ * @property {string=} scheme
+ */
+
+/**
+ * Enum of URLPattern types.
+ *
+ * @readonly
+ * @enum {URLPatternType}
+ */
+const URLPatternType = {
+ Pattern: "pattern",
+ String: "string",
+};
+
+const supportedURLPatternTypes = Object.values(URLPatternType);
+
+const SPECIAL_SCHEMES = ["file", "http", "https", "ws", "wss"];
+const DEFAULT_PORTS = {
+ file: null,
+ http: 80,
+ https: 443,
+ ws: 80,
+ wss: 443,
+};
+
+/**
+ * Check if a given URL pattern is compatible with the provided URL.
+ *
+ * Implements https://w3c.github.io/webdriver-bidi/#match-url-pattern
+ *
+ * @param {ParsedURLPattern} urlPattern
+ * The URL pattern to match.
+ * @param {string} url
+ * The string representation of a URL to test against the pattern.
+ *
+ * @returns {boolean}
+ * True if the pattern is compatible with the provided URL, false otherwise.
+ */
+export function matchURLPattern(urlPattern, url) {
+ const parsedURL = parseURL(url);
+
+ if (urlPattern.protocol !== null && urlPattern.protocol != parsedURL.scheme) {
+ return false;
+ }
+
+ if (urlPattern.hostname !== null && urlPattern.hostname != parsedURL.host) {
+ return false;
+ }
+
+ if (urlPattern.port !== null && urlPattern.port != serializePort(parsedURL)) {
+ return false;
+ }
+
+ if (
+ urlPattern.pathname !== null &&
+ urlPattern.pathname != serializePath(parsedURL)
+ ) {
+ return false;
+ }
+
+ if (urlPattern.search !== null) {
+ const urlQuery = parsedURL.query === null ? "" : parsedURL.query;
+ if (urlPattern.search != urlQuery) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Parse a URLPattern into a parsed pattern object which can be used to match
+ * URLs using `matchURLPattern`.
+ *
+ * Implements https://w3c.github.io/webdriver-bidi/#parse-url-pattern
+ *
+ * @param {URLPattern} pattern
+ * The pattern to parse.
+ *
+ * @returns {ParsedURLPattern}
+ * The parsed URL pattern.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ * @throws {UnsupportedOperationError}
+ * Raised if the pattern uses a protocol not supported by Firefox.
+ */
+export function parseURLPattern(pattern) {
+ lazy.assert.object(
+ pattern,
+ `Expected url pattern to be an object, got ${pattern}`
+ );
+
+ let hasProtocol = true;
+ let hasHostname = true;
+ let hasPort = true;
+ let hasPathname = true;
+ let hasSearch = true;
+
+ let patternUrl;
+ switch (pattern.type) {
+ case URLPatternType.Pattern:
+ patternUrl = "";
+ if ("protocol" in pattern) {
+ patternUrl += parseProtocol(pattern.protocol);
+ } else {
+ hasProtocol = false;
+ patternUrl += "http";
+ }
+
+ const scheme = patternUrl.toLowerCase();
+ patternUrl += ":";
+ if (SPECIAL_SCHEMES.includes(scheme)) {
+ patternUrl += "//";
+ }
+
+ if ("hostname" in pattern) {
+ patternUrl += parseHostname(pattern.hostname, scheme);
+ } else {
+ if (scheme != "file") {
+ patternUrl += "placeholder";
+ }
+ hasHostname = false;
+ }
+
+ if ("port" in pattern) {
+ patternUrl += parsePort(pattern.port);
+ } else {
+ hasPort = false;
+ }
+
+ if ("pathname" in pattern) {
+ patternUrl += parsePathname(pattern.pathname);
+ } else {
+ hasPathname = false;
+ }
+
+ if ("search" in pattern) {
+ patternUrl += parseSearch(pattern.search);
+ } else {
+ hasSearch = false;
+ }
+ break;
+ case URLPatternType.String:
+ lazy.assert.string(
+ pattern.pattern,
+ `Expected "urlPattern" of type "string" to have a string "pattern" property, got ${pattern.pattern}`
+ );
+ patternUrl = unescapeUrlPattern(pattern.pattern);
+ break;
+ default:
+ throw new lazy.error.InvalidArgumentError(
+ `Expected "urlPattern" type to be one of ${supportedURLPatternTypes}, got ${pattern.type}`
+ );
+ }
+
+ if (!URL.canParse(patternUrl)) {
+ throw new lazy.error.InvalidArgumentError(
+ `Unable to parse URL "${patternUrl}"`
+ );
+ }
+
+ let parsedURL;
+ try {
+ parsedURL = parseURL(patternUrl);
+ } catch (e) {
+ throw new lazy.error.InvalidArgumentError(
+ `Failed to parse URL "${patternUrl}"`
+ );
+ }
+
+ if (hasProtocol && !SPECIAL_SCHEMES.includes(parsedURL.scheme)) {
+ throw new lazy.error.UnsupportedOperationError(
+ `URL pattern did not specify a supported protocol (one of ${SPECIAL_SCHEMES}), got ${parsedURL.scheme}`
+ );
+ }
+
+ return {
+ protocol: hasProtocol ? parsedURL.scheme : null,
+ hostname: hasHostname ? parsedURL.host : null,
+ port: hasPort ? serializePort(parsedURL) : null,
+ pathname:
+ hasPathname && parsedURL.path.length ? serializePath(parsedURL) : null,
+ search: hasSearch ? parsedURL.query || "" : null,
+ };
+}
+
+/**
+ * Parse the hostname property of a URLPatternPattern.
+ *
+ * @param {string} hostname
+ * A hostname property.
+ * @param {string} scheme
+ * The scheme for the URLPatternPattern.
+ *
+ * @returns {string}
+ * The parsed property.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ */
+function parseHostname(hostname, scheme) {
+ if (typeof hostname != "string" || hostname == "") {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected URLPattern "hostname" to be a non-empty string, got ${hostname}`
+ );
+ }
+
+ if (scheme == "file") {
+ throw new lazy.error.InvalidArgumentError(
+ `URLPattern with "file" scheme cannot specify a hostname, got ${hostname}`
+ );
+ }
+
+ hostname = unescapeUrlPattern(hostname);
+
+ const forbiddenHostnameCharacters = ["/", "?", "#"];
+ let insideBrackets = false;
+ for (const codepoint of hostname) {
+ if (
+ forbiddenHostnameCharacters.includes(codepoint) ||
+ (!insideBrackets && codepoint == ":")
+ ) {
+ throw new lazy.error.InvalidArgumentError(
+ `URL pattern "hostname" contained a forbidden character, got "${hostname}"`
+ );
+ }
+
+ if (codepoint == "[") {
+ insideBrackets = true;
+ } else if (codepoint == "]") {
+ insideBrackets = false;
+ }
+ }
+
+ return hostname;
+}
+
+/**
+ * Parse the pathname property of a URLPatternPattern.
+ *
+ * @param {string} pathname
+ * A pathname property.
+ *
+ * @returns {string}
+ * The parsed property.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ */
+function parsePathname(pathname) {
+ lazy.assert.string(
+ pathname,
+ `Expected URLPattern "pathname" to be a string, got ${pathname}`
+ );
+
+ pathname = unescapeUrlPattern(pathname);
+ if (!pathname.startsWith("/")) {
+ pathname = `/${pathname}`;
+ }
+
+ if (pathname.includes("?") || pathname.includes("#")) {
+ throw new lazy.error.InvalidArgumentError(
+ `URL pattern "pathname" contained a forbidden character, got "${pathname}"`
+ );
+ }
+
+ return pathname;
+}
+
+/**
+ * Parse the port property of a URLPatternPattern.
+ *
+ * @param {string} port
+ * A port property.
+ *
+ * @returns {string}
+ * The parsed property.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ */
+function parsePort(port) {
+ if (typeof port != "string" || port == "") {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected URLPattern "port" to be a non-empty string, got ${port}`
+ );
+ }
+
+ port = unescapeUrlPattern(port);
+
+ const isNumber = /^\d*$/.test(port);
+ if (!isNumber) {
+ throw new lazy.error.InvalidArgumentError(
+ `URL pattern "port" is not a valid number, got "${port}"`
+ );
+ }
+
+ return `:${port}`;
+}
+
+/**
+ * Parse the protocol property of a URLPatternPattern.
+ *
+ * @param {string} protocol
+ * A protocol property.
+ *
+ * @returns {string}
+ * The parsed property.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ */
+function parseProtocol(protocol) {
+ if (typeof protocol != "string" || protocol == "") {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected URLPattern "protocol" to be a non-empty string, got ${protocol}`
+ );
+ }
+
+ protocol = unescapeUrlPattern(protocol);
+ if (!/^[a-zA-Z0-9+-.]*$/.test(protocol)) {
+ throw new lazy.error.InvalidArgumentError(
+ `URL pattern "protocol" contained a forbidden character, got "${protocol}"`
+ );
+ }
+
+ return protocol;
+}
+
+/**
+ * Parse the search property of a URLPatternPattern.
+ *
+ * @param {string} search
+ * A search property.
+ *
+ * @returns {string}
+ * The parsed property.
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ */
+function parseSearch(search) {
+ lazy.assert.string(
+ search,
+ `Expected URLPattern "search" to be a string, got ${search}`
+ );
+
+ search = unescapeUrlPattern(search);
+ if (!search.startsWith("?")) {
+ search = `?${search}`;
+ }
+
+ if (search.includes("#")) {
+ throw new lazy.error.InvalidArgumentError(
+ `Expected URLPattern "search" to never contain "#", got ${search}`
+ );
+ }
+
+ return search;
+}
+
+/**
+ * Parse a string URL. This tries to be close to Basic URL Parser, however since
+ * this is not currently implemented in Firefox and URL parsing has many edge
+ * cases, it does not try to be a faithful implementation.
+ *
+ * Edge cases which are not supported are mostly about non-special URLs, which
+ * in practice should not be observable in automation.
+ *
+ * @param {string} url
+ * The string based URL to parse.
+ * @returns {ParsedURL}
+ * The parsed URL.
+ */
+function parseURL(url) {
+ const urlObj = new URL(url);
+ const uri = urlObj.URI;
+
+ return {
+ scheme: uri.scheme,
+ // Note: Use urlObj instead of uri for hostname:
+ // nsIURI removes brackets from ipv6 hostnames (eg [::1] becomes ::1).
+ host: urlObj.hostname,
+ path: uri.filePath,
+ // Note: Use urlObj instead of uri for port:
+ // nsIURI throws on the port getter for non-special schemes.
+ port: urlObj.port != "" ? Number(uri.port) : null,
+ query: uri.hasQuery ? uri.query : null,
+ };
+}
+
+/**
+ * Serialize the path of a parsed URL.
+ *
+ * @see https://pr-preview.s3.amazonaws.com/w3c/webdriver-bidi/pull/429.html#parse-url-pattern
+ *
+ * @param {ParsedURL} url
+ * A parsed url.
+ *
+ * @returns {string}
+ * The serialized path
+ */
+function serializePath(url) {
+ // Check for opaque path
+ if (typeof url.path == "string") {
+ return url.path;
+ }
+
+ let serialized = "";
+ for (const segment of url.path) {
+ serialized += `/${segment}`;
+ }
+
+ return serialized;
+}
+
+/**
+ * Serialize the port of a parsed URL.
+ *
+ * @see https://pr-preview.s3.amazonaws.com/w3c/webdriver-bidi/pull/429.html#parse-url-pattern
+ *
+ * @param {ParsedURL} url
+ * A parsed url.
+ *
+ * @returns {string}
+ * The serialized port
+ */
+function serializePort(url) {
+ let port = null;
+ if (
+ SPECIAL_SCHEMES.includes(url.scheme) &&
+ DEFAULT_PORTS[url.scheme] !== null &&
+ (url.port === null || url.port == DEFAULT_PORTS[url.scheme])
+ ) {
+ port = "";
+ } else if (url.port !== null) {
+ port = `${url.port}`;
+ }
+
+ return port;
+}
+
+/**
+ * Unescape and check a pattern string against common forbidden characters.
+ *
+ * @see https://pr-preview.s3.amazonaws.com/w3c/webdriver-bidi/pull/429.html#unescape-url-pattern
+ *
+ * @param {string} pattern
+ * Either a full URLPatternString pattern or a property of a URLPatternPattern.
+ *
+ * @returns {string}
+ * The unescaped pattern
+ *
+ * @throws {InvalidArgumentError}
+ * Raised if an argument is of an invalid type or value.
+ */
+function unescapeUrlPattern(pattern) {
+ const forbiddenCharacters = ["(", ")", "*", "{", "}"];
+ const escapeCharacter = "\\";
+
+ let isEscaped = false;
+ let result = "";
+
+ for (const codepoint of Array.from(pattern)) {
+ if (!isEscaped) {
+ if (forbiddenCharacters.includes(codepoint)) {
+ throw new lazy.error.InvalidArgumentError(
+ `URL pattern contained an unescaped forbidden character ${codepoint}`
+ );
+ }
+
+ if (codepoint == escapeCharacter) {
+ isEscaped = true;
+ continue;
+ }
+ }
+
+ result += codepoint;
+ isEscaped = false;
+ }
+
+ return result;
+}