Bug 1370499 - Support WebExtensions bookmark context menus. r=mixedpuppy draft
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 20 Nov 2017 23:01:02 +0000
changeset 700804 104e86ceec0994076b1c05d6de2000ac1bc77e8c
parent 700247 ef82504a27826468cf9885b22380acefac277bda
child 741011 62c62ac6d91d79ad7c59ecf54bce4391dc84d05e
push id89983
push userbmo:ntim.bugs@gmail.com
push dateMon, 20 Nov 2017 23:01:42 +0000
reviewersmixedpuppy
bugs1370499
milestone59.0a1
Bug 1370499 - Support WebExtensions bookmark context menus. r=mixedpuppy MozReview-Commit-ID: AkYxeGHlDvi
browser/components/extensions/ext-menus.js
browser/components/extensions/schemas/menus.json
browser/components/extensions/test/browser/browser_ext_contextMenus.js
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -231,17 +231,19 @@ var gMenuBuilder = {
           if (child.type == "radio" && child.groupName == item.groupName) {
             child.checked = false;
           }
         }
         // Select the clicked radio item.
         item.checked = true;
       }
 
-      item.tabManager.addActiveTabPermission();
+      if (!contextData.onBookmark) {
+        item.tabManager.addActiveTabPermission();
+      }
 
       let tab = contextData.tab && item.tabManager.convert(contextData.tab);
       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");
@@ -312,16 +314,17 @@ const contextsMap = {
   onEditableArea: "editable",
   inFrame: "frame",
   onImage: "image",
   onLink: "link",
   onPassword: "password",
   isTextSelected: "selection",
   onVideo: "video",
 
+  onBookmark: "bookmark",
   onBrowserAction: "browser_action",
   onPageAction: "page_action",
   onTab: "tab",
   inToolsMenu: "tools_menu",
 };
 
 const getMenuContexts = contextData => {
   let contexts = new Set();
@@ -332,17 +335,17 @@ const getMenuContexts = contextData => {
     }
   }
 
   if (contexts.size === 0) {
     contexts.add("page");
   }
 
   // New non-content contexts supported in Firefox are not part of "all".
-  if (!contextData.onTab && !contextData.inToolsMenu) {
+  if (!contextData.onBookmark && !contextData.onTab && !contextData.inToolsMenu) {
     contexts.add("all");
   }
 
   return contexts;
 };
 
 function MenuItem(extension, createProperties, isRoot = false) {
   this.extension = extension;
@@ -518,31 +521,36 @@ MenuItem.prototype = {
     setIfDefined("mediaType", mediaType);
     setIfDefined("linkText", contextData.linkText);
     setIfDefined("linkUrl", contextData.linkUrl);
     setIfDefined("srcUrl", contextData.srcUrl);
     setIfDefined("pageUrl", contextData.pageUrl);
     setIfDefined("frameUrl", contextData.frameUrl);
     setIfDefined("frameId", contextData.frameId);
     setIfDefined("selectionText", contextData.selectionText);
+    setIfDefined("bookmarkId", contextData.bookmarkId);
 
     if ((this.type === "checkbox") || (this.type === "radio")) {
       info.checked = this.checked;
       info.wasChecked = wasChecked;
     }
 
     return info;
   },
 
   enabledForContext(contextData) {
     let contexts = getMenuContexts(contextData);
     if (!this.contexts.some(n => contexts.has(n))) {
       return false;
     }
 
+    if (contextData.onBookmark) {
+      return this.extension.hasPermission("bookmarks");
+    }
+
     let docPattern = this.documentUrlMatchPattern;
     let pageURI = Services.io.newURI(contextData[contextData.inFrame ? "frameUrl" : "pageUrl"]);
     if (docPattern && !docPattern.matches(pageURI)) {
       return false;
     }
 
     let targetPattern = this.targetUrlMatchPattern;
     if (targetPattern) {
@@ -561,17 +569,17 @@ MenuItem.prototype = {
 
     return true;
   },
 };
 
 // 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: ["menu_ToolsPopup", "tabContextMenu"],
+  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);
   },
@@ -596,16 +604,28 @@ const menuTracker = {
     for (const id of menuTracker.menuIds) {
       const menu = window.document.getElementById(id);
       menu.addEventListener("popupshowing", menuTracker);
     }
   },
 
   handleEvent(event) {
     const menu = event.target;
+    if (menu.id === "placesContext") {
+      const trigger = menu.triggerNode;
+      if (!trigger._placesNode) {
+        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 trigger = menu.triggerNode;
       const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
--- a/browser/components/extensions/schemas/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -22,17 +22,17 @@
     "namespace": "contextMenus",
     "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",
     "types": [
       {
         "id": "ContextType",
         "type": "string",
-        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab"],
+        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "bookmark", "browser_action", "page_action", "tab"],
         "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
       }
     ]
   },
   {
     "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.",
@@ -41,17 +41,17 @@
         "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": [
       {
         "id": "ContextType",
         "type": "string",
-        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab", "tools_menu"],
+        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "bookmark", "browser_action", "page_action", "tab", "tools_menu"],
         "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
       },
       {
         "id": "ItemType",
         "type": "string",
         "enum": ["normal", "checkbox", "radio", "separator"],
         "description": "The type of menu item."
       },
@@ -119,16 +119,20 @@
             "optional": true,
             "description": "A flag indicating the state of a checkbox or radio item before it was clicked."
           },
           "checked": {
             "type": "boolean",
             "optional": true,
             "description": "A flag indicating the state of a checkbox or radio item after it is clicked."
           },
+          "bookmarkId": {
+            "type": "string",
+            "description": "The id of the bookmark where the context menu was clicked, if it was on a bookmark."
+          },
           "modifiers": {
             "type": "array",
             "items": {
               "type": "string",
               "enum": ["Shift", "Alt", "Command", "Ctrl", "MacCtrl"]
             },
             "description": "An array of keyboard modifiers that were held while the menu item was clicked."
           }
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -445,8 +445,84 @@ add_task(async function testRemoveAllWit
   // Confirm only gamma is left.
   await confirmMenuItems("gamma");
   await closeContextMenu();
 
   await first.unload();
   await second.unload();
   await BrowserTestUtils.removeTab(tab);
 });
+
+add_task(async function test_bookmark_contextmenu() {
+  const bookmarksToolbar = document.getElementById("PersonalToolbar");
+  setToolbarVisibility(bookmarksToolbar, true);
+
+  const extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["contextMenus", "bookmarks"],
+    },
+    async background() {
+      const url = "https://example.com/";
+      const title = "Example";
+      let newBookmark = await browser.bookmarks.create({
+        url,
+        title,
+        parentId: "toolbar_____",
+      });
+      await browser.contextMenus.create({
+        title: "Get bookmark",
+        contexts: ["bookmark"],
+      });
+      browser.test.sendMessage("bookmark-created");
+      browser.contextMenus.onClicked.addListener(async (info) => {
+        browser.test.assertEq(newBookmark.id, info.bookmarkId, "Bookmark ID matches");
+
+        let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
+        browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
+        browser.test.assertEq(url, bookmark.url, "Bookmark url matches");
+        browser.test.assertFalse(info.hasOwnProperty("pageUrl"), "Context menu does not expose pageUrl");
+        await browser.bookmarks.remove(info.bookmarkId);
+        browser.test.sendMessage("test-finish");
+      });
+    },
+  });
+  await extension.startup();
+  await extension.awaitMessage("bookmark-created");
+  let menu = await openChromeContextMenu("placesContext",
+    "#PersonalToolbar .bookmark-item:last-child");
+
+  let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+  closeChromeContextMenu("placesContext", menuItem);
+
+  await extension.awaitMessage("test-finish");
+  await extension.unload();
+  setToolbarVisibility(bookmarksToolbar, false);
+});
+
+add_task(async function test_bookmark_context_requires_permission() {
+  const bookmarksToolbar = document.getElementById("PersonalToolbar");
+  setToolbarVisibility(bookmarksToolbar, true);
+
+  const extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["contextMenus"],
+    },
+    async background() {
+      await browser.contextMenus.create({
+        title: "Get bookmark",
+        contexts: ["bookmark"],
+      });
+      browser.test.sendMessage("bookmark-created");
+    },
+  });
+  await extension.startup();
+  await extension.awaitMessage("bookmark-created");
+  let menu = await openChromeContextMenu("placesContext",
+    "#PersonalToolbar .bookmark-item:last-child");
+
+  Assert.equal(menu.getElementsByAttribute("label", "Get bookmark").length, 0,
+    "bookmark context menu not created with `bookmarks` permission.");
+
+  closeChromeContextMenu("placesContext");
+
+  await extension.unload();
+  setToolbarVisibility(bookmarksToolbar, false);
+});