forked from mirrors/gecko-dev
		
	Depends on D175553 Differential Revision: https://phabricator.services.mozilla.com/D176005
		
			
				
	
	
		
			1471 lines
		
	
	
	
		
			43 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1471 lines
		
	
	
	
		
			43 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- 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, {
 | 
						|
  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, StartupCache } = 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 -> Map[ID -> MenuCreateProperties]]
 | 
						|
// The map object for each extension is a reference to the same
 | 
						|
// object in StartupCache.menus.  This provides a non-async
 | 
						|
// getter for that object.
 | 
						|
var gStartupCache = 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, item) {
 | 
						|
    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);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static mergeProps(obj, properties) {
 | 
						|
    for (let propName in properties) {
 | 
						|
      if (properties[propName] === null) {
 | 
						|
        // Omitted optional argument.
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      obj[propName] = properties[propName];
 | 
						|
    }
 | 
						|
 | 
						|
    if ("icons" in properties && properties.icons === null && obj.icons) {
 | 
						|
      obj.icons = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  setProps(createProperties) {
 | 
						|
    MenuItem.mergeProps(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(
 | 
						|
        `Could not find any MenuItem 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."
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * When updating menu properties we need to ensure parents exist
 | 
						|
   * in the cache map before children.  That allows the menus to be
 | 
						|
   * created in the correct sequence on startup.  This reparents the
 | 
						|
   * tree starting from this instance of MenuItem.
 | 
						|
   */
 | 
						|
  reparentInCache() {
 | 
						|
    let { id, extension } = this;
 | 
						|
    let cachedMap = gStartupCache.get(extension);
 | 
						|
    let createProperties = cachedMap.get(id);
 | 
						|
    cachedMap.delete(id);
 | 
						|
    cachedMap.set(id, createProperties);
 | 
						|
 | 
						|
    for (let child of this.children) {
 | 
						|
      child.reparentInCache();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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);
 | 
						|
  }
 | 
						|
 | 
						|
  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);
 | 
						|
    // Menu items are saved if !extension.persistentBackground.
 | 
						|
    if (gStartupCache.get(this.extension)?.delete(this.id)) {
 | 
						|
      StartupCache.save();
 | 
						|
    }
 | 
						|
    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, topic, data) {
 | 
						|
    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.SidebarUI.promiseInitialized;
 | 
						|
 | 
						|
    if (
 | 
						|
      !window.closed &&
 | 
						|
      window.SidebarUI.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.SidebarUI.currentID === "viewBookmarksSidebar") {
 | 
						|
      let sidebarBrowser = window.SidebarUI.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.SidebarUI.currentID === "viewBookmarksSidebar") {
 | 
						|
      let sidebarBrowser = window.SidebarUI.browser;
 | 
						|
      if (sidebarBrowser.contentDocument.readyState !== "complete") {
 | 
						|
        // SidebarUI.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 {
 | 
						|
  constructor(extension) {
 | 
						|
    super(extension);
 | 
						|
 | 
						|
    if (!gMenuMap.size) {
 | 
						|
      menuTracker.register();
 | 
						|
    }
 | 
						|
    gMenuMap.set(extension, new Map());
 | 
						|
  }
 | 
						|
 | 
						|
  restoreFromCache() {
 | 
						|
    let { extension } = this;
 | 
						|
    // ensure extension has not shutdown
 | 
						|
    if (!this.extension) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    for (let createProperties of gStartupCache.get(extension).values()) {
 | 
						|
      // The order of menu creation is significant, see reparentInCache.
 | 
						|
      let menuItem = new MenuItem(extension, createProperties);
 | 
						|
      gMenuMap.get(extension).set(menuItem.id, menuItem);
 | 
						|
    }
 | 
						|
    // Used for testing
 | 
						|
    extension.emit("webext-menus-created", gMenuMap.get(extension));
 | 
						|
  }
 | 
						|
 | 
						|
  async onStartup() {
 | 
						|
    let { extension } = this;
 | 
						|
    if (extension.persistentBackground) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // Using the map retains insertion order.
 | 
						|
    let cachedMenus = await StartupCache.menus.get(extension.id, () => {
 | 
						|
      return new Map();
 | 
						|
    });
 | 
						|
    gStartupCache.set(extension, cachedMenus);
 | 
						|
    if (!cachedMenus.size) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.restoreFromCache();
 | 
						|
  }
 | 
						|
 | 
						|
  onShutdown() {
 | 
						|
    let { extension } = this;
 | 
						|
 | 
						|
    if (gMenuMap.has(extension)) {
 | 
						|
      gMenuMap.delete(extension);
 | 
						|
      gRootItems.delete(extension);
 | 
						|
      gShownMenuItems.delete(extension);
 | 
						|
      gStartupCache.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(createProperties) {
 | 
						|
          // event pages require id
 | 
						|
          if (!extension.persistentBackground) {
 | 
						|
            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);
 | 
						|
          gMenuMap.get(extension).set(menuItem.id, menuItem);
 | 
						|
          if (!extension.persistentBackground) {
 | 
						|
            // Only cache properties that are necessary.
 | 
						|
            let cached = {};
 | 
						|
            MenuItem.mergeProps(cached, createProperties);
 | 
						|
            gStartupCache.get(extension).set(menuItem.id, cached);
 | 
						|
            StartupCache.save();
 | 
						|
          }
 | 
						|
        },
 | 
						|
 | 
						|
        update(id, updateProperties) {
 | 
						|
          let menuItem = gMenuMap.get(extension).get(id);
 | 
						|
          if (!menuItem) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          menuItem.setProps(updateProperties);
 | 
						|
 | 
						|
          // Update the startup cache for non-persistent extensions.
 | 
						|
          if (extension.persistentBackground) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
 | 
						|
          let cached = gStartupCache.get(extension).get(id);
 | 
						|
          let reparent =
 | 
						|
            updateProperties.parentId != null &&
 | 
						|
            cached.parentId != updateProperties.parentId;
 | 
						|
          MenuItem.mergeProps(cached, updateProperties);
 | 
						|
          if (reparent) {
 | 
						|
            // The order of menu creation is significant, see reparentInCache.
 | 
						|
            menuItem.reparentInCache();
 | 
						|
          }
 | 
						|
          StartupCache.save();
 | 
						|
        },
 | 
						|
 | 
						|
        remove(id) {
 | 
						|
          let menuItem = gMenuMap.get(extension).get(id);
 | 
						|
          if (menuItem) {
 | 
						|
            menuItem.remove();
 | 
						|
          }
 | 
						|
        },
 | 
						|
 | 
						|
        removeAll() {
 | 
						|
          let root = gRootItems.get(extension);
 | 
						|
          if (root) {
 | 
						|
            root.remove();
 | 
						|
          }
 | 
						|
          // Should be empty, just extra assurance.
 | 
						|
          if (!extension.persistentBackground) {
 | 
						|
            let cached = gStartupCache.get(extension);
 | 
						|
            if (cached.size) {
 | 
						|
              cached.clear();
 | 
						|
              StartupCache.save();
 | 
						|
            }
 | 
						|
          }
 | 
						|
        },
 | 
						|
 | 
						|
        onClicked: new EventManager({
 | 
						|
          context,
 | 
						|
          module: "menusInternal",
 | 
						|
          event: "onClicked",
 | 
						|
          name: "menus.onClicked",
 | 
						|
          extensionApi: this,
 | 
						|
        }).api(),
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
};
 |