Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


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.49 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge