/* 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/. */

// This module provides a mapper between internal innerWindowId and documentId,
// which is publicly exposed in extension APIs.
//
// The mapper relies on a secret key, initialized on first use and fixed for
// the lifetime of the application, exposed to all processes via sharedData
// with "extensions/documentIdKey". The main requirement is that its
// initialization happens before its first use in child processes, such as
// synchronous runtime.getDocumentId() calls in the child.
//
// In practice, the key is initialized from Extension.sys.mjs, by assigning to
// "extensions/documentIdKey" (if needed), before sending the initialization
// data for the first extension to the child (with sharedData.flush()).

let gMapper = null;

// Returns a mapper between innerWindowId numbers and documentId UUID.
function ensureMapper() {
  if (gMapper) {
    return gMapper;
  }
  // The random key that is fixed for the duration of the application session.
  // This supports a stable mapping that cannot be predicted by extensions.
  let key;
  if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
    // Look up the key generated by Extension.sys.mjs on the first run.
    key = Services.ppmm.sharedData.get("extensions/documentIdKey");
    if (!key) {
      // key not found? Something invoked ExtensionDocumentId.sys.mjs before
      // the first extension started. Generate the key now.
      key = Cc["@mozilla.org/keyed-uuid-mapper;1"]
        .createInstance(Ci.nsIKeyedUUIDMapper)
        .generateKey();
      Services.ppmm.sharedData.set("extensions/documentIdKey", key);
      // Note: sharedData.flush() is not called because there is no imminent
      // need for the data in the child. Extension.sys.mjs flushes as needed
      // when it starts the first extension.
    }
  } else {
    key = Services.cpmm.sharedData.get("extensions/documentIdKey");
    if (!key) {
      // This is unexpected - the parent should have assigned the common random
      // key before the first extension has started, in particular before any
      // extension context can call runtime.getDocumentId().
      throw new Error("Failed to gather key to compute documentId");
    }
  }
  gMapper = Cc["@mozilla.org/keyed-uuid-mapper;1"].createInstance(
    Ci.nsIKeyedUUIDMapper
  );
  gMapper.init(key);
  return gMapper;
}

/** @typedef {number} integer */

export const ExtensionDocumentId = {
  /**
   * @param {integer} [innerWindowId]
   * @returns {string|undefined} documentId (UUID) string, or undefined if the
   *        input is omitted or zero (a valid innerWindowId is at least 1).
   */
  getDocumentId(innerWindowId) {
    if (!innerWindowId) {
      // Note: 0 is also an invalid innerWindowId because it is never generated
      // by nsContentUtils::GenerateWindowId().
      return undefined;
    }
    return ensureMapper().toUUID(innerWindowId);
  },

  /**
   * @param {string} documentId - Supposedly the return value of getDocumentId.
   * @returns {integer|undefined} The positive non-zero innerWindowId, if any.
   *        Returns undefined if the input was not from getDocumentId().
   */
  getInnerWindowIdForDocumentId(documentId) {
    try {
      // There are 2^128 possible UUIDs. But getDocumentId() took innerWindowId
      // as input, and nsContentUtils::GenerateWindowId() returns numbers that
      // are at least 1 and at most 2^53, which is far fewer than the range of
      // potential UUIDs. In other words, extensions cannot guess a valid UUID.
      // nsIKeyedUUIDMapper::FromUUID returning implies that the UUID is valid.
      return ensureMapper().fromUUID(documentId);
    } catch {
      return undefined;
    }
  },

  /**
   * Returns BrowsingContext for a documentId, if still the current document.
   *
   * @param {string} documentId
   * @returns {BrowsingContext|null}
   */
  getBrowsingContextForDocumentId(documentId) {
    const innerWindowId = this.getInnerWindowIdForDocumentId(documentId);
    if (!innerWindowId) {
      return null;
    }
    const wgp = WindowGlobalParent.getByInnerWindowId(innerWindowId);
    if (!wgp) {
      return null;
    }
    if (!wgp.isCurrentGlobal) {
      return null;
    }
    return wgp.browsingContext;
  },
};
