Bug 1333403 - Part 2: Implement browser.menus as alias for contextMenus draft
authorTomislav Jovanovic <tomica@gmail.com>
Sat, 10 Jun 2017 16:43:20 +0200
changeset 592123 e0e2b1864f74df220260e573ef90b4dfa6ebf820
parent 592122 c2ccd2821f7a73bf683f366d81ede9819613c982
child 592124 3a3db094eccd0dbe5288d200a584c84b0c351f09
push id63281
push userbmo:tomica@gmail.com
push dateSat, 10 Jun 2017 14:46:45 +0000
bugs1333403
milestone55.0a1
Bug 1333403 - Part 2: Implement browser.menus as alias for contextMenus MozReview-Commit-ID: JPaKsOyavDb
browser/components/extensions/ext-browser.js
browser/components/extensions/ext-c-browser.js
browser/components/extensions/ext-c-contextMenus.js
browser/components/extensions/ext-c-menus.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-menus.js
browser/components/extensions/extensions-browser.manifest
browser/components/extensions/jar.mn
browser/components/extensions/schemas/context_menus.json
browser/components/extensions/schemas/context_menus_internal.json
browser/components/extensions/schemas/jar.mn
browser/components/extensions/schemas/menus.json
browser/components/extensions/schemas/menus_internal.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
browser/components/extensions/test/browser/browser_ext_menus.js
toolkit/components/extensions/ExtensionCommon.jsm
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -110,24 +110,16 @@ extensions.registerModules({
     url: "chrome://browser/content/ext-commands.js",
     schema: "chrome://browser/content/schemas/commands.json",
     scopes: ["addon_parent"],
     manifest: ["commands"],
     paths: [
       ["commands"],
     ],
   },
-  contextMenus: {
-    url: "chrome://browser/content/ext-contextMenus.js",
-    schema: "chrome://browser/content/schemas/context_menus.json",
-    scopes: ["addon_parent"],
-    paths: [
-      ["contextMenus"],
-    ],
-  },
   devtools: {
     url: "chrome://browser/content/ext-devtools.js",
     schema: "chrome://browser/content/schemas/devtools.json",
     scopes: ["devtools_parent"],
     manifest: ["devtools_page"],
     paths: [
       ["devtools"],
     ],
@@ -159,16 +151,26 @@ extensions.registerModules({
   history: {
     url: "chrome://browser/content/ext-history.js",
     schema: "chrome://browser/content/schemas/history.json",
     scopes: ["addon_parent"],
     paths: [
       ["history"],
     ],
   },
+  // This module supports the "menus" and "contextMenus" namespaces,
+  // and because of permissions, the module name must differ from both.
+  menusInternal: {
+    url: "chrome://browser/content/ext-menus.js",
+    schema: "chrome://browser/content/schemas/menus.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["menusInternal"],
+    ],
+  },
   omnibox: {
     url: "chrome://browser/content/ext-omnibox.js",
     schema: "chrome://browser/content/schemas/omnibox.json",
     scopes: ["addon_parent"],
     manifest: ["omnibox"],
     paths: [
       ["omnibox"],
     ],
--- a/browser/components/extensions/ext-c-browser.js
+++ b/browser/components/extensions/ext-c-browser.js
@@ -17,21 +17,23 @@ extensions.registerModules({
   },
   devtools_panels: {
     url: "chrome://browser/content/ext-c-devtools-panels.js",
     scopes: ["devtools_child"],
     paths: [
       ["devtools", "panels"],
     ],
   },
-  contextMenus: {
-    url: "chrome://browser/content/ext-c-contextMenus.js",
+  // Because of permissions, the module name must differ from both namespaces.
+  menusInternal: {
+    url: "chrome://browser/content/ext-c-menus.js",
     scopes: ["addon_child"],
     paths: [
       ["contextMenus"],
+      ["menus"],
     ],
   },
   omnibox: {
     url: "chrome://browser/content/ext-c-omnibox.js",
     scopes: ["addon_child"],
     paths: [
       ["omnibox"],
     ],
rename from browser/components/extensions/ext-c-contextMenus.js
rename to browser/components/extensions/ext-c-menus.js
--- a/browser/components/extensions/ext-c-contextMenus.js
+++ b/browser/components/extensions/ext-c-menus.js
@@ -1,12 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+// The ext-* files are imported into the same scopes.
+/* import-globals-from ../../../toolkit/components/extensions/ext-c-toolkit.js */
+
 // If id is not specified for an item we use an integer.
 // This ID need only be unique within a single addon. Since all addon code that
 // can use this API runs in the same process, this local variable suffices.
 var gNextMenuItemID = 0;
 
 // Map[Extension -> Map[string or id, ContextMenusClickPropHandler]]
 var gPropHandlers = new Map();
 
@@ -31,17 +34,17 @@ class ContextMenusClickPropHandler {
       onclick(info, tab);
     }
   }
 
   // Sets the `onclick` handler for the given menu item.
   // The `onclick` function MUST be owned by `this.context`.
   setListener(id, onclick) {
     if (this.onclickMap.size === 0) {
-      this.context.childManager.getParentEvent("contextMenus.onClicked").addListener(this.dispatchEvent);
+      this.context.childManager.getParentEvent("menusInternal.onClicked").addListener(this.dispatchEvent);
       this.context.callOnClose(this);
     }
     this.onclickMap.set(id, onclick);
 
     let propHandlerMap = gPropHandlers.get(this.context.extension);
     if (!propHandlerMap) {
       propHandlerMap = new Map();
     } else {
@@ -58,17 +61,17 @@ class ContextMenusClickPropHandler {
 
   // Deletes the `onclick` handler for the given menu item.
   // The `onclick` function MUST be owned by `this.context`.
   unsetListener(id) {
     if (!this.onclickMap.delete(id)) {
       return;
     }
     if (this.onclickMap.size === 0) {
-      this.context.childManager.getParentEvent("contextMenus.onClicked").removeListener(this.dispatchEvent);
+      this.context.childManager.getParentEvent("menusInternal.onClicked").removeListener(this.dispatchEvent);
       this.context.forgetOnClose(this);
     }
     let propHandlerMap = gPropHandlers.get(this.context.extension);
     propHandlerMap.delete(id);
     if (propHandlerMap.size === 0) {
       gPropHandlers.delete(this.context.extension);
     }
   }
@@ -96,65 +99,86 @@ class ContextMenusClickPropHandler {
   // Removes all `onclick` handlers from this context.
   close() {
     for (let id of this.onclickMap.keys()) {
       this.unsetListener(id);
     }
   }
 }
 
-this.contextMenus = class extends ExtensionAPI {
+this.menusInternal = class extends ExtensionAPI {
   getAPI(context) {
     let onClickedProp = new ContextMenusClickPropHandler(context);
 
-    return {
-      contextMenus: {
+    let api = {
+      menus: {
         create(createProperties, callback) {
           if (createProperties.id === null) {
             createProperties.id = ++gNextMenuItemID;
           }
           let {onclick} = createProperties;
           delete createProperties.onclick;
-          context.childManager.callParentAsyncFunction("contextMenus.createInternal", [
+          context.childManager.callParentAsyncFunction("menusInternal.create", [
             createProperties,
           ]).then(() => {
             if (onclick) {
               onClickedProp.setListener(createProperties.id, onclick);
             }
             if (callback) {
               callback();
             }
           });
           return createProperties.id;
         },
 
         update(id, updateProperties) {
           let {onclick} = updateProperties;
           delete updateProperties.onclick;
-          return context.childManager.callParentAsyncFunction("contextMenus.update", [
+          return context.childManager.callParentAsyncFunction("menusInternal.update", [
             id,
             updateProperties,
           ]).then(() => {
             if (onclick) {
               onClickedProp.setListener(id, onclick);
             } else if (onclick === null) {
               onClickedProp.unsetListenerFromAnyContext(id);
             }
             // else onclick is not set so it should not be changed.
           });
         },
 
         remove(id) {
           onClickedProp.unsetListenerFromAnyContext(id);
-          return context.childManager.callParentAsyncFunction("contextMenus.remove", [
+          return context.childManager.callParentAsyncFunction("menusInternal.remove", [
             id,
           ]);
         },
 
         removeAll() {
           onClickedProp.deleteAllListenersFromExtension();
 
-          return context.childManager.callParentAsyncFunction("contextMenus.removeAll", []);
+          return context.childManager.callParentAsyncFunction("menusInternal.removeAll", []);
         },
+
+        onClicked: new SingletonEventManager(context, "menus.onClicked", fire => {
+          let listener = (info, tab) => {
+            fire.async(info, tab);
+          };
+
+          let event = context.childManager.getParentEvent("menusInternal.onClicked");
+          event.addListener(listener);
+          return () => {
+            event.removeListener(listener);
+          };
+        }).api(),
       },
     };
+
+    const result = {};
+    if (context.extension.hasPermission("menus")) {
+      result.menus = api.menus;
+    }
+    if (context.extension.hasPermission("contextMenus")) {
+      result.contextMenus = api.menus;
+    }
+    return result;
   }
 };
rename from browser/components/extensions/ext-contextMenus.js
rename to browser/components/extensions/ext-menus.js
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -20,32 +20,32 @@ 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 gContextMenuMap = new Map();
+var gMenuMap = new Map();
 
 // Map[Extension -> MenuItem]
 var gRootItems = new Map();
 
 // 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 contextMenu is opened, this function is called and
+  // 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) {
     let firstItem = true;
     let xulMenu = contextData.menu;
     xulMenu.addEventListener("popuphidden", this);
     this.xulMenu = xulMenu;
@@ -253,29 +253,29 @@ var gMenuBuilder = {
       let info = item.getClickInfo(contextData, wasChecked);
 
       const map = {shiftKey: "Shift", altKey: "Alt", metaKey: "Command", ctrlKey: "Ctrl"};
       info.modifiers = Object.keys(map).filter(key => event[key]).map(key => map[key]);
       if (event.ctrlKey && AppConstants.platform === "macosx") {
         info.modifiers.push("MacCtrl");
       }
 
-      // Allow context menu's to open various actions supported in webext prior
+      // Allow menus to open various actions supported in webext prior
       // to notifying onclicked.
       let actionFor = {
         _execute_page_action: global.pageActionFor,
         _execute_browser_action: global.browserActionFor,
         _execute_sidebar_action: global.sidebarActionFor,
       }[item.command];
       if (actionFor) {
         let win = event.target.ownerGlobal;
         actionFor(item.extension).triggerAction(win);
       }
 
-      item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
+      item.extension.emit("webext-menu-menuitem-click", info, tab);
     });
 
     return element;
   },
 
   handleEvent(event) {
     if (this.xulMenu != event.target || event.type != "popuphidden") {
       return;
@@ -409,32 +409,32 @@ MenuItem.prototype = {
       enabled: true,
     });
   },
 
   set id(id) {
     if (this.hasOwnProperty("_id")) {
       throw new Error("Id of a MenuItem cannot be changed");
     }
-    let isIdUsed = gContextMenuMap.get(this.extension).has(id);
+    let isIdUsed = gMenuMap.get(this.extension).has(id);
     if (isIdUsed) {
       throw new Error("Id already exists");
     }
     this._id = id;
   },
 
   get id() {
     return this._id;
   },
 
   ensureValidParentId(parentId) {
     if (parentId === undefined) {
       return;
     }
-    let menuMap = gContextMenuMap.get(this.extension);
+    let menuMap = gMenuMap.get(this.extension);
     if (!menuMap.has(parentId)) {
       throw new Error("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.");
       }
     }
@@ -445,17 +445,17 @@ MenuItem.prototype = {
 
     if (this.parent) {
       this.parent.detachChild(this);
     }
 
     if (parentId === undefined) {
       this.root.addChild(this);
     } else {
-      let menuMap = gContextMenuMap.get(this.extension);
+      let menuMap = gMenuMap.get(this.extension);
       menuMap.get(parentId).addChild(this);
     }
   },
 
   get parentId() {
     return this.parent ? this.parent.id : undefined;
   },
 
@@ -492,17 +492,17 @@ MenuItem.prototype = {
     if (this.parent) {
       this.parent.detachChild(this);
     }
     let children = this.children.slice(0);
     for (let child of children) {
       child.remove();
     }
 
-    let menuMap = gContextMenuMap.get(this.extension);
+    let menuMap = gMenuMap.get(this.extension);
     menuMap.delete(this.id);
     if (this.root == this) {
       gRootItems.delete(this.extension);
     }
   },
 
   getClickInfo(contextData, wasChecked) {
     let mediaType;
@@ -572,17 +572,17 @@ MenuItem.prototype = {
     }
 
     return true;
   },
 };
 
 // While any extensions are active, this Tracker registers to observe/listen
 // for contex-menu events from both content and chrome.
-const contextMenuTracker = {
+const menuTracker = {
   register() {
     Services.obs.addObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
       this.onWindowOpen(window);
     }
     windowTracker.addOpenListener(this.onWindowOpen);
   },
 
@@ -597,90 +597,90 @@ const contextMenuTracker = {
 
   observe(subject, topic, data) {
     subject = subject.wrappedJSObject;
     gMenuBuilder.build(subject);
   },
 
   onWindowOpen(window) {
     const menu = window.document.getElementById("tabContextMenu");
-    menu.addEventListener("popupshowing", contextMenuTracker);
+    menu.addEventListener("popupshowing", menuTracker);
   },
 
   handleEvent(event) {
     const menu = event.target;
     if (menu.id === "tabContextMenu") {
       const trigger = menu.triggerNode;
       const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
       const pageUrl = tab.linkedBrowser.currentURI.spec;
       gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
     }
   },
 };
 
 var gExtensionCount = 0;
 
-this.contextMenus = class extends ExtensionAPI {
+this.menusInternal = class extends ExtensionAPI {
   onShutdown(reason) {
     let {extension} = this;
 
-    if (gContextMenuMap.has(extension)) {
-      gContextMenuMap.delete(extension);
+    if (gMenuMap.has(extension)) {
+      gMenuMap.delete(extension);
       gRootItems.delete(extension);
       if (--gExtensionCount == 0) {
-        contextMenuTracker.unregister();
+        menuTracker.unregister();
       }
     }
   }
 
   getAPI(context) {
     let {extension} = context;
 
-    gContextMenuMap.set(extension, new Map());
+    gMenuMap.set(extension, new Map());
     if (++gExtensionCount == 1) {
-      contextMenuTracker.register();
+      menuTracker.register();
     }
 
     return {
-      contextMenus: {
-        createInternal: function(createProperties) {
+      menusInternal: {
+        create: function(createProperties) {
           // Note that the id is required by the schema. If the addon did not set
-          // it, the implementation of contextMenus.create in the child should
+          // it, the implementation of menus.create in the child should
           // have added it.
           let menuItem = new MenuItem(extension, createProperties);
-          gContextMenuMap.get(extension).set(menuItem.id, menuItem);
+          gMenuMap.get(extension).set(menuItem.id, menuItem);
         },
 
         update: function(id, updateProperties) {
-          let menuItem = gContextMenuMap.get(extension).get(id);
+          let menuItem = gMenuMap.get(extension).get(id);
           if (menuItem) {
             menuItem.setProps(updateProperties);
           }
         },
 
         remove: function(id) {
-          let menuItem = gContextMenuMap.get(extension).get(id);
+          let menuItem = gMenuMap.get(extension).get(id);
           if (menuItem) {
             menuItem.remove();
           }
         },
 
         removeAll: function() {
           let root = gRootItems.get(extension);
           if (root) {
             root.remove();
           }
         },
 
-        onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => {
+        onClicked: new SingletonEventManager(context, "menusInternal.onClicked", fire => {
           let listener = (event, info, tab) => {
             fire.async(info, tab);
           };
 
-          extension.on("webext-contextmenu-menuitem-click", listener);
+          extension.on("webext-menu-menuitem-click", listener);
           return () => {
-            extension.off("webext-contextmenu-menuitem-click", listener);
+            extension.off("webext-menu-menuitem-click", listener);
           };
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,6 +1,6 @@
 category webextension-scripts browser chrome://browser/content/ext-browser.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
 category webextension-scripts-devtools browser chrome://browser/content/ext-c-browser.js
 category webextension-scripts-addon browser chrome://browser/content/ext-c-browser.js
 
-category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
+category webextension-schemas menus_internal chrome://browser/content/schemas/menus_internal.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -13,30 +13,30 @@ browser.jar:
 #endif
     content/browser/extension.svg
     content/browser/ext-bookmarks.js
     content/browser/ext-browser.js
     content/browser/ext-browserAction.js
     content/browser/ext-browsingData.js
     content/browser/ext-chrome-settings-overrides.js
     content/browser/ext-commands.js
-    content/browser/ext-contextMenus.js
     content/browser/ext-devtools.js
     content/browser/ext-devtools-inspectedWindow.js
     content/browser/ext-devtools-network.js
     content/browser/ext-devtools-panels.js
     content/browser/ext-geckoProfiler.js
     content/browser/ext-history.js
+    content/browser/ext-menus.js
     content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-sessions.js
     content/browser/ext-sidebarAction.js
     content/browser/ext-tabs.js
     content/browser/ext-url-overrides.js
     content/browser/ext-utils.js
     content/browser/ext-windows.js
     content/browser/ext-c-browser.js
-    content/browser/ext-c-contextMenus.js
     content/browser/ext-c-devtools-inspectedWindow.js
     content/browser/ext-c-devtools-panels.js
     content/browser/ext-c-devtools.js
+    content/browser/ext-c-menus.js
     content/browser/ext-c-omnibox.js
     content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -3,23 +3,23 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 browser.jar:
     content/browser/schemas/bookmarks.json
     content/browser/schemas/browser_action.json
     content/browser/schemas/browsing_data.json
     content/browser/schemas/chrome_settings_overrides.json
     content/browser/schemas/commands.json
-    content/browser/schemas/context_menus.json
-    content/browser/schemas/context_menus_internal.json
     content/browser/schemas/devtools.json
     content/browser/schemas/devtools_inspected_window.json
     content/browser/schemas/devtools_network.json
     content/browser/schemas/devtools_panels.json
     content/browser/schemas/geckoProfiler.json
     content/browser/schemas/history.json
+    content/browser/schemas/menus.json
+    content/browser/schemas/menus_internal.json
     content/browser/schemas/omnibox.json
     content/browser/schemas/page_action.json
     content/browser/schemas/sessions.json
     content/browser/schemas/sidebar_action.json
     content/browser/schemas/tabs.json
     content/browser/schemas/url_overrides.json
     content/browser/schemas/windows.json
rename from browser/components/extensions/schemas/context_menus.json
rename to browser/components/extensions/schemas/menus.json
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -6,26 +6,33 @@
   {
     "namespace": "manifest",
     "types": [
       {
         "$extend": "Permission",
         "choices": [{
           "type": "string",
           "enum": [
+            "menus",
             "contextMenus"
           ]
         }]
       }
     ]
   },
   {
     "namespace": "contextMenus",
-    "description": "Use the <code>browser.contextMenus</code> API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
     "permissions": ["contextMenus"],
+    "description": "Use the browser.contextMenus API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
+    "$import": "menus"
+  },
+  {
+    "namespace": "menus",
+    "permissions": ["menus"],
+    "description": "Use the browser.menus API to add items to the browser's menus. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
     "properties": {
       "ACTION_MENU_TOP_LEVEL_LIMIT": {
         "value": 6,
         "description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
       }
     },
     "types": [
       {
@@ -259,17 +266,17 @@
                 "optional": true
               },
               "onclick": {
                 "type": "function",
                 "optional": "omit-key-if-missing",
                 "parameters": [
                   {
                     "name": "info",
-                    "$ref": "contextMenusInternal.OnClickData"
+                    "$ref": "menusInternal.OnClickData"
                   },
                   {
                     "name": "tab",
                     "$ref": "tabs.Tab",
                     "description": "The details of the tab where the click took place. Note: this parameter only present for extensions."
                   }
                 ]
               },
rename from browser/components/extensions/schemas/context_menus_internal.json
rename to browser/components/extensions/schemas/menus_internal.json
--- a/browser/components/extensions/schemas/context_menus_internal.json
+++ b/browser/components/extensions/schemas/menus_internal.json
@@ -1,15 +1,16 @@
 // Copyright 2014 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
 [
   {
-    "namespace": "contextMenusInternal",
+    "namespace": "menusInternal",
+    "allowedContexts": ["addon_parent_only"],
     "description": "Use the <code>browser.contextMenus</code> API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
     "types": [
       {
         "id": "OnClickData",
         "type": "object",
         "description": "Information sent when a context menu item is clicked.",
         "properties": {
           "menuItemId": {
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -47,17 +47,16 @@ skip-if = (os == 'win' && !debug) # bug 
 [browser_ext_commands_execute_browser_action.js]
 [browser_ext_commands_execute_page_action.js]
 [browser_ext_commands_execute_sidebar_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_contentscript_connect.js]
 [browser_ext_contextMenus.js]
 [browser_ext_contextMenus_checkboxes.js]
-[browser_ext_contextMenus_chrome.js]
 [browser_ext_contextMenus_commands.js]
 [browser_ext_contextMenus_icons.js]
 [browser_ext_contextMenus_onclick.js]
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_uninstall.js]
 [browser_ext_contextMenus_urlPatterns.js]
 [browser_ext_currentWindow.js]
 [browser_ext_devtools_inspectedWindow.js]
@@ -67,16 +66,17 @@ skip-if = (os == 'win' && !debug) # bug 
 [browser_ext_devtools_page.js]
 [browser_ext_devtools_panel.js]
 [browser_ext_geckoProfiler_symbolicate.js]
 [browser_ext_getViews.js]
 [browser_ext_identity_indication.js]
 [browser_ext_incognito_views.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
+[browser_ext_menus.js]
 [browser_ext_omnibox.js]
 skip-if = debug || asan # Bug 1354681
 [browser_ext_optionsPage_browser_style.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_contextMenu.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_pageAction_popup_resize.js]
rename from browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
rename to browser/components/extensions/test/browser/browser_ext_menus.js
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -1,32 +1,62 @@
 /* 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";
 
+add_task(async function test_permissions() {
+  function background() {
+    browser.test.sendMessage("apis", {
+      menus: typeof browser.menus,
+      contextMenus: typeof browser.contextMenus,
+      menusInternal: typeof browser.menusInternal,
+    });
+  }
+
+  const first = ExtensionTestUtils.loadExtension({manifest: {permissions: ["menus"]}, background});
+  const second = ExtensionTestUtils.loadExtension({manifest: {permissions: ["contextMenus"]}, background});
+
+  await first.startup();
+  await second.startup();
+
+  const apis1 = await first.awaitMessage("apis");
+  const apis2 = await second.awaitMessage("apis");
+
+  is(apis1.menus, "object", "browser.menus available with 'menus' permission");
+  is(apis1.contextMenus, "undefined", "browser.contextMenus unavailable with  'menus' permission");
+  is(apis1.menusInternal, "undefined", "browser.menusInternal is never available");
+
+  is(apis2.menus, "undefined", "browser.menus unavailable with 'contextMenus' permission");
+  is(apis2.contextMenus, "object", "browser.contextMenus unavailable with  'contextMenus' permission");
+  is(apis2.menusInternal, "undefined", "browser.menusInternal is never available");
+
+  await first.unload();
+  await second.unload();
+});
+
 add_task(async function test_actionContextMenus() {
   const manifest = {
     page_action: {},
     browser_action: {},
-    permissions: ["contextMenus"],
+    permissions: ["menus"],
   };
 
   async function background() {
     const contexts = ["page_action", "browser_action"];
 
-    const parentId = browser.contextMenus.create({contexts, title: "parent"});
-    await browser.contextMenus.create({parentId, title: "click A"});
-    await browser.contextMenus.create({parentId, title: "click B"});
+    const parentId = browser.menus.create({contexts, title: "parent"});
+    await browser.menus.create({parentId, title: "click A"});
+    await browser.menus.create({parentId, title: "click B"});
 
     for (let i = 1; i < 9; i++) {
-      await browser.contextMenus.create({contexts, id: `${i}`, title: `click ${i}`});
+      await browser.menus.create({contexts, id: `${i}`, title: `click ${i}`});
     }
 
-    browser.contextMenus.onClicked.addListener((info, tab) => {
+    browser.menus.onClicked.addListener((info, tab) => {
       browser.test.sendMessage("click", {info, tab});
     });
 
     const [tab] = await browser.tabs.query({active: true});
     await browser.pageAction.show(tab.id);
     browser.test.sendMessage("ready", tab.id);
   }
 
@@ -65,43 +95,43 @@ add_task(async function test_actionConte
 
   await BrowserTestUtils.removeTab(tab);
   await extension.unload();
 });
 
 add_task(async function test_tabContextMenu() {
   const first = ExtensionTestUtils.loadExtension({
     manifest: {
-      permissions: ["contextMenus"],
+      permissions: ["menus"],
     },
     async background() {
-      await browser.contextMenus.create({
+      await browser.menus.create({
         id: "alpha-beta-parent", title: "alpha-beta parent", contexts: ["tab"],
       });
 
-      await browser.contextMenus.create({parentId: "alpha-beta-parent", title: "alpha"});
-      await browser.contextMenus.create({parentId: "alpha-beta-parent", title: "beta"});
+      await browser.menus.create({parentId: "alpha-beta-parent", title: "alpha"});
+      await browser.menus.create({parentId: "alpha-beta-parent", title: "beta"});
 
-      await browser.contextMenus.create({title: "dummy", contexts: ["page"]});
+      await browser.menus.create({title: "dummy", contexts: ["page"]});
 
-      browser.contextMenus.onClicked.addListener((info, tab) => {
+      browser.menus.onClicked.addListener((info, tab) => {
         browser.test.sendMessage("click", {info, tab});
       });
 
       const [tab] = await browser.tabs.query({active: true});
       browser.test.sendMessage("ready", tab.id);
     },
   });
 
   const second = ExtensionTestUtils.loadExtension({
     manifest: {
-      permissions: ["contextMenus"],
+      permissions: ["menus"],
     },
     async background() {
-      await browser.contextMenus.create({title: "gamma", contexts: ["tab"]});
+      await browser.menus.create({title: "gamma", contexts: ["tab"]});
       browser.test.sendMessage("ready");
     },
   });
 
   const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
   await first.startup();
   await second.startup();
 
@@ -138,24 +168,24 @@ add_task(async function test_tabContextM
 
   await BrowserTestUtils.removeTab(tab);
   await first.unload();
   await second.unload();
 });
 
 add_task(async function test_onclick_frameid() {
   const manifest = {
-    permissions: ["contextMenus"],
+    permissions: ["menus"],
   };
 
   function background() {
     function onclick(info) {
       browser.test.sendMessage("click", info);
     }
-    browser.contextMenus.create({contexts: ["frame", "page"], title: "modify", onclick});
+    browser.menus.create({contexts: ["frame", "page"], title: "modify", onclick});
     browser.test.sendMessage("ready");
   }
 
   const extension = ExtensionTestUtils.loadExtension({manifest, background});
   const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser,
     "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html");
 
   await extension.startup();
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1072,16 +1072,17 @@ class SchemaAPIManager extends EventEmit
     if (!Schemas.checkPermissions(module.namespaceName, extension)) {
       return false;
     }
 
     return true;
   }
 
   _initModule(info, cls) {
+    // FIXME: This both a) does nothing, and b) is not used anymore.
     cls.namespaceName = cls.namespaceName;
     cls.scopes = new Set(info.scopes);
 
     return cls;
   }
 
   _checkLoadModule(module, name) {
     if (!module) {