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

Quelle  ext-menus.js   Sprache: JAVA

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


"use strict";

ChromeUtils.defineESModuleGetters(this, {
  ExtensionMenus: "resource://gre/modules/ExtensionMenus.sys.mjs",
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

var { DefaultMap, ExtensionError, parseMatchPatterns } = ExtensionUtils;

var { ExtensionParent } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionParent.sys.mjs"
);

var { IconDetails } = ExtensionParent;

const ACTION_MENU_TOP_LEVEL_LIMIT = 6;

// Map[Extension -> Map[ID -> MenuItem]]
// Note: we want to enumerate all the menu items so
// this cannot be a weak map.
var gMenuMap = new Map();

// Map[Extension -> MenuItem]
var gRootItems = new Map();

// Map[Extension -> ID[]]
// Menu IDs that were eligible for being shown in the current menu.
var gShownMenuItems = new DefaultMap(() => []);

// Map[Extension -> Set[Contexts]]
// A DefaultMap (keyed by extension) which keeps track of the
// contexts with a subscribed onShown event listener.
var gOnShownSubscribers = new DefaultMap(() => new Set());

// If id is not specified for an item we use an integer.
var gNextMenuItemID = 0;

// Used to assign unique names to radio groups.
var gNextRadioGroupID = 0;

// The max length of a menu item's label.
var gMaxLabelLength = 64;

var gMenuBuilder = {
  // When a new menu is opened, this function is called and
  // we populate the |xulMenu| with all the items from extensions
  // to be displayed. We always clear all the items again when
  // popuphidden fires.
  build(contextData) {
    contextData = this.maybeOverrideContextData(contextData);
    let xulMenu = contextData.menu;
    xulMenu.addEventListener("popuphidden"this);
    this.xulMenu = xulMenu;
    for (let [, root] of gRootItems) {
      this.createAndInsertTopLevelElements(root, contextData, null);
    }
    this.afterBuildingMenu(contextData);

    if (
      contextData.webExtContextData &&
      !contextData.webExtContextData.showDefaults
    ) {
      // Wait until nsContextMenu.js has toggled the visibility of the default
      // menu items before hiding the default items.
      Promise.resolve().then(() => this.hideDefaultMenuItems());
    }
  },

  maybeOverrideContextData(contextData) {
    let { webExtContextData } = contextData;
    if (!webExtContextData || !webExtContextData.overrideContext) {
      return contextData;
    }
    let contextDataBase = {
      menu: contextData.menu,
      // eslint-disable-next-line no-use-before-define
      originalViewType: getContextViewType(contextData),
      originalViewUrl: contextData.inFrame
        ? contextData.frameUrl
        : contextData.pageUrl,
      webExtContextData,
    };
    if (webExtContextData.overrideContext === "bookmark") {
      return {
        ...contextDataBase,
        bookmarkId: webExtContextData.bookmarkId,
        onBookmark: true,
      };
    }
    if (webExtContextData.overrideContext === "tab") {
      // TODO: Handle invalid tabs more gracefully (instead of throwing).
      let tab = tabTracker.getTab(webExtContextData.tabId);
      return {
        ...contextDataBase,
        tab,
        pageUrl: tab.linkedBrowser.currentURI.spec,
        onTab: true,
      };
    }
    throw new Error(
      `Unexpected overrideContext: ${webExtContextData.overrideContext}`
    );
  },

  canAccessContext(extension, contextData) {
    if (!extension.privateBrowsingAllowed) {
      let nativeTab = contextData.tab;
      if (
        nativeTab &&
        PrivateBrowsingUtils.isBrowserPrivate(nativeTab.linkedBrowser)
      ) {
        return false;
      } else if (
        PrivateBrowsingUtils.isWindowPrivate(contextData.menu.ownerGlobal)
      ) {
        return false;
      }
    }
    return true;
  },

  createAndInsertTopLevelElements(root, contextData, nextSibling) {
    let rootElements;
    if (!this.canAccessContext(root.extension, contextData)) {
      return;
    }
    if (
      contextData.onAction ||
      contextData.onBrowserAction ||
      contextData.onPageAction
    ) {
      if (contextData.extension.id !== root.extension.id) {
        return;
      }
      rootElements = this.buildTopLevelElements(
        root,
        contextData,
        ACTION_MENU_TOP_LEVEL_LIMIT,
        false
      );

      // Action menu items are prepended to the menu, followed by a separator.
      nextSibling = nextSibling || this.xulMenu.firstElementChild;
      if (rootElements.length && !this.itemsToCleanUp.has(nextSibling)) {
        rootElements.push(
          this.xulMenu.ownerDocument.createXULElement("menuseparator")
        );
      }
    } else if (contextData.webExtContextData) {
      let { extensionId, showDefaults, overrideContext } =
        contextData.webExtContextData;
      if (extensionId === root.extension.id) {
        rootElements = this.buildTopLevelElements(
          root,
          contextData,
          Infinity,
          false
        );
        if (!nextSibling) {
          // The extension menu should be rendered at the top. If we use
          // a navigation group (on non-macOS), the extension menu should
          // come after that to avoid styling issues.
          if (AppConstants.platform == "macosx") {
            nextSibling = this.xulMenu.firstElementChild;
          } else {
            nextSibling = this.xulMenu.querySelector(
              ":scope > #context-sep-navigation + *"
            );
          }
        }
        if (
          rootElements.length &&
          showDefaults &&
          !this.itemsToCleanUp.has(nextSibling)
        ) {
          rootElements.push(
            this.xulMenu.ownerDocument.createXULElement("menuseparator")
          );
        }
      } else if (!showDefaults && !overrideContext) {
        // When the default menu items should be hidden, menu items from other
        // extensions should be hidden too.
        return;
      }
      // Fall through to show default extension menu items.
    }
    if (!rootElements) {
      rootElements = this.buildTopLevelElements(root, contextData, 1, true);
      if (
        rootElements.length &&
        !this.itemsToCleanUp.has(this.xulMenu.lastElementChild)
      ) {
        // All extension menu items are appended at the end.
        // Prepend separator if this is the first extension menu item.
        rootElements.unshift(
          this.xulMenu.ownerDocument.createXULElement("menuseparator")
        );
      }
    }

    if (!rootElements.length) {
      return;
    }

    if (nextSibling) {
      nextSibling.before(...rootElements);
    } else {
      this.xulMenu.append(...rootElements);
    }
    for (let item of rootElements) {
      this.itemsToCleanUp.add(item);
    }
  },

  buildElementWithChildren(item, contextData) {
    const element = this.buildSingleElement(item, contextData);
    const children = this.buildChildren(item, contextData);
    if (children.length) {
      element.firstElementChild.append(...children);
    }
    return element;
  },

  buildChildren(item, contextData) {
    let groupName;
    let children = [];
    for (let child of item.children) {
      if (child.type == "radio" && !child.groupName) {
        if (!groupName) {
          groupName = `webext-radio-group-${gNextRadioGroupID++}`;
        }
        child.groupName = groupName;
      } else {
        groupName = null;
      }

      if (child.enabledForContext(contextData)) {
        children.push(this.buildElementWithChildren(child, contextData));
      }
    }
    return children;
  },

  buildTopLevelElements(root, contextData, maxCount, forceManifestIcons) {
    let children = this.buildChildren(root, contextData);

    // TODO: Fix bug 1492969 and remove this whole if block.
    if (
      children.length === 1 &&
      maxCount === 1 &&
      forceManifestIcons &&
      AppConstants.platform === "linux" &&
      children[0].getAttribute("type") === "checkbox"
    ) {
      // Keep single checkbox items in the submenu on Linux since
      // the extension icon overlaps the checkbox otherwise.
      maxCount = 0;
    }

    if (children.length > maxCount) {
      // Move excess items into submenu.
      let rootElement = this.buildSingleElement(root, contextData);
      rootElement.setAttribute("ext-type""top-level-menu");
      rootElement.firstElementChild.append(...children.splice(maxCount - 1));
      children.push(rootElement);
    }

    if (forceManifestIcons) {
      for (let rootElement of children) {
        // Display the extension icon on the root element.
        if (
          root.extension.manifest.icons &&
          rootElement.getAttribute("type") !== "checkbox"
        ) {
          this.setMenuItemIcon(
            rootElement,
            root.extension,
            contextData,
            root.extension.manifest.icons
          );
        } else {
          this.removeMenuItemIcon(rootElement);
        }
      }
    }
    return children;
  },

  buildSingleElement(item, contextData) {
    let doc = contextData.menu.ownerDocument;
    let element;
    if (item.children.length) {
      element = this.createMenuElement(doc, item);
    } else if (item.type == "separator") {
      element = doc.createXULElement("menuseparator");
    } else {
      element = doc.createXULElement("menuitem");
    }

    return this.customizeElement(element, item, contextData);
  },

  createMenuElement(doc) {
    let element = doc.createXULElement("menu");
    // Menu elements need to have a menupopup child for its menu items.
    let menupopup = doc.createXULElement("menupopup");
    element.appendChild(menupopup);
    return element;
  },

  customizeElement(element, item, contextData) {
    let label = item.title;
    if (label) {
      let accessKey;
      label = label.replace(/&([\S\s]|$)/g, (_, nextChar, i) => {
        if (nextChar === "&") {
          return "&";
        }
        if (accessKey === undefined) {
          if (nextChar === "%" && label.charAt(i + 2) === "s") {
            accessKey = "";
          } else {
            accessKey = nextChar;
          }
        }
        return nextChar;
      });
      element.setAttribute("accesskey", accessKey || "");

      if (contextData.isTextSelected && label.indexOf("%s") > -1) {
        let selection = contextData.selectionText.trim();
        // The rendering engine will truncate the title if it's longer than 64 characters.
        // But if it makes sense let's try truncate selection text only, to handle cases like
        // 'look up "%s" in MyDictionary' more elegantly.

        let codePointsToRemove = 0;

        let selectionArray = Array.from(selection);

        let completeLabelLength = label.length - 2 + selectionArray.length;
        if (completeLabelLength > gMaxLabelLength) {
          codePointsToRemove = completeLabelLength - gMaxLabelLength;
        }

        if (codePointsToRemove) {
          let ellipsis = "\u2026";
          try {
            ellipsis = Services.prefs.getComplexValue(
              "intl.ellipsis",
              Ci.nsIPrefLocalizedString
            ).data;
          } catch (e) {}
          codePointsToRemove += 1;
          selection =
            selectionArray.slice(0, -codePointsToRemove).join("") + ellipsis;
        }

        label = label.replace(/%s/g, selection);
      }

      element.setAttribute("label", label);
    }

    element.setAttribute("id", item.elementId);

    if ("icons" in item) {
      if (item.icons) {
        this.setMenuItemIcon(element, item.extension, contextData, item.icons);
      } else {
        this.removeMenuItemIcon(element);
      }
    }

    if (item.type == "checkbox") {
      element.setAttribute("type""checkbox");
      if (item.checked) {
        element.setAttribute("checked""true");
      }
    } else if (item.type == "radio") {
      element.setAttribute("type""radio");
      element.setAttribute("name", item.groupName);
      if (item.checked) {
        element.setAttribute("checked""true");
      }
    }

    if (!item.enabled) {
      element.setAttribute("disabled""true");
    }

    element.addEventListener(
      "command",
      event => {
        if (event.target !== event.currentTarget) {
          return;
        }
        const wasChecked = item.checked;
        if (item.type == "checkbox") {
          item.checked = !item.checked;
        } else if (item.type == "radio") {
          // Deselect all radio items in the current radio group.
          for (let child of item.parent.children) {
            if (child.type == "radio" && child.groupName == item.groupName) {
              child.checked = false;
            }
          }
          // Select the clicked radio item.
          item.checked = true;
        }

        let { webExtContextData } = contextData;
        if (
          contextData.tab &&
          // If the menu context was overridden by the extension, do not grant
          // activeTab since the extension also controls the tabId.
          (!webExtContextData ||
            webExtContextData.extensionId !== item.extension.id)
        ) {
          item.tabManager.addActiveTabPermission(contextData.tab);
        }

        let info = item.getClickInfo(contextData, wasChecked);
        info.modifiers = clickModifiersFromEvent(event);

        info.button = event.button;

        let _execute_action =
          item.extension.manifestVersion < 3
            ? "_execute_browser_action"
            : "_execute_action";

        // Allow menus to open various actions supported in webext prior
        // to notifying onclicked.
        let actionFor = {
          [_execute_action]: global.browserActionFor,
          _execute_page_action: global.pageActionFor,
          _execute_sidebar_action: global.sidebarActionFor,
        }[item.command];
        if (actionFor) {
          let win = event.target.ownerGlobal;
          actionFor(item.extension).triggerAction(win);
          return;
        }

        item.extension.emit(
          "webext-menu-menuitem-click",
          info,
          contextData.tab
        );
      },
      { once: true }
    );

    // Don't publish the ID of the root because the root element is
    // auto-generated.
    if (item.parent) {
      gShownMenuItems.get(item.extension).push(item.id);
    }

    return element;
  },

  setMenuItemIcon(element, extension, contextData, icons) {
    let parentWindow = contextData.menu.ownerGlobal;

    let { icon } = IconDetails.getPreferredIcon(
      icons,
      extension,
      16 * parentWindow.devicePixelRatio
    );

    // The extension icons in the manifest are not pre-resolved, since
    // they're sometimes used by the add-on manager when the extension is
    // not enabled, and its URLs are not resolvable.
    let resolvedURL = extension.baseURI.resolve(icon);

    if (element.localName == "menu") {
      element.setAttribute("class""menu-iconic");
    } else if (element.localName == "menuitem") {
      element.setAttribute("class""menuitem-iconic");
    }

    element.setAttribute("image", resolvedURL);
  },

  // Undo changes from setMenuItemIcon.
  removeMenuItemIcon(element) {
    element.removeAttribute("class");
    element.removeAttribute("image");
  },

  rebuildMenu(extension) {
    let { contextData } = this;
    if (!contextData) {
      // This happens if the menu is not visible.
      return;
    }

    // Find the group of existing top-level items (usually 0 or 1 items)
    // and remember its position for when the new items are inserted.
    let elementIdPrefix = `${makeWidgetId(extension.id)}-menuitem-`;
    let nextSibling = null;
    for (let item of this.itemsToCleanUp) {
      if (item.id && item.id.startsWith(elementIdPrefix)) {
        nextSibling = item.nextSibling;
        item.remove();
        this.itemsToCleanUp.delete(item);
      }
    }

    let root = gRootItems.get(extension);
    if (root) {
      this.createAndInsertTopLevelElements(root, contextData, nextSibling);
    }

    this.xulMenu.showHideSeparators?.();
  },

  // This should be called once, after constructing the top-level menus, if any.
  afterBuildingMenu(contextData) {
    let dispatchOnShownEvent = extension => {
      if (!this.canAccessContext(extension, contextData)) {
        return;
      }

      // Note: gShownMenuItems is a DefaultMap, so .get(extension) causes the
      // extension to be stored in the map even if there are currently no
      // shown menu items. This ensures that the onHidden event can be fired
      // when the menu is closed.
      let menuIds = gShownMenuItems.get(extension);
      extension.emit("webext-menu-shown", menuIds, contextData);
    };

    if (
      contextData.onAction ||
      contextData.onBrowserAction ||
      contextData.onPageAction
    ) {
      dispatchOnShownEvent(contextData.extension);
    } else {
      for (const extension of gOnShownSubscribers.keys()) {
        dispatchOnShownEvent(extension);
      }
    }

    this.contextData = contextData;
  },

  hideDefaultMenuItems() {
    for (let item of this.xulMenu.children) {
      if (!this.itemsToCleanUp.has(item)) {
        item.hidden = true;
      }
    }

    if (this.xulMenu.showHideSeparators) {
      this.xulMenu.showHideSeparators();
    }
  },

  handleEvent(event) {
    if (this.xulMenu != event.target || event.type != "popuphidden") {
      return;
    }

    delete this.xulMenu;
    delete this.contextData;

    let target = event.target;
    target.removeEventListener("popuphidden"this);
    for (let item of this.itemsToCleanUp) {
      item.remove();
    }
    this.itemsToCleanUp.clear();
    for (let extension of gShownMenuItems.keys()) {
      extension.emit("webext-menu-hidden");
    }
    gShownMenuItems.clear();
  },

  itemsToCleanUp: new Set(),
};

// Called from pageAction or browserAction popup.
global.actionContextMenu = function (contextData) {
  contextData.tab = tabTracker.activeTab;
  contextData.pageUrl = contextData.tab.linkedBrowser.currentURI.spec;
  gMenuBuilder.build(contextData);
};

const contextsMap = {
  onAudio: "audio",
  onEditable: "editable",
  inFrame: "frame",
  onImage: "image",
  onLink: "link",
  onPassword: "password",
  isTextSelected: "selection",
  onVideo: "video",

  onBookmark: "bookmark",
  onAction: "action",
  onBrowserAction: "browser_action",
  onPageAction: "page_action",
  onTab: "tab",
  inToolsMenu: "tools_menu",
};

const getMenuContexts = contextData => {
  let contexts = new Set();

  for (const [key, value] of Object.entries(contextsMap)) {
    if (contextData[key]) {
      contexts.add(value);
    }
  }

  if (contexts.size === 0) {
    contexts.add("page");
  }

  // New non-content contexts supported in Firefox are not part of "all".
  if (
    !contextData.onBookmark &&
    !contextData.onTab &&
    !contextData.inToolsMenu
  ) {
    contexts.add("all");
  }

  return contexts;
};

function getContextViewType(contextData) {
  if ("originalViewType" in contextData) {
    return contextData.originalViewType;
  }
  if (
    contextData.webExtBrowserType === "popup" ||
    contextData.webExtBrowserType === "sidebar"
  ) {
    return contextData.webExtBrowserType;
  }
  if (contextData.tab && contextData.menu.id === "contentAreaContextMenu") {
    return "tab";
  }
  return undefined;
}

function addMenuEventInfo(info, contextData, extension, includeSensitiveData) {
  info.viewType = getContextViewType(contextData);
  if (contextData.onVideo) {
    info.mediaType = "video";
  } else if (contextData.onAudio) {
    info.mediaType = "audio";
  } else if (contextData.onImage) {
    info.mediaType = "image";
  }
  if (contextData.frameId !== undefined) {
    info.frameId = contextData.frameId;
  }
  if (contextData.onBookmark) {
    info.bookmarkId = contextData.bookmarkId;
  }
  info.editable = contextData.onEditable || false;
  if (includeSensitiveData) {
    // menus.getTargetElement requires the "menus" permission, so do not set
    // targetElementId for extensions with only the "contextMenus" permission.
    if (contextData.timeStamp && extension.hasPermission("menus")) {
      // Convert to integer, in case the DOMHighResTimeStamp has a fractional part.
      info.targetElementId = Math.floor(contextData.timeStamp);
    }
    if (contextData.onLink) {
      info.linkText = contextData.linkText;
      info.linkUrl = contextData.linkUrl;
    }
    if (contextData.onAudio || contextData.onImage || contextData.onVideo) {
      info.srcUrl = contextData.srcUrl;
    }
    if (!contextData.onBookmark) {
      info.pageUrl = contextData.pageUrl;
    }
    if (contextData.inFrame) {
      info.frameUrl = contextData.frameUrl;
    }
    if (contextData.isTextSelected) {
      info.selectionText = contextData.selectionText;
    }
  }
  // If the context was overridden, then frameUrl should be the URL of the
  // document in which the menu was opened (instead of undefined, even if that
  // document is not in a frame).
  if (contextData.originalViewUrl) {
    info.frameUrl = contextData.originalViewUrl;
  }
}

class MenuItem {
  constructor(extension, createProperties, isRoot = false) {
    this.extension = extension;
    this.children = [];
    this.parent = null;
    this.tabManager = extension.tabManager;

    this.setDefaults();
    this.setProps(createProperties);

    if (!this.hasOwnProperty("_id")) {
      this.id = gNextMenuItemID++;
    }
    // If the item is not the root and has no parent
    // it must be a child of the root.
    if (!isRoot && !this.parent) {
      this.root.addChild(this);
    }
  }

  setProps(createProperties) {
    ExtensionMenus.mergeMenuProperties(this, createProperties);

    if (createProperties.documentUrlPatterns != null) {
      this.documentUrlMatchPattern = parseMatchPatterns(
        this.documentUrlPatterns,
        {
          restrictSchemes: this.extension.restrictSchemes,
        }
      );
    }

    if (createProperties.targetUrlPatterns != null) {
      this.targetUrlMatchPattern = parseMatchPatterns(this.targetUrlPatterns, {
        // restrictSchemes default to false when matching links instead of pages
        // (see Bug 1280370 for a rationale).
        restrictSchemes: false,
      });
    }

    // If a child MenuItem does not specify any contexts, then it should
    // inherit the contexts specified from its parent.
    if (createProperties.parentId && !createProperties.contexts) {
      this.contexts = this.parent.contexts;
    }
  }

  setDefaults() {
    this.setProps({
      type: "normal",
      checked: false,
      contexts: ["all"],
      enabled: true,
      visible: true,
    });
  }

  set id(id) {
    if (this.hasOwnProperty("_id")) {
      throw new ExtensionError("ID of a MenuItem cannot be changed");
    }
    let isIdUsed = gMenuMap.get(this.extension).has(id);
    if (isIdUsed) {
      throw new ExtensionError(`ID already exists: ${id}`);
    }
    this._id = id;
  }

  get id() {
    return this._id;
  }

  get elementId() {
    let id = this.id;
    // If the ID is an integer, it is auto-generated and globally unique.
    // If the ID is a string, it is only unique within one extension and the
    // ID needs to be concatenated with the extension ID.
    if (typeof id !== "number") {
      // To avoid collisions with numeric IDs, add a prefix to string IDs.
      id = `_${id}`;
    }
    return `${makeWidgetId(this.extension.id)}-menuitem-${id}`;
  }

  ensureValidParentId(parentId) {
    if (parentId === undefined) {
      return;
    }
    let menuMap = gMenuMap.get(this.extension);
    if (!menuMap.has(parentId)) {
      throw new ExtensionError(`Cannot find menu item with id ${parentId}`);
    }
    for (let item = menuMap.get(parentId); item; item = item.parent) {
      if (item === this) {
        throw new ExtensionError(
          "MenuItem cannot be an ancestor (or self) of its new parent."
        );
      }
    }
  }

  set parentId(parentId) {
    this.ensureValidParentId(parentId);

    if (this.parent) {
      this.parent.detachChild(this);
    }

    if (parentId === undefined) {
      this.root.addChild(this);
    } else {
      let menuMap = gMenuMap.get(this.extension);
      menuMap.get(parentId).addChild(this);
    }
  }

  get parentId() {
    return this.parent ? this.parent.id : undefined;
  }

  addChild(child) {
    if (child.parent) {
      throw new Error("Child MenuItem already has a parent.");
    }
    this.children.push(child);
    child.parent = this;
  }

  detachChild(child) {
    let idx = this.children.indexOf(child);
    if (idx < 0) {
      throw new Error("Child MenuItem not found, it cannot be removed.");
    }
    this.children.splice(idx, 1);
    child.parent = null;
  }

  get root() {
    let extension = this.extension;
    if (!gRootItems.has(extension)) {
      let root = new MenuItem(
        extension,
        { title: extension.name },
        /* isRoot = */ true
      );
      gRootItems.set(extension, root);
    }

    return gRootItems.get(extension);
  }

  get descendantIds() {
    return this.children
      ? this.children.flatMap(m => [m.id, ...m.descendantIds])
      : [];
  }

  remove() {
    if (this.parent) {
      this.parent.detachChild(this);
    }
    let children = this.children.slice(0);
    for (let child of children) {
      child.remove();
    }

    let menuMap = gMenuMap.get(this.extension);
    menuMap.delete(this.id);
    if (this.root == this) {
      gRootItems.delete(this.extension);
    }
  }

  getClickInfo(contextData, wasChecked) {
    let info = {
      menuItemId: this.id,
    };
    if (this.parent) {
      info.parentMenuItemId = this.parentId;
    }

    addMenuEventInfo(info, contextData, this.extension, true);

    if (this.type === "checkbox" || this.type === "radio") {
      info.checked = this.checked;
      info.wasChecked = wasChecked;
    }

    return info;
  }

  enabledForContext(contextData) {
    if (!this.visible) {
      return false;
    }
    let contexts = getMenuContexts(contextData);
    if (!this.contexts.some(n => contexts.has(n))) {
      return false;
    }

    if (
      this.viewTypes &&
      !this.viewTypes.includes(getContextViewType(contextData))
    ) {
      return false;
    }

    let docPattern = this.documentUrlMatchPattern;
    // When viewTypes is specified, the menu item is expected to be restricted
    // to documents. So let documentUrlPatterns always apply to the URL of the
    // document in which the menu was opened. When maybeOverrideContextData
    // changes the context, contextData.pageUrl does not reflect that URL any
    // more, so use contextData.originalViewUrl instead.
    if (docPattern && this.viewTypes && contextData.originalViewUrl) {
      if (
        !docPattern.matches(Services.io.newURI(contextData.originalViewUrl))
      ) {
        return false;
      }
      docPattern = null// Null it so that it won't be used with pageURI below.
    }

    if (contextData.onBookmark) {
      return this.extension.hasPermission("bookmarks");
    }

    let pageURI = Services.io.newURI(
      contextData[contextData.inFrame ? "frameUrl" : "pageUrl"]
    );
    if (docPattern && !docPattern.matches(pageURI)) {
      return false;
    }

    let targetPattern = this.targetUrlMatchPattern;
    if (targetPattern) {
      let targetURIs = [];
      if (contextData.onImage || contextData.onAudio || contextData.onVideo) {
        // TODO: double check if srcUrl is always set when we need it
        targetURIs.push(Services.io.newURI(contextData.srcUrl));
      }
      // contextData.linkURI may be null despite contextData.onLink, when
      // contextData.linkUrl is an invalid URL.
      if (contextData.onLink && contextData.linkURI) {
        targetURIs.push(contextData.linkURI);
      }
      if (!targetURIs.some(targetURI => targetPattern.matches(targetURI))) {
        return false;
      }
    }

    return true;
  }
}

// windowTracker only looks as browser windows, but we're also interested in
// the Library window.  Helper for menuTracker below.
const libraryTracker = {
  libraryWindowType: "Places:Organizer",

  isLibraryWindow(window) {
    let winType = window.document.documentElement.getAttribute("windowtype");
    return winType === this.libraryWindowType;
  },

  init(listener) {
    this._listener = listener;
    Services.ww.registerNotification(this);

    // See WindowTrackerBase#*browserWindows in ext-tabs-base.js for why we
    // can't use the enumerator's windowtype filter.
    for (let window of Services.wm.getEnumerator("")) {
      if (window.document.readyState === "complete") {
        if (this.isLibraryWindow(window)) {
          this.notify(window);
        }
      } else {
        window.addEventListener("load"this, { once: true });
      }
    }
  },

  // cleanupWindow is called on any library window that's open.
  uninit(cleanupWindow) {
    Services.ww.unregisterNotification(this);

    for (let window of Services.wm.getEnumerator("")) {
      window.removeEventListener("load"this);
      try {
        if (this.isLibraryWindow(window)) {
          cleanupWindow(window);
        }
      } catch (e) {
        Cu.reportError(e);
      }
    }
  },

  // Gets notifications from Services.ww.registerNotification.
  // Defer actually doing anything until the window's loaded, though.
  observe(window, topic) {
    if (topic === "domwindowopened") {
      window.addEventListener("load"this, { once: true });
    }
  },

  // Gets the load event for new windows(registered in observe()).
  handleEvent(event) {
    let window = event.target.defaultView;
    if (this.isLibraryWindow(window)) {
      this.notify(window);
    }
  },

  notify(window) {
    try {
      this._listener.call(null, window);
    } catch (e) {
      Cu.reportError(e);
    }
  },
};

// While any extensions are active, this Tracker registers to observe/listen
// for menu events from both Tools and context menus, both content and chrome.
const menuTracker = {
  menuIds: ["placesContext""menu_ToolsPopup""tabContextMenu"],

  register() {
    Services.obs.addObserver(this"on-build-contextmenu");
    for (const window of windowTracker.browserWindows()) {
      this.onWindowOpen(window);
    }
    windowTracker.addOpenListener(this.onWindowOpen);
    libraryTracker.init(this.onLibraryOpen);
  },

  unregister() {
    Services.obs.removeObserver(this"on-build-contextmenu");
    for (const window of windowTracker.browserWindows()) {
      this.cleanupWindow(window);
    }
    windowTracker.removeOpenListener(this.onWindowOpen);
    libraryTracker.uninit(this.cleanupLibrary);
  },

  observe(subject) {
    subject = subject.wrappedJSObject;
    gMenuBuilder.build(subject);
  },

  async onWindowOpen(window) {
    for (const id of menuTracker.menuIds) {
      const menu = window.document.getElementById(id);
      menu.addEventListener("popupshowing", menuTracker);
    }

    const sidebarHeader = window.document.getElementById(
      "sidebar-switcher-target"
    );
    sidebarHeader.addEventListener("SidebarShown", menuTracker.onSidebarShown);

    await window.SidebarController.promiseInitialized;

    if (
      !window.closed &&
      window.SidebarController.currentID === "viewBookmarksSidebar"
    ) {
      menuTracker.onSidebarShown({ currentTarget: sidebarHeader });
    }
  },

  cleanupWindow(window) {
    for (const id of this.menuIds) {
      const menu = window.document.getElementById(id);
      menu.removeEventListener("popupshowing"this);
    }

    const sidebarHeader = window.document.getElementById(
      "sidebar-switcher-target"
    );
    sidebarHeader.removeEventListener("SidebarShown"this.onSidebarShown);

    if (window.SidebarController.currentID === "viewBookmarksSidebar") {
      let sidebarBrowser = window.SidebarController.browser;
      sidebarBrowser.removeEventListener("load"this.onSidebarShown);
      const menu =
        sidebarBrowser.contentDocument.getElementById("placesContext");
      menu.removeEventListener("popupshowing"this.onBookmarksContextMenu);
    }
  },

  onSidebarShown(event) {
    // The event target is an element in a browser window, so |window| will be
    // the browser window that contains the sidebar.
    const window = event.currentTarget.ownerGlobal;
    if (window.SidebarController.currentID === "viewBookmarksSidebar") {
      let sidebarBrowser = window.SidebarController.browser;
      if (sidebarBrowser.contentDocument.readyState !== "complete") {
        // SidebarController.currentID may be updated before the bookmark sidebar's
        // document has finished loading. This sometimes happens when the
        // sidebar is automatically shown when a new window is opened.
        sidebarBrowser.addEventListener("load", menuTracker.onSidebarShown, {
          once: true,
        });
        return;
      }
      const menu =
        sidebarBrowser.contentDocument.getElementById("placesContext");
      menu.addEventListener("popupshowing", menuTracker.onBookmarksContextMenu);
    }
  },

  onLibraryOpen(window) {
    const menu = window.document.getElementById("placesContext");
    menu.addEventListener("popupshowing", menuTracker.onBookmarksContextMenu);
  },

  cleanupLibrary(window) {
    const menu = window.document.getElementById("placesContext");
    menu.removeEventListener(
      "popupshowing",
      menuTracker.onBookmarksContextMenu
    );
  },

  handleEvent(event) {
    const menu = event.target;

    if (menu.id === "placesContext") {
      const trigger = menu.triggerNode;
      if (!trigger._placesNode?.bookmarkGuid) {
        return;
      }

      gMenuBuilder.build({
        menu,
        bookmarkId: trigger._placesNode.bookmarkGuid,
        onBookmark: true,
      });
    }
    if (menu.id === "menu_ToolsPopup") {
      const tab = tabTracker.activeTab;
      const pageUrl = tab.linkedBrowser.currentURI.spec;
      gMenuBuilder.build({ menu, tab, pageUrl, inToolsMenu: true });
    }
    if (menu.id === "tabContextMenu") {
      const tab = menu.ownerGlobal.TabContextMenu.contextTab;
      const pageUrl = tab.linkedBrowser.currentURI.spec;
      gMenuBuilder.build({ menu, tab, pageUrl, onTab: true });
    }
  },

  onBookmarksContextMenu(event) {
    const menu = event.target;
    const tree = menu.triggerNode.parentElement;
    const cell = tree.getCellAt(event.x, event.y);
    const node = tree.view.nodeForTreeIndex(cell.row);
    const bookmarkId = node && PlacesUtils.getConcreteItemGuid(node);

    if (!bookmarkId || PlacesUtils.isVirtualLeftPaneItem(bookmarkId)) {
      return;
    }

    gMenuBuilder.build({ menu, bookmarkId, onBookmark: true });
  },
};

this.menusInternal = class extends ExtensionAPIPersistent {
  #promiseInitialized = null;

  constructor(extension) {
    super(extension);

    if (!gMenuMap.size) {
      menuTracker.register();
    }
    gMenuMap.set(extension, new Map());
  }

  async initExtensionMenus() {
    let { extension } = this;

    await ExtensionMenus.asyncInitForExtension(extension);

    if (
      extension.hasShutdown ||
      !ExtensionMenus.shouldPersistMenus(extension)
    ) {
      return;
    }

    // Used for testing
    const notifyMenusCreated = () =>
      extension.emit("webext-menus-created", gMenuMap.get(extension));

    const menus = ExtensionMenus.getMenus(extension);
    if (!menus.size) {
      notifyMenusCreated();
      return;
    }

    let createErrorMenuIds = [];
    for (let createProperties of menus.values()) {
      // The order of menu creation is significant:
      // When creating and reparenting the menu we ensure parents exist
      // in the persisted menus map before children.  That allows the
      // menus to be recreated in the correct sequence on startup.
      //
      // For details, see ExtensionMenusManager's updateMenus in
      // ExtensionMenus.sys.mjs
      try {
        let menuItem = new MenuItem(extension, createProperties);
        gMenuMap.get(extension).set(menuItem.id, menuItem);
      } catch (err) {
        Cu.reportError(
          `Unexpected error on recreating persisted menu ${createProperties?.id} for ${extension.id}: ${err}`
        );
        createErrorMenuIds.push(createProperties.id);
      }
    }

    if (createErrorMenuIds.length) {
      ExtensionMenus.deleteMenus(extension, createErrorMenuIds);
    }

    notifyMenusCreated();
  }

  onStartup() {
    this.#promiseInitialized = this.initExtensionMenus();
  }

  onShutdown() {
    let { extension } = this;

    if (gMenuMap.has(extension)) {
      gMenuMap.delete(extension);
      gRootItems.delete(extension);
      gShownMenuItems.delete(extension);
      gOnShownSubscribers.delete(extension);
      if (!gMenuMap.size) {
        menuTracker.unregister();
      }
    }
  }

  PERSISTENT_EVENTS = {
    onShown({ fire }) {
      let { extension } = this;
      let listener = (event, menuIds, contextData) => {
        let info = {
          menuIds,
          contexts: Array.from(getMenuContexts(contextData)),
        };

        let nativeTab = contextData.tab;

        // The menus.onShown event is fired before the user has consciously
        // interacted with an extension, so we require permissions before
        // exposing sensitive contextual data.
        let contextUrl = contextData.inFrame
          ? contextData.frameUrl
          : contextData.pageUrl;
        let includeSensitiveData =
          (nativeTab &&
            extension.tabManager.hasActiveTabPermission(nativeTab)) ||
          (contextUrl && extension.allowedOrigins.matches(contextUrl));

        addMenuEventInfo(info, contextData, extension, includeSensitiveData);

        let tab = nativeTab && extension.tabManager.convert(nativeTab);
        fire.sync(info, tab);
      };
      gOnShownSubscribers.get(extension).add(listener);
      extension.on("webext-menu-shown", listener);
      return {
        unregister() {
          const listeners = gOnShownSubscribers.get(extension);
          listeners.delete(listener);
          if (listeners.size === 0) {
            gOnShownSubscribers.delete(extension);
          }
          extension.off("webext-menu-shown", listener);
        },
        convert(_fire) {
          fire = _fire;
        },
      };
    },
    onHidden({ fire }) {
      let { extension } = this;
      let listener = () => {
        fire.sync();
      };
      extension.on("webext-menu-hidden", listener);
      return {
        unregister() {
          extension.off("webext-menu-hidden", listener);
        },
        convert(_fire) {
          fire = _fire;
        },
      };
    },
    onClicked({ context, fire }) {
      let { extension } = this;
      let listener = async (event, info, nativeTab) => {
        let { linkedBrowser } = nativeTab || tabTracker.activeTab;
        let tab = nativeTab && extension.tabManager.convert(nativeTab);
        if (fire.wakeup) {
          // force the wakeup, thus the call to convert to get the context.
          await fire.wakeup();
          // If while waiting the tab disappeared we bail out.
          if (
            !linkedBrowser.ownerGlobal.gBrowser.getTabForBrowser(linkedBrowser)
          ) {
            Cu.reportError(
              `menus.onClicked: target tab closed during background startup.`
            );
            return;
          }
        }
        context.withPendingBrowser(linkedBrowser, () => fire.sync(info, tab));
      };

      extension.on("webext-menu-menuitem-click", listener);
      return {
        unregister() {
          extension.off("webext-menu-menuitem-click", listener);
        },
        convert(_fire, _context) {
          fire = _fire;
          context = _context;
        },
      };
    },
  };

  getAPI(context) {
    let { extension } = context;

    const menus = {
      refresh() {
        gMenuBuilder.rebuildMenu(extension);
      },

      onShown: new EventManager({
        context,
        module: "menusInternal",
        event: "onShown",
        name: "menus.onShown",
        extensionApi: this,
      }).api(),
      onHidden: new EventManager({
        context,
        module: "menusInternal",
        event: "onHidden",
        name: "menus.onHidden",
        extensionApi: this,
      }).api(),
    };

    return {
      contextMenus: menus,
      menus,
      menusInternal: {
        create: async createProperties => {
          await this.#promiseInitialized;
          if (extension.hasShutdown) {
            return;
          }

          // event pages require id
          if (ExtensionMenus.shouldPersistMenus(extension)) {
            if (!createProperties.id) {
              throw new ExtensionError(
                "menus.create requires an id for non-persistent background scripts."
              );
            }
            if (gMenuMap.get(extension).has(createProperties.id)) {
              throw new ExtensionError(
                `The menu id ${createProperties.id} already exists in menus.create.`
              );
            }
          }

          // Note that the id is required by the schema. If the addon did not set
          // it, the implementation of menus.create in the child will add it for
          // extensions with persistent backgrounds, but not otherwise.

          let menuItem = new MenuItem(extension, createProperties);
          ExtensionMenus.addMenu(extension, createProperties);
          gMenuMap.get(extension).set(menuItem.id, menuItem);
        },

        update: async (id, updateProperties) => {
          await this.#promiseInitialized;
          if (extension.hasShutdown) {
            return;
          }

          let menuItem = gMenuMap.get(extension).get(id);
          if (!menuItem) {
            throw new ExtensionError(`Cannot find menu item with id ${id}`);
          }

          menuItem.setProps(updateProperties);
          ExtensionMenus.updateMenu(extension, id, updateProperties);
        },

        remove: async id => {
          await this.#promiseInitialized;
          if (extension.hasShutdown) {
            return;
          }

          let menuItem = gMenuMap.get(extension).get(id);
          if (!menuItem) {
            throw new ExtensionError(`Cannot find menu item with id ${id}`);
          }

          const menuIds = [menuItem.id, ...menuItem.descendantIds];
          menuItem.remove();
          ExtensionMenus.deleteMenus(extension, menuIds);
        },

        removeAll: async () => {
          await this.#promiseInitialized;
          if (extension.hasShutdown) {
            return;
          }

          let root = gRootItems.get(extension);
          if (root) {
            root.remove();
          }
          ExtensionMenus.deleteAllMenus(extension);
        },

        onClicked: new EventManager({
          context,
          module: "menusInternal",
          event: "onClicked",
          name: "menus.onClicked",
          extensionApi: this,
        }).api(),
      },
    };
  }
};

Messung V0.5
C=94 H=98 G=95

¤ Dauer der Verarbeitung: 0.21 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.