Bug 1370499 - Support WebExtensions bookmark context menus. r=mixedpuppy
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 20 Nov 2017 23:01:02 +0000
changeset 392804 09c62da7c2a5098be3ca9fd43ab6c90370bd5930
parent 392803 76ff493e279fc1cb4706117971bbfddb0a0712b1
child 392805 a41c4aadd9145da461b0c60637c2dd20777118d9
push id97527
push usertoros@mozilla.com
push dateTue, 21 Nov 2017 10:20:05 +0000
treeherdermozilla-inbound@60d0f3ee0c43 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1370499
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);
+});