mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			521 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			521 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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;
 | 
						|
}
 |