diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/shared/webdriver/URLPattern.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-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.mjs | 521 |
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; +} |