Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/toolkit/components/extensions/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 114 kB image not shown  

Quelle  Schemas.sys.mjs   Sprache: unbekannt

 
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";

var { DefaultMap, DefaultWeakMap } = ExtensionUtils;

/** @type {Lazy} */
const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
  ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "contentPolicyService",
  "@mozilla.org/addons/content-policy;1",
  "nsIAddonContentPolicy"
);

ChromeUtils.defineLazyGetter(
  lazy,
  "StartupCache",
  () => lazy.ExtensionParent.StartupCache
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "treatWarningsAsErrors",
  "extensions.webextensions.warnings-as-errors",
  false
);

const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";

const MIN_MANIFEST_VERSION = 2;
const MAX_MANIFEST_VERSION = 3;

const { DEBUG } = AppConstants;

const isParentProcess =
  Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;

function readJSON(url) {
  return new Promise((resolve, reject) => {
    lazy.NetUtil.asyncFetch(
      { uri: url, loadUsingSystemPrincipal: true },
      (inputStream, status) => {
        if (!Components.isSuccessCode(status)) {
          // Convert status code to a string
          let e = Components.Exception("", status);
          reject(new Error(`Error while loading '${url}' (${e.name})`));
          return;
        }
        try {
          let text = lazy.NetUtil.readInputStreamToString(
            inputStream,
            inputStream.available()
          );

          // Chrome JSON files include a license comment that we need to
          // strip off for this to be valid JSON. As a hack, we just
          // look for the first '[' character, which signals the start
          // of the JSON content.
          let index = text.indexOf("[");
          text = text.slice(index);

          resolve(JSON.parse(text));
        } catch (e) {
          reject(e);
        }
      }
    );
  });
}

function stripDescriptions(json, stripThis = true) {
  if (Array.isArray(json)) {
    for (let i = 0; i < json.length; i++) {
      if (typeof json[i] === "object" && json[i] !== null) {
        json[i] = stripDescriptions(json[i]);
      }
    }
    return json;
  }

  let result = {};

  // Objects are handled much more efficiently, both in terms of memory and
  // CPU, if they have the same shape as other objects that serve the same
  // purpose. So, normalize the order of properties to increase the chances
  // that the majority of schema objects wind up in large shape groups.
  for (let key of Object.keys(json).sort()) {
    if (stripThis && key === "description" && typeof json[key] === "string") {
      continue;
    }

    if (typeof json[key] === "object" && json[key] !== null) {
      result[key] = stripDescriptions(json[key], key !== "properties");
    } else {
      result[key] = json[key];
    }
  }

  return result;
}

function blobbify(json) {
  // We don't actually use descriptions at runtime, and they make up about a
  // third of the size of our structured clone data, so strip them before
  // blobbifying.
  json = stripDescriptions(json);

  return new StructuredCloneHolder("Schemas/blobbify", null, json);
}

async function readJSONAndBlobbify(url) {
  let json = await readJSON(url);

  return blobbify(json);
}

/**
 * Defines a lazy getter for the given property on the given object. Any
 * security wrappers are waived on the object before the property is
 * defined, and the getter and setter methods are wrapped for the target
 * scope.
 *
 * The given getter function is guaranteed to be called only once, even
 * if the target scope retrieves the wrapped getter from the property
 * descriptor and calls it directly.
 *
 * @param {object} object
 *        The object on which to define the getter.
 * @param {string | symbol} prop
 *        The property name for which to define the getter.
 * @param {Function} getter
 *        The function to call in order to generate the final property
 *        value.
 */
function exportLazyGetter(object, prop, getter) {
  object = ChromeUtils.waiveXrays(object);

  let redefine = value => {
    if (value === undefined) {
      delete object[prop];
    } else {
      Object.defineProperty(object, prop, {
        enumerable: true,
        configurable: true,
        writable: true,
        value,
      });
    }

    getter = null;

    return value;
  };

  Object.defineProperty(object, prop, {
    enumerable: true,
    configurable: true,

    get: Cu.exportFunction(function () {
      return redefine(getter.call(this));
    }, object),

    set: Cu.exportFunction(value => {
      redefine(value);
    }, object),
  });
}

/**
 * Defines a lazily-instantiated property descriptor on the given
 * object. Any security wrappers are waived on the object before the
 * property is defined.
 *
 * The given getter function is guaranteed to be called only once, even
 * if the target scope retrieves the wrapped getter from the property
 * descriptor and calls it directly.
 *
 * @param {object} object
 *        The object on which to define the getter.
 * @param {string | symbol} prop
 *        The property name for which to define the getter.
 * @param {Function} getter
 *        The function to call in order to generate the final property
 *        descriptor object. This will be called, and the property
 *        descriptor installed on the object, the first time the
 *        property is written or read. The function may return
 *        undefined, which will cause the property to be deleted.
 */
function exportLazyProperty(object, prop, getter) {
  object = ChromeUtils.waiveXrays(object);

  let redefine = obj => {
    let desc = getter.call(obj);
    getter = null;

    delete object[prop];
    if (desc) {
      let defaults = {
        configurable: true,
        enumerable: true,
      };

      if (!desc.set && !desc.get) {
        defaults.writable = true;
      }

      Object.defineProperty(object, prop, Object.assign(defaults, desc));
    }
  };

  Object.defineProperty(object, prop, {
    enumerable: true,
    configurable: true,

    get: Cu.exportFunction(function () {
      redefine(this);
      return object[prop];
    }, object),

    set: Cu.exportFunction(function (value) {
      redefine(this);
      object[prop] = value;
    }, object),
  });
}

const POSTPROCESSORS = {
  convertImageDataToURL(imageData, context) {
    let document = context.cloneScope.document;
    let canvas = document.createElementNS(
      "http://www.w3.org/1999/xhtml",
      "canvas"
    );
    canvas.width = imageData.width;
    canvas.height = imageData.height;
    canvas.getContext("2d").putImageData(imageData, 0, 0);

    return canvas.toDataURL("image/png");
  },
  mutuallyExclusiveBlockingOrAsyncBlocking(value, context) {
    if (!Array.isArray(value)) {
      return value;
    }
    if (value.includes("blocking") && value.includes("asyncBlocking")) {
      throw new context.cloneScope.Error(
        "'blocking' and 'asyncBlocking' are mutually exclusive"
      );
    }
    return value;
  },
  webRequestBlockingPermissionRequired(string, context) {
    if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
      throw new context.cloneScope.Error(
        "Using webRequest.addListener with the " +
          "blocking option requires the 'webRequestBlocking' permission."
      );
    }

    return string;
  },
  webRequestBlockingOrAuthProviderPermissionRequired(string, context) {
    if (
      string === "blocking" &&
      !(
        context.hasPermission("webRequestBlocking") ||
        context.hasPermission("webRequestAuthProvider")
      )
    ) {
      throw new context.cloneScope.Error(
        "Using webRequest.onAuthRequired.addListener with the " +
          "blocking option requires either the 'webRequestBlocking' " +
          "or 'webRequestAuthProvider' permission."
      );
    }

    return string;
  },
  checkRequiredManifestBackgroundKeys(value, context) {
    const serviceWorkerEnabled =
      WebExtensionPolicy.backgroundServiceWorkerEnabled;

    // At least one environment is required
    if (!value.page && !value.scripts?.length) {
      if (!value.service_worker) {
        // Add an error to the manifest validations and throw the
        // same error.
        const msg = `background requires at least one of ${
          serviceWorkerEnabled ? '"service_worker", ' : ""
        }"scripts" or "page".`;
        context.logError(context.makeError(msg));
        throw new Error(msg);
      } else if (!serviceWorkerEnabled) {
        // throw if only service_worker is specified and not enabled
        const msg =
          "background.service_worker is currently disabled. Add background.scripts.";
        context.logError(context.makeError(msg));
        throw new Error(msg);
      }
    }

    return value;
  },

  manifestVersionCheck(value, context) {
    if (
      value == 2 ||
      (value == 3 &&
        Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
    ) {
      return value;
    }
    const msg = `Unsupported manifest version: ${value}`;
    context.logError(context.makeError(msg));
    throw new Error(msg);
  },

  webAccessibleMatching(value, context) {
    // Ensure each object has at least one of matches or extension_ids array.
    for (let obj of value) {
      if (!obj.matches && !obj.extension_ids) {
        const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
        context.logError(context.makeError(msg));
        throw new Error(msg);
      }
    }
    return value;
  },

  incognitoSplitUnsupportedAndFallback(value, context) {
    if (value === "split") {
      // incognito:split has not been implemented (bug 1380812). There are two
      // alternatives: "spanning" and "not_allowed".
      //
      // "incognito":"split" is required by Chrome when extensions want to load
      // any extension page in a tab in Chrome. In Firefox that is not required,
      // so extensions could replace "split" with "spanning".
      // Another (poorly documented) effect of "incognito":"split" is separation
      // of some state between some extension APIs. Because this can in theory
      // result in unwanted mixing of state between private and non-private
      // browsing, we fall back to "not_allowed", which prevents the user from
      // enabling the extension in private browsing windows.
      value = "not_allowed";
      context.logWarning(
        `incognito "split" is unsupported. Falling back to incognito "${value}".`
      );
    }
    return value;
  },
};

// Parses a regular expression, with support for the Python extended
// syntax that allows setting flags by including the string (?im)
function parsePattern(pattern) {
  let flags = "";
  let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
  if (match) {
    [, flags, pattern] = match;
  }
  return new RegExp(pattern, flags);
}

function getValueBaseType(value) {
  let type = typeof value;
  switch (type) {
    case "object":
      if (value === null) {
        return "null";
      }
      if (Array.isArray(value)) {
        return "array";
      }
      break;

    case "number":
      if (value % 1 === 0) {
        return "integer";
      }
  }
  return type;
}

// Methods of Context that are used by Schemas.normalize. These methods can be
// overridden at the construction of Context.
const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];

// Methods of Context that are used by Schemas.inject.
// Callers of Schemas.inject should implement all of these methods.
const CONTEXT_FOR_INJECTION = [
  ...CONTEXT_FOR_VALIDATION,
  "getImplementation",
  "isPermissionRevokable",
  "shouldInject",
];

// If the message is a function, call it and return the result.
// Otherwise, assume it's a string.
function forceString(msg) {
  if (typeof msg === "function") {
    return msg();
  }
  return msg;
}

/**
 * A context for schema validation and error reporting. This class is only used
 * internally within Schemas.
 */
class Context {
  /**
   * @param {object} params Provides the implementation of this class.
   * @param {Array<string>} overridableMethods
   */
  constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
    this.params = params;

    if (typeof params.manifestVersion !== "number") {
      throw new Error(
        `Unexpected params.manifestVersion value: ${params.manifestVersion}`
      );
    }

    this.path = [];
    this.preprocessors = {
      localize(value) {
        return value;
      },
      ...params.preprocessors,
    };

    this.postprocessors = POSTPROCESSORS;
    this.isChromeCompat = params.isChromeCompat ?? false;
    this.manifestVersion = params.manifestVersion;

    this.currentChoices = new Set();
    this.choicePathIndex = 0;

    for (let method of overridableMethods) {
      if (method in params) {
        this[method] = params[method].bind(params);
      }
    }
  }

  get choicePath() {
    let path = this.path.slice(this.choicePathIndex);
    return path.join(".");
  }

  get cloneScope() {
    return this.params.cloneScope || undefined;
  }

  get url() {
    return this.params.url;
  }

  get ignoreUnrecognizedProperties() {
    return !!this.params.ignoreUnrecognizedProperties;
  }

  get principal() {
    return (
      this.params.principal ||
      Services.scriptSecurityManager.createNullPrincipal({})
    );
  }

  /**
   * Checks whether `url` may be loaded by the extension in this context.
   *
   * @param {string} url The URL that the extension wished to load.
   * @returns {boolean} Whether the context may load `url`.
   */
  checkLoadURL(url) {
    let ssm = Services.scriptSecurityManager;
    try {
      ssm.checkLoadURIWithPrincipal(
        this.principal,
        Services.io.newURI(url),
        ssm.DISALLOW_INHERIT_PRINCIPAL
      );
    } catch (e) {
      return false;
    }
    return true;
  }

  /**
   * Checks whether this context has the given permission.
   *
   * @param {string} _permission
   *        The name of the permission to check.
   *
   * @returns {boolean} True if the context has the given permission.
   */
  hasPermission(_permission) {
    return false;
  }

  /**
   * Checks whether the given permission can be dynamically revoked or
   * granted.
   *
   * @param {string} _permission
   *        The name of the permission to check.
   *
   * @returns {boolean} True if the given permission is revokable.
   */
  isPermissionRevokable(_permission) {
    return false;
  }

  /**
   * Returns an error result object with the given message, for return
   * by Type normalization functions.
   *
   * If the context has a `currentTarget` value, this is prepended to
   * the message to indicate the location of the error.
   *
   * @param {string | Function} errorMessage
   *        The error message which will be displayed when this is the
   *        only possible matching schema. If a function is passed, it
   *        will be evaluated when the error string is first needed, and
   *        must return a string.
   * @param {string | Function} choicesMessage
   *        The message describing the valid what constitutes a valid
   *        value for this schema, which will be displayed when multiple
   *        schema choices are available and none match.
   *
   *        A caller may pass `null` to prevent a choice from being
   *        added, but this should *only* be done from code processing a
   *        choices type.
   * @param {boolean} [warning = false]
   *        If true, make message prefixed `Warning`. If false, make message
   *        prefixed `Error`
   * @returns {object}
   */
  error(errorMessage, choicesMessage = undefined, warning = false) {
    if (choicesMessage !== null) {
      let { choicePath } = this;
      if (choicePath) {
        choicesMessage = `.${choicePath} must ${choicesMessage}`;
      }

      this.currentChoices.add(choicesMessage);
    }

    if (this.currentTarget) {
      let { currentTarget } = this;
      return {
        error: () =>
          `${
            warning ? "Warning" : "Error"
          } processing ${currentTarget}: ${forceString(errorMessage)}`,
      };
    }
    return { error: errorMessage };
  }

  /**
   * Creates an `Error` object belonging to the current unprivileged
   * scope. If there is no unprivileged scope associated with this
   * context, the message is returned as a string.
   *
   * If the context has a `currentTarget` value, this is prepended to
   * the message, in the same way as for the `error` method.
   *
   * @param {string} message
   * @param {object} [options]
   * @param {boolean} [options.warning = false]
   * @returns {Error}
   */
  makeError(message, { warning = false } = {}) {
    let error = forceString(this.error(message, null, warning).error);
    if (this.cloneScope) {
      return new this.cloneScope.Error(error);
    }
    return error;
  }

  /**
   * Logs the given error to the console. May be overridden to enable
   * custom logging.
   *
   * @param {Error|string} error
   */
  logError(error) {
    if (this.cloneScope) {
      Cu.reportError(
        // Error objects logged using Cu.reportError are not associated
        // to the related innerWindowID. This results in a leaked docshell
        // since consoleService cannot release the error object when the
        // extension global is destroyed.
        typeof error == "string" ? error : String(error),
        // Report the error with the appropriate stack trace when the
        // is related to an actual extension global (instead of being
        // related to a manifest validation).
        this.principal && ChromeUtils.getCallerLocation(this.principal)
      );
    } else {
      Cu.reportError(error);
    }
  }

  /**
   * Logs a warning. An error might be thrown when we treat warnings as errors.
   *
   * @param {string} warningMessage
   */
  logWarning(warningMessage) {
    let error = this.makeError(warningMessage, { warning: true });
    this.logError(error);

    if (lazy.treatWarningsAsErrors) {
      // This pref is false by default, and true by default in tests to
      // discourage the use of deprecated APIs in our unit tests.
      // If a warning is an expected part of a test, temporarily set the pref
      // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
      Services.console.logStringMessage(
        "Treating warning as error because the preference " +
          "extensions.webextensions.warnings-as-errors is set to true"
      );
      if (typeof error === "string") {
        error = new Error(error);
      }
      throw error;
    }
  }

  /**
   * Returns the name of the value currently being normalized. For a
   * nested object, this is usually approximately equivalent to the
   * JavaScript property accessor for that property. Given:
   *
   *   { foo: { bar: [{ baz: x }] } }
   *
   * When processing the value for `x`, the currentTarget is
   * 'foo.bar.0.baz'
   */
  get currentTarget() {
    return this.path.join(".");
  }

  /**
   * Executes the given callback, and returns an array of choice strings
   * passed to {@see #error} during its execution.
   *
   * @param {Function} callback
   * @returns {object}
   *          An object with a `result` property containing the return
   *          value of the callback, and a `choice` property containing
   *          an array of choices.
   */
  withChoices(callback) {
    let { currentChoices, choicePathIndex } = this;

    let choices = new Set();
    this.currentChoices = choices;
    this.choicePathIndex = this.path.length;

    try {
      let result = callback();

      return { result, choices };
    } finally {
      this.currentChoices = currentChoices;
      this.choicePathIndex = choicePathIndex;

      if (choices.size == 1) {
        for (let choice of choices) {
          currentChoices.add(choice);
        }
      } else if (choices.size) {
        this.error(null, () => {
          let array = Array.from(choices, forceString);
          let n = array.length - 1;
          array[n] = `or ${array[n]}`;

          return `must either [${array.join(", ")}]`;
        });
      }
    }
  }

  /**
   * Appends the given component to the `currentTarget` path to indicate
   * that it is being processed, calls the given callback function, and
   * then restores the original path.
   *
   * This is used to identify the path of the property being processed
   * when reporting type errors.
   *
   * @param {string} component
   * @param {Function} callback
   * @returns {*}
   */
  withPath(component, callback) {
    this.path.push(component);
    try {
      return callback();
    } finally {
      this.path.pop();
    }
  }

  matchManifestVersion(entry) {
    let { manifestVersion } = this;
    return (
      manifestVersion >= entry.min_manifest_version &&
      manifestVersion <= entry.max_manifest_version
    );
  }
}

/**
 * Represents a schema entry to be injected into an object. Handles the
 * injection, revocation, and permissions of said entry.
 *
 * @param {InjectionContext} context
 *        The injection context for the entry.
 * @param {Entry} entry
 *        The entry to inject.
 * @param {object} parentObject
 *        The object into which to inject this entry.
 * @param {string} name
 *        The property name at which to inject this entry.
 * @param {Array<string>} path
 *        The full path from the root entry to this entry.
 * @param {Entry} parentEntry
 *        The parent entry for the injected entry.
 */
class InjectionEntry {
  constructor(context, entry, parentObj, name, path, parentEntry) {
    this.context = context;
    this.entry = entry;
    this.parentObj = parentObj;
    this.name = name;
    this.path = path;
    this.parentEntry = parentEntry;

    this.injected = null;
    this.lazyInjected = null;
  }

  /**
   * @property {Array<string>} allowedContexts
   *        The list of allowed contexts into which the entry may be
   *        injected.
   */
  get allowedContexts() {
    let { allowedContexts } = this.entry;
    if (allowedContexts.length) {
      return allowedContexts;
    }
    return this.parentEntry.defaultContexts;
  }

  /**
   * @property {boolean} isRevokable
   *        Returns true if this entry may be dynamically injected or
   *        revoked based on its permissions.
   */
  get isRevokable() {
    return (
      this.entry.permissions &&
      this.entry.permissions.some(perm =>
        this.context.isPermissionRevokable(perm)
      )
    );
  }

  /**
   * @property {boolean} hasPermission
   *        Returns true if the injection context currently has the
   *        appropriate permissions to access this entry.
   */
  get hasPermission() {
    return (
      !this.entry.permissions ||
      this.entry.permissions.some(perm => this.context.hasPermission(perm))
    );
  }

  /**
   * @property {boolean} shouldInject
   *        Returns true if this entry should be injected in the given
   *        context, without respect to permissions.
   */
  get shouldInject() {
    return (
      this.context.matchManifestVersion(this.entry) &&
      this.context.shouldInject(
        this.path.join("."),
        this.name,
        this.allowedContexts
      )
    );
  }

  /**
   * Revokes this entry, removing its property from its parent object,
   * and invalidating its wrappers.
   */
  revoke() {
    if (this.lazyInjected) {
      this.lazyInjected = false;
    } else if (this.injected) {
      if (this.injected.revoke) {
        this.injected.revoke();
      }

      try {
        let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
        delete unwrapped[this.name];
      } catch (e) {
        Cu.reportError(e);
      }

      let { value } = this.injected.descriptor;
      if (value) {
        this.context.revokeChildren(value);
      }

      this.injected = null;
    }
  }

  /**
   * Returns a property descriptor object for this entry, if it should
   * be injected, or undefined if it should not.
   *
   * @returns {object?}
   *        A property descriptor object, or undefined if the property
   *        should be removed.
   */
  getDescriptor() {
    this.lazyInjected = false;

    if (this.injected) {
      let path = [...this.path, this.name];
      throw new Error(
        `Attempting to re-inject already injected entry: ${path.join(".")}`
      );
    }

    if (!this.shouldInject) {
      return;
    }

    if (this.isRevokable) {
      this.context.pendingEntries.add(this);
    }

    if (!this.hasPermission) {
      return;
    }

    this.injected = this.entry.getDescriptor(this.path, this.context);
    if (!this.injected) {
      return undefined;
    }

    return this.injected.descriptor;
  }

  /**
   * Injects a lazy property descriptor into the parent object which
   * checks permissions and eligibility for injection the first time it
   * is accessed.
   */
  lazyInject() {
    if (this.lazyInjected || this.injected) {
      let path = [...this.path, this.name];
      throw new Error(
        `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
      );
    }

    this.lazyInjected = true;
    exportLazyProperty(this.parentObj, this.name, () => {
      if (this.lazyInjected) {
        return this.getDescriptor();
      }
    });
  }

  /**
   * Injects or revokes this entry if its current state does not match
   * the context's current permissions.
   */
  permissionsChanged() {
    if (this.injected) {
      this.maybeRevoke();
    } else {
      this.maybeInject();
    }
  }

  maybeInject() {
    if (!this.injected && !this.lazyInjected) {
      this.lazyInject();
    }
  }

  maybeRevoke() {
    if (this.injected && !this.hasPermission) {
      this.revoke();
    }
  }
}

/**
 * Holds methods that run the actual implementation of the extension APIs. These
 * methods are only called if the extension API invocation matches the signature
 * as defined in the schema. Otherwise an error is reported to the context.
 */
class InjectionContext extends Context {
  constructor(params, schemaRoot) {
    super(params, CONTEXT_FOR_INJECTION);

    this.schemaRoot = schemaRoot;

    this.pendingEntries = new Set();
    this.children = new DefaultWeakMap(() => new Map());

    this.injectedRoots = new Set();

    if (params.setPermissionsChangedCallback) {
      params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
    }
  }

  /**
   * Check whether the API should be injected.
   *
   * @abstract
   * @param {string} _namespace The namespace of the API. This may contain dots,
   *     e.g. in the case of "devtools.inspectedWindow".
   * @param {string?} _name The name of the property in the namespace.
   *     `null` if we are checking whether the namespace should be injected.
   * @param {Array<string>} _allowedContexts A list of additional contexts in
   *      which this API should be available. May include any of:
   *         "main" - The main chrome browser process.
   *         "addon" - An addon process.
   *         "content" - A content process.
   * @returns {boolean} Whether the API should be injected.
   */
  shouldInject(_namespace, _name, _allowedContexts) {
    throw new Error("Not implemented");
  }

  /**
   * Generate the implementation for `namespace`.`name`.
   *
   * @abstract
   * @param {string} _namespace The full path to the namespace of the API, minus
   *     the name of the method or property. E.g. "storage.local".
   * @param {string} _name The name of the method, property or event.
   * @returns {import("ExtensionCommon.sys.mjs").SchemaAPIInterface}
   *          The implementation of the API.
   */
  getImplementation(_namespace, _name) {
    throw new Error("Not implemented");
  }

  /**
   * Updates all injection entries which may need to be updated after a
   * permission change, revoking or re-injecting them as necessary.
   */
  permissionsChanged() {
    for (let entry of this.pendingEntries) {
      try {
        entry.permissionsChanged();
      } catch (e) {
        Cu.reportError(e);
      }
    }
  }

  /**
   * Recursively revokes all child injection entries of the given
   * object.
   *
   * @param {object} object
   *        The object for which to invoke children.
   */
  revokeChildren(object) {
    if (!this.children.has(object)) {
      return;
    }

    let children = this.children.get(object);
    for (let [name, entry] of children.entries()) {
      try {
        entry.revoke();
      } catch (e) {
        Cu.reportError(e);
      }
      children.delete(name);

      // When we revoke children for an object, we consider that object
      // dead. If the entry is ever reified again, a new object is
      // created, with new child entries.
      this.pendingEntries.delete(entry);
    }
    this.children.delete(object);
  }

  _getInjectionEntry(entry, dest, name, path, parentEntry) {
    let injection = new InjectionEntry(
      this,
      entry,
      dest,
      name,
      path,
      parentEntry
    );

    this.children.get(dest).set(name, injection);

    return injection;
  }

  /**
   * Returns the property descriptor for the given entry.
   *
   * @param {Entry|Namespace} entry
   *        The entry instance to return a descriptor for.
   * @param {object} dest
   *        The object into which this entry is being injected.
   * @param {string} name
   *        The property name on the destination object where the entry
   *        will be injected.
   * @param {Array<string>} path
   *        The full path from the root injection object to this entry.
   * @param {Partial<Entry>} parentEntry
   *        The parent entry for this entry.
   *
   * @returns {object?}
   *        A property descriptor object, or null if the entry should
   *        not be injected.
   */
  getDescriptor(entry, dest, name, path, parentEntry) {
    let injection = this._getInjectionEntry(
      entry,
      dest,
      name,
      path,
      parentEntry
    );

    return injection.getDescriptor();
  }

  /**
   * Lazily injects the given entry into the given object.
   *
   * @param {Entry} entry
   *        The entry instance to lazily inject.
   * @param {object} dest
   *        The object into which to inject this entry.
   * @param {string} name
   *        The property name at which to inject the entry.
   * @param {Array<string>} path
   *        The full path from the root injection object to this entry.
   * @param {Entry} parentEntry
   *        The parent entry for this entry.
   */
  injectInto(entry, dest, name, path, parentEntry) {
    let injection = this._getInjectionEntry(
      entry,
      dest,
      name,
      path,
      parentEntry
    );

    injection.lazyInject();
  }
}

/**
 * The methods in this singleton represent the "format" specifier for
 * JSON Schema string types.
 *
 * Each method either returns a normalized version of the original
 * value, or throws an error if the value is not valid for the given
 * format.
 */
const FORMATS = {
  hostname(string) {
    // TODO bug 1797376: Despite the name, this format is NOT a "hostname",
    // but hostname + port and may fail with IPv6. Use canonicalDomain instead.
    let valid = true;

    try {
      valid = new URL(`http://${string}`).host === string;
    } catch (e) {
      valid = false;
    }

    if (!valid) {
      throw new Error(`Invalid hostname ${string}`);
    }

    return string;
  },

  canonicalDomain(string) {
    let valid;

    try {
      valid = new URL(`http://${string}`).hostname === string;
    } catch (e) {
      valid = false;
    }

    if (!valid) {
      // Require the input to be a canonical domain.
      // Rejects obvious non-domains such as URLs,
      // but also catches non-IDN (punycode) domains.
      throw new Error(`Invalid domain ${string}`);
    }

    return string;
  },

  url(string, context) {
    let url = new URL(string).href;

    if (!context.checkLoadURL(url)) {
      throw new Error(`Access denied for URL ${url}`);
    }
    return url;
  },

  origin(string, context) {
    let url;
    try {
      url = new URL(string);
    } catch (e) {
      throw new Error(`Invalid origin: ${string}`);
    }
    if (!/^https?:/.test(url.protocol)) {
      throw new Error(`Invalid origin must be http or https for URL ${string}`);
    }
    // url.origin is punycode so a direct check against string wont work.
    // url.href appends a slash even if not in the original string, we we
    // additionally check that string does not end in slash.
    if (string.endsWith("/") || url.href != new URL(url.origin).href) {
      throw new Error(
        `Invalid origin for URL ${string}, replace with origin ${url.origin}`
      );
    }
    if (!context.checkLoadURL(url.origin)) {
      throw new Error(`Access denied for URL ${url}`);
    }
    return url.origin;
  },

  relativeUrl(string, context) {
    if (!context.url) {
      // If there's no context URL, return relative URLs unresolved, and
      // skip security checks for them.
      try {
        new URL(string);
      } catch (e) {
        return string;
      }
    }

    let url = new URL(string, context.url).href;

    if (!context.checkLoadURL(url)) {
      throw new Error(`Access denied for URL ${url}`);
    }
    return url;
  },

  strictRelativeUrl(string, context) {
    void FORMATS.unresolvedRelativeUrl(string);
    return FORMATS.relativeUrl(string, context);
  },

  unresolvedRelativeUrl(string) {
    if (!string.startsWith("//")) {
      try {
        new URL(string);
      } catch (e) {
        return string;
      }
    }

    throw new SyntaxError(
      `String ${JSON.stringify(string)} must be a relative URL`
    );
  },

  homepageUrl(string, context) {
    // Pipes are used for separating homepages, but we only allow extensions to
    // set a single homepage. Encoding any pipes makes it one URL.
    return FORMATS.relativeUrl(
      string.replace(new RegExp("\\|", "g"), "%7C"),
      context
    );
  },

  imageDataOrStrictRelativeUrl(string, context) {
    // Do not accept a string which resolves as an absolute URL, or any
    // protocol-relative URL, except PNG or JPG data URLs
    if (
      !string.startsWith("data:image/png;base64,") &&
      !string.startsWith("data:image/jpeg;base64,")
    ) {
      try {
        return FORMATS.strictRelativeUrl(string, context);
      } catch (e) {
        throw new SyntaxError(
          `String ${JSON.stringify(
            string
          )} must be a relative or PNG or JPG data:image URL`
        );
      }
    }
    return string;
  },

  contentSecurityPolicy(string, context) {
    // Manifest V3 extension_pages allows WASM.  When sandbox is
    // implemented, or any other V3 or later directive, the flags
    // logic will need to be updated.
    let flags =
      context.manifestVersion < 3
        ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
        : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM;
    let error = lazy.contentPolicyService.validateAddonCSP(string, flags);
    if (error != null) {
      // The CSP validation error is not reported as part of the "choices" error message,
      // we log the CSP validation error explicitly here to make it easier for the addon developers
      // to see and fix the extension CSP.
      context.logError(`Error processing ${context.currentTarget}: ${error}`);
      return null;
    }
    return string;
  },

  date(string) {
    // A valid ISO 8601 timestamp.
    const PATTERN =
      /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
    if (!PATTERN.test(string)) {
      throw new Error(`Invalid date string ${string}`);
    }
    // Our pattern just checks the format, we could still have invalid
    // values (e.g., month=99 or month=02 and day=31).  Let the Date
    // constructor do the dirty work of validating.
    if (isNaN(Date.parse(string))) {
      throw new Error(`Invalid date string ${string}`);
    }
    return string;
  },

  manifestShortcutKey(string, { extensionManifest = true } = {}) {
    const result = lazy.ShortcutUtils.validate(string, { extensionManifest });
    if (result == lazy.ShortcutUtils.IS_VALID) {
      return string;
    }

    const SEE_DETAILS =
      `For details see: ` +
      `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
    let errorMessage;

    switch (result) {
      case lazy.ShortcutUtils.INVALID_KEY_IN_EXTENSION_MANIFEST:
        errorMessage =
          `Value "${string}" must not include extended F13-F19 keys. ` +
          `F13-F19 keys can only be used for user-defined keyboard shortcuts in about:addons ` +
          `"Manage Extension Shortcuts". ${SEE_DETAILS}`;
        break;
      default:
        errorMessage =
          `Value "${string}" must consist of ` +
          `either a combination of one or two modifiers, including ` +
          `a mandatory primary modifier and a key, separated by '+', ` +
          `or a media key. ${SEE_DETAILS}`;
    }
    throw new Error(errorMessage);
  },

  manifestShortcutKeyOrEmpty(string) {
    // manifestShortcutKey is the formatter applied to the manifest keys assigned
    // through the manifest, while manifestShortcutKeyOrEmpty is the formatter
    // used by the commands.update API method JSONSchema.
    return string === ""
      ? ""
      : FORMATS.manifestShortcutKey(string, { extensionManifest: false });
  },

  versionString(string, context) {
    const parts = string.split(".");

    if (
      // We accept up to 4 numbers.
      parts.length > 4 ||
      // Non-zero values cannot start with 0 and we allow numbers up to 9 digits.
      parts.some(part => !/^(0|[1-9][0-9]{0,8})$/.test(part))
    ) {
      context.logWarning(
        `version must be a version string consisting of at most 4 integers ` +
          `of at most 9 digits without leading zeros, and separated with dots`
      );
    }

    // The idea is to only emit a warning when the version string does not
    // match the simple format we want to encourage developers to use. Given
    // the version is required, we always accept the value as is.
    return string;
  },
};

// Schema files contain namespaces, and each namespace contains types,
// properties, functions, and events. An Entry is a base class for
// types, properties, functions, and events.
class Entry {
  /** @type {Entry} */
  fallbackEntry;

  constructor(schema = {}) {
    /**
     * If set to any value which evaluates as true, this entry is
     * deprecated, and any access to it will result in a deprecation
     * warning being logged to the browser console.
     *
     * If the value is a string, it will be appended to the deprecation
     * message. If it contains the substring "${value}", it will be
     * replaced with a string representation of the value being
     * processed.
     *
     * If the value is any other truthy value, a generic deprecation
     * message will be emitted.
     */
    this.deprecated = false;
    if ("deprecated" in schema) {
      this.deprecated = schema.deprecated;
    }

    /**
     * @property {string} [preprocessor]
     * If set to a string value, and a preprocessor of the same is
     * defined in the validation context, it will be applied to this
     * value prior to any normalization.
     */
    this.preprocessor = schema.preprocess || null;

    /**
     * @property {string} [postprocessor]
     * If set to a string value, and a postprocessor of the same is
     * defined in the validation context, it will be applied to this
     * value after any normalization.
     */
    this.postprocessor = schema.postprocess || null;

    /**
     * @property {Array<string>} allowedContexts A list of allowed contexts
     * to consider before generating the API.
     * These are not parsed by the schema, but passed to `shouldInject`.
     */
    this.allowedContexts = schema.allowedContexts || [];

    this.min_manifest_version =
      schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
    this.max_manifest_version =
      schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
  }

  /**
   * Preprocess the given value with the preprocessor declared in
   * `preprocessor`.
   *
   * @param {*} value
   * @param {Context} context
   * @returns {*}
   */
  preprocess(value, context) {
    if (this.preprocessor) {
      return context.preprocessors[this.preprocessor](value, context);
    }
    return value;
  }

  /**
   * Postprocess the given result with the postprocessor declared in
   * `postprocessor`.
   *
   * @param {object} result
   * @param {Context} context
   * @returns {object}
   */
  postprocess(result, context) {
    if (result.error || !this.postprocessor) {
      return result;
    }

    let value = context.postprocessors[this.postprocessor](
      result.value,
      context
    );
    return { value };
  }

  /**
   * Logs a deprecation warning for this entry, based on the value of
   * its `deprecated` property.
   *
   * @param {Context} context
   * @param {any} [value]
   */
  logDeprecation(context, value = null) {
    let message = "This property is deprecated";
    if (typeof this.deprecated == "string") {
      message = this.deprecated;
      if (message.includes("${value}")) {
        try {
          value = JSON.stringify(value);
        } catch (e) {
          value = String(value);
        }
        message = message.replace(/\$\{value\}/g, () => value);
      }
    }

    context.logWarning(message);
  }

  /**
   * Checks whether the entry is deprecated and, if so, logs a
   * deprecation message.
   *
   * @param {Context} context
   * @param {any} [value]
   */
  checkDeprecated(context, value = null) {
    if (this.deprecated) {
      this.logDeprecation(context, value);
    }
  }

  /**
   * Returns an object containing property descriptor for use when
   * injecting this entry into an API object.
   *
   * @param {Array<string>} _path The API path, e.g. `["storage", "local"]`.
   * @param {InjectionContext} _context
   *
   * @returns {object?}
   *        An object containing a `descriptor` property, specifying the
   *        entry's property descriptor, and an optional `revoke`
   *        method, to be called when the entry is being revoked.
   */
  getDescriptor(_path, _context) {
    return undefined;
  }
}

// Corresponds either to a type declared in the "types" section of the
// schema or else to any type object used throughout the schema.
class Type extends Entry {
  /**
   * @property {Array<string>} EXTRA_PROPERTIES
   *        An array of extra properties which may be present for
   *        schemas of this type.
   */
  static get EXTRA_PROPERTIES() {
    return [
      "description",
      "deprecated",
      "preprocess",
      "postprocess",
      "privileged",
      "allowedContexts",
      "min_manifest_version",
      "max_manifest_version",
    ];
  }

  /**
   * Parses the given schema object and returns an instance of this
   * class which corresponds to its properties.
   *
   * @param {SchemaRoot} root
   *        The root schema for this type.
   * @param {object} schema
   *        A JSON schema object which corresponds to a definition of
   *        this type.
   * @param {Array<string>} path
   *        The path to this schema object from the root schema,
   *        corresponding to the property names and array indices
   *        traversed during parsing in order to arrive at this schema
   *        object.
   * @param {Array<string>} [extraProperties]
   *        An array of extra property names which are valid for this
   *        schema in the current context.
   * @returns {Type}
   *        An instance of this type which corresponds to the given
   *        schema object.
   * @static
   */
  static parseSchema(root, schema, path, extraProperties = []) {
    this.checkSchemaProperties(schema, path, extraProperties);

    return new this(schema);
  }

  /**
   * Checks that all of the properties present in the given schema
   * object are valid properties for this type, and throws if invalid.
   *
   * @param {object} schema
   *        A JSON schema object.
   * @param {Array<string>} path
   *        The path to this schema object from the root schema,
   *        corresponding to the property names and array indices
   *        traversed during parsing in order to arrive at this schema
   *        object.
   * @param {Iterable<string>} [extra]
   *        An array of extra property names which are valid for this
   *        schema in the current context.
   * @throws {Error}
   *        An error describing the first invalid property found in the
   *        schema object.
   */
  static checkSchemaProperties(schema, path, extra = []) {
    if (DEBUG) {
      let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);

      for (let prop of Object.keys(schema)) {
        if (!allowedSet.has(prop)) {
          throw new Error(
            `Internal error: Namespace ${path.join(".")} has ` +
              `invalid type property "${prop}" ` +
              `in type "${schema.id || JSON.stringify(schema)}"`
          );
        }
      }
    }
  }

  /**
   * Takes a value, checks that it has the correct type, and returns a
   * "normalized" version of the value. The normalized version will
   * include "nulls" in place of omitted optional properties. The
   * result of this function is either {error: "Some type error"} or
   * {value: <normalized-value>}.
   */
  normalize(value, context) {
    return context.error("invalid type");
  }

  /**
   * Unlike normalize, this function does a shallow check to see if
   * |baseType| (one of the possible getValueBaseType results) is
   * valid for this type. It returns true or false. It's used to fill
   * in optional arguments to functions before actually type checking
   *
   * @param {string} _baseType
   */
  checkBaseType(_baseType) {
    return false;
  }

  /**
   * Helper method that simply relies on checkBaseType to implement
   * normalize. Subclasses can choose to use it or not.
   */
  normalizeBase(type, value, context) {
    if (this.checkBaseType(getValueBaseType(value))) {
      this.checkDeprecated(context, value);
      return { value: this.preprocess(value, context) };
    }

    let choice;
    if ("aeiou".includes(type[0])) {
      choice = `be an ${type} value`;
    } else {
      choice = `be a ${type} value`;
    }

    return context.error(
      () => `Expected ${type} instead of ${JSON.stringify(value)}`,
      choice
    );
  }
}

// Type that allows any value.
class AnyType extends Type {
  normalize(value, context) {
    this.checkDeprecated(context, value);
    return this.postprocess({ value }, context);
  }

  checkBaseType() {
    return true;
  }
}

// An untagged union type.
class ChoiceType extends Type {
  static get EXTRA_PROPERTIES() {
    return ["choices", ...super.EXTRA_PROPERTIES];
  }

  /** @type {(root, schema, path, extraProperties?: Iterable) => ChoiceType} */
  static parseSchema(root, schema, path, extraProperties = []) {
    this.checkSchemaProperties(schema, path, extraProperties);

    let choices = schema.choices.map(t => root.parseSchema(t, path));
    return new this(schema, choices);
  }

  constructor(schema, choices) {
    super(schema);
    this.choices = choices;
  }

  extend(type) {
    this.choices.push(...type.choices);

    return this;
  }

  normalize(value, context) {
    this.checkDeprecated(context, value);

    let error;
    let { choices, result } = context.withChoices(() => {
      for (let choice of this.choices) {
        // Ignore a possible choice if it is not supported by
        // the manifest version we are normalizing.
        if (!context.matchManifestVersion(choice)) {
          continue;
        }

        let r = choice.normalize(value, context);
        if (!r.error) {
          return r;
        }

        error = r;
      }
    });

    if (result) {
      return result;
    }
    if (choices.size <= 1) {
      return error;
    }

    choices = Array.from(choices, forceString);
    let n = choices.length - 1;
    choices[n] = `or ${choices[n]}`;

    let message;
    if (typeof value === "object") {
      message = () => `Value must either: ${choices.join(", ")}`;
    } else {
      message = () =>
        `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
    }

    return context.error(message, null);
  }

  checkBaseType(baseType) {
    return this.choices.some(t => t.checkBaseType(baseType));
  }

  getDescriptor(path, context) {
    // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
    // it is an enumeration.  Since we need versioned choices in some cases, here we
    // build a list of valid enumerations that will work for a given manifest version.
    if (
      !this.choices.length ||
      !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
    ) {
      return;
    }

    let obj = Cu.createObjectIn(context.cloneScope);
    let descriptor = { value: obj };
    for (let choice of this.choices) {
      // Ignore a possible choice if it is not supported by
      // the manifest version we are normalizing.
      if (!context.matchManifestVersion(choice)) {
        continue;
      }
      let d = choice.getDescriptor(path, context);
      if (d) {
        Object.assign(obj, d.descriptor.value);
      }
    }

    return { descriptor };
  }
}

// This is a reference to another type--essentially a typedef.
class RefType extends Type {
  static get EXTRA_PROPERTIES() {
    return ["$ref", ...super.EXTRA_PROPERTIES];
  }

  /** @type {(root, schema, path, extraProperties?: Iterable) => RefType} */
  static parseSchema(root, schema, path, extraProperties = []) {
    this.checkSchemaProperties(schema, path, extraProperties);

    let ref = schema.$ref;
    let ns = path.join(".");
    if (ref.includes(".")) {
      [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
    }
    return new this(root, schema, ns, ref);
  }

  // For a reference to a type named T declared in namespace NS,
  // namespaceName will be NS and reference will be T.
  constructor(root, schema, namespaceName, reference) {
    super(schema);
    this.root = root;
    this.namespaceName = namespaceName;
    this.reference = reference;
  }

  get targetType() {
    let ns = this.root.getNamespace(this.namespaceName);
    let type = ns.get(this.reference);
    if (!type) {
      throw new Error(`Internal error: Type ${this.reference} not found`);
    }
    return type;
  }

  normalize(value, context) {
    this.checkDeprecated(context, value);
    return this.targetType.normalize(value, context);
  }

  checkBaseType(baseType) {
    return this.targetType.checkBaseType(baseType);
  }
}

class StringType extends Type {
  static get EXTRA_PROPERTIES() {
    return [
      "enum",
      "minLength",
      "maxLength",
      "pattern",
      "format",
      ...super.EXTRA_PROPERTIES,
    ];
  }

  static parseSchema(root, schema, path, extraProperties = []) {
    this.checkSchemaProperties(schema, path, extraProperties);

    let enumeration = schema.enum || null;
    if (enumeration) {
      // The "enum" property is either a list of strings that are
      // valid values or else a list of {name, description} objects,
      // where the .name values are the valid values.
      enumeration = enumeration.map(e => {
        if (typeof e == "object") {
          return e.name;
        }
        return e;
      });
    }

    let pattern = null;
    if (schema.pattern) {
      try {
        pattern = parsePattern(schema.pattern);
      } catch (e) {
        throw new Error(
          `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
        );
      }
    }

    let format = null;
    if (schema.format) {
      if (!(schema.format in FORMATS)) {
        throw new Error(
          `Internal error: Invalid string format ${schema.format}`
        );
      }
      format = FORMATS[schema.format];
    }
    return new this(
      schema,
      schema.id || undefined,
      enumeration,
      schema.minLength || 0,
      schema.maxLength || Infinity,
      pattern,
      format
    );
  }

  constructor(
    schema,
    name,
    enumeration,
    minLength,
    maxLength,
    pattern,
    format
  ) {
    super(schema);
    this.name = name;
    this.enumeration = enumeration;
    this.minLength = minLength;
    this.maxLength = maxLength;
    this.pattern = pattern;
    this.format = format;
  }

  normalize(value, context) {
    let r = this.normalizeBase("string", value, context);
    if (r.error) {
      return r;
    }
    value = r.value;

    if (this.enumeration) {
      if (this.enumeration.includes(value)) {
        return this.postprocess({ value }, context);
      }

      let choices = this.enumeration.map(JSON.stringify).join(", ");

      return context.error(
        () => `Invalid enumeration value ${JSON.stringify(value)}`,
        `be one of [${choices}]`
      );
    }

    if (value.length < this.minLength) {
      return context.error(
        () =>
          `String ${JSON.stringify(value)} is too short (must be ${
            this.minLength
          })`,
        `be longer than ${this.minLength}`
      );
    }
    if (value.length > this.maxLength) {
      return context.error(
        () =>
          `String ${JSON.stringify(value)} is too long (must be ${
            this.maxLength
          })`,
        `be shorter than ${this.maxLength}`
      );
    }

    if (this.pattern && !this.pattern.test(value)) {
      return context.error(
        () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
        `match the pattern ${this.pattern.toSource()}`
      );
    }

    if (this.format) {
      try {
        r.value = this.format(r.value, context);
      } catch (e) {
        return context.error(
          String(e),
          `match the format "${this.format.name}"`
        );
      }
    }

    return r;
  }

  checkBaseType(baseType) {
    return baseType == "string";
  }

  getDescriptor(path, context) {
    if (this.enumeration) {
      let obj = Cu.createObjectIn(context.cloneScope);

      for (let e of this.enumeration) {
        obj[e.toUpperCase()] = e;
      }

      return {
        descriptor: { value: obj },
      };
    }
  }
}

class NullType extends Type {
  normalize(value, context) {
    return this.normalizeBase("null", value, context);
  }

  checkBaseType(baseType) {
    return baseType == "null";
  }
}

let FunctionEntry;
let Event;
let SubModuleType;

class ObjectType extends Type {
  static get EXTRA_PROPERTIES() {
    return [
      "properties",
      "patternProperties",
      "$import",
      ...super.EXTRA_PROPERTIES,
    ];
  }

  static parseSchema(root, schema, path, extraProperties = []) {
    if ("functions" in schema) {
      return SubModuleType.parseSchema(root, schema, path, extraProperties);
    }

    if (DEBUG && !("$extend" in schema)) {
      // Only allow extending "properties" and "patternProperties".
      extraProperties = [
        "additionalProperties",
        "isInstanceOf",
        ...extraProperties,
      ];
    }
    this.checkSchemaProperties(schema, path, extraProperties);

    let imported = null;
    if ("$import" in schema) {
      let importPath = schema.$import;
      let idx = importPath.indexOf(".");
      if (idx === -1) {
        imported = [path[0], importPath];
      } else {
        imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
      }
    }

    let parseProperty = (schema, extraProps = []) => {
      return {
        type: root.parseSchema(
          schema,
          path,
          DEBUG && [
            "unsupported",
            "onError",
            "permissions",
            "default",
            ...extraProps,
          ]
        ),
        optional: schema.optional || false,
        unsupported: schema.unsupported || false,
        onError: schema.onError || null,
        default: schema.default === undefined ? null : schema.default,
      };
    };

    // Parse explicit "properties" object.
    let properties = Object.create(null);
    for (let propName of Object.keys(schema.properties || {})) {
      properties[propName] = parseProperty(schema.properties[propName], [
        "optional",
      ]);
    }

    // Parse regexp properties from "patternProperties" object.
    let patternProperties = [];
    for (let propName of Object.keys(schema.patternProperties || {})) {
      let pattern;
      try {
        pattern = parsePattern(propName);
      } catch (e) {
        throw new Error(
          `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
        );
      }

      patternProperties.push({
        pattern,
        type: parseProperty(schema.patternProperties[propName]),
      });
    }

    // Parse "additionalProperties" schema.
    let additionalProperties = null;
    if (schema.additionalProperties) {
      let type = schema.additionalProperties;
      if (type === true) {
        type = { type: "any" };
      }

      additionalProperties = root.parseSchema(type, path);
    }

    return new this(
      schema,
      properties,
      additionalProperties,
      patternProperties,
      schema.isInstanceOf || null,
      imported
    );
  }

  constructor(
    schema,
    properties,
    additionalProperties,
    patternProperties,
    isInstanceOf,
    imported
  ) {
    super(schema);
    this.properties = properties;
    this.additionalProperties = additionalProperties;
    this.patternProperties = patternProperties;
    this.isInstanceOf = isInstanceOf;

    if (imported) {
      let [ns, path] = imported;
      ns = Schemas.getNamespace(ns);
      let importedType = ns.get(path);
      if (!importedType) {
        throw new Error(`Internal error: imported type ${path} not found`);
      }

      if (DEBUG && !(importedType instanceof ObjectType)) {
        throw new Error(
          `Internal error: cannot import non-object type ${path}`
        );
      }

      this.properties = Object.assign(
        {},
        importedType.properties,
        this.properties
      );
      this.patternProperties = [
        ...importedType.patternProperties,
        ...this.patternProperties,
      ];
      this.additionalProperties =
        importedType.additionalProperties || this.additionalProperties;
    }
  }

  extend(type) {
    for (let key of Object.keys(type.properties)) {
      if (key in this.properties) {
        throw new Error(
          `InternalError: Attempt to extend an object with conflicting property "${key}"`
        );
      }
      this.properties[key] = type.properties[key];
    }

    this.patternProperties.push(...type.patternProperties);

    return this;
  }

  checkBaseType(baseType) {
    return baseType == "object";
  }

  /**
   * Extracts the enumerable properties of the given object, including
   * function properties which would normally be omitted by X-ray
   * wrappers.
   *
   * @param {object} value
   * @param {Context} context
   *        The current parse context.
   * @returns {object}
   *        An object with an `error` or `value` property.
   */
  extractProperties(value, context) {
    // |value| should be a JS Xray wrapping an object in the
    // extension compartment. This works well except when we need to
    // access callable properties on |value| since JS Xrays don't
    // support those. To work around the problem, we verify that
    // |value| is a plain JS object (i.e., not anything scary like a
    // Proxy). Then we copy the properties out of it into a normal
    // object using a waiver wrapper.

    let klass = ChromeUtils.getClassName(value, true);
    if (klass != "Object") {
      throw context.error(
        `Expected a plain JavaScript object, got a ${klass}`,
        `be a plain JavaScript object`
      );
    }

    return ChromeUtils.shallowClone(value);
  }

  checkProperty(context, prop, propType, result, properties, remainingProps) {
    let { type, optional, unsupported, onError } = propType;
    let error = null;

    if (!context.matchManifestVersion(type)) {
      if (prop in properties) {
        error = context.error(
          `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
          `not contain an unsupported "${prop}" property`
        );

        context.logWarning(forceString(error.error));
        if (this.additionalProperties) {
          // When `additionalProperties` is set to UnrecognizedProperty, the
          // caller (i.e. ObjectType's normalize method) assigns the original
          // value to `result[prop]`. Erase the property now to prevent
          // `result[prop]` from becoming anything other than `undefined.
          //
          // A warning was already logged above, so we do not need to also log
          // "An unexpected property was found in the WebExtension manifest."
          remainingProps.delete(prop);
        }
        // When `additionalProperties` is not set, ObjectType's normalize method
        // will return an error because prop is still in remainingProps.
        return;
      }
    } else if (unsupported) {
      if (prop in properties) {
        error = context.error(
          `Property "${prop}" is unsupported by Firefox`,
          `not contain an unsupported "${prop}" property`
        );
      }
    } else if (prop in properties) {
      if (
        optional &&
        (properties[prop] === null || properties[prop] === undefined)
      ) {
        result[prop] = propType.default;
      } else {
        let r = context.withPath(prop, () =>
          type.normalize(properties[prop], context)
        );
        if (r.error) {
          error = r;
        } else {
          result[prop] = r.value;
          properties[prop] = r.value;
        }
      }
      remainingProps.delete(prop);
    } else if (!optional) {
      error = context.error(
        `Property "${prop}" is required`,
        `contain the required "${prop}" property`
      );
    } else if (optional !== "omit-key-if-missing") {
      result[prop] = propType.default;
    }

    if (error) {
      if (onError == "warn") {
        context.logWarning(forceString(error.error));
      } else if (onError != "ignore") {
        throw error;
      }

      result[prop] = propType.default;
    }
  }

  normalize(value, context) {
    try {
      let v = this.normalizeBase("object", value, context);
      if (v.error) {
        return v;
      }
      value = v.value;

      if (this.isInstanceOf) {
        if (DEBUG) {
          if (
            Object.keys(this.properties).length ||
            this.patternProperties.length ||
            !(this.additionalProperties instanceof AnyType)
          ) {
            throw new Error(
              "InternalError: isInstanceOf can only be used " +
                "with objects that are otherwise unrestricted"
            );
          }
        }

        if (
          ChromeUtils.getClassName(value) !== this.isInstanceOf &&
          (this.isInstanceOf !== "Element" || value.nodeType !== 1)
        ) {
          return context.error(
            `Object must be an instance of ${this.isInstanceOf}`,
            `be an instance of ${this.isInstanceOf}`
          );
        }

        // This is kind of a hack, but we can't normalize things that
        // aren't JSON, so we just return them.
        return this.postprocess({ value }, context);
      }

      let properties = this.extractProperties(value, context);
      let remainingProps = new Set(Object.keys(properties));

      let result = {};
      for (let prop of Object.keys(this.properties)) {
        this.checkProperty(
          context,
          prop,
          this.properties[prop],
          result,
          properties,
          remainingProps
        );
      }

      for (let prop of Object.keys(properties)) {
        for (let { pattern, type } of this.patternProperties) {
          if (pattern.test(prop)) {
            this.checkProperty(
              context,
              prop,
              type,
              result,
              properties,
              remainingProps
            );
          }
        }
      }

      if (this.additionalProperties) {
        for (let prop of remainingProps) {
          let r = context.withPath(prop, () =>
            this.additionalProperties.normalize(properties[prop], context)
          );
          if (r.error) {
            return r;
          }
          result[prop] = r.value;
        }
      } else if (remainingProps.size && !context.ignoreUnrecognizedProperties) {
        if (remainingProps.size == 1) {
          return context.error(
            `Unexpected property "${[...remainingProps]}"`,
--> --------------------

--> maximum size reached

--> --------------------

[ Dauer der Verarbeitung: 0.16 Sekunden  (vorverarbeitet)  ]