diff --git a/browser/components/extensions/parent/ext-menus.js b/browser/components/extensions/parent/ext-menus.js index b099de7d6598..219233f4c475 100644 --- a/browser/components/extensions/parent/ext-menus.js +++ b/browser/components/extensions/parent/ext-menus.js @@ -4,6 +4,9 @@ ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.defineModuleGetter(this, "Bookmarks", + "resource://gre/modules/Bookmarks.jsm"); + var { DefaultMap, ExtensionError, @@ -863,6 +866,72 @@ MenuItem.prototype = { }, }; +// 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(this.libraryWindowType)) { + try { + window.removeEventListener("load", this); + 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 = { @@ -874,6 +943,7 @@ const menuTracker = { this.onWindowOpen(window); } windowTracker.addOpenListener(this.onWindowOpen); + libraryTracker.init(this.onLibraryOpen); }, unregister() { @@ -882,6 +952,7 @@ const menuTracker = { this.cleanupWindow(window); } windowTracker.removeOpenListener(this.onWindowOpen); + libraryTracker.uninit(this.cleanupLibrary); }, observe(subject, topic, data) { @@ -929,6 +1000,16 @@ const menuTracker = { } }, + 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; @@ -963,6 +1044,10 @@ const menuTracker = { const cell = tree.boxObject.getCellAt(event.x, event.y); const node = tree.view.nodeForTreeIndex(cell.row); + if (!node.bookmarkGuid || Bookmarks.isVirtualRootItem(node.bookmarkGuid)) { + return; + } + gMenuBuilder.build({ menu, bookmarkId: node.bookmarkGuid, diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus.js b/browser/components/extensions/test/browser/browser_ext_contextMenus.js index 006b70c2f404..b18c023adf77 100644 --- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js @@ -5,7 +5,9 @@ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/browser/components/places/tests/browser/head.js", this); -/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell */ +/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell, + * promiseLibrary, promiseLibraryClosed + */ const PAGE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"; @@ -606,6 +608,75 @@ add_task(async function test_bookmark_sidebar_contextmenu() { }); }); +function bookmarkFolderContextMenuExtension() { + return ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["contextMenus", "bookmarks"], + }, + async background() { + const title = "Example"; + let newBookmark = await browser.bookmarks.create({ + title, + parentId: "toolbar_____", + }); + await new Promise(resolve => + browser.contextMenus.create({ + title: "Get bookmark", + contexts: ["bookmark"], + }, resolve)); + 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.assertFalse(info.hasOwnProperty("pageUrl"), "Context menu does not expose pageUrl"); + await browser.bookmarks.remove(info.bookmarkId); + browser.test.sendMessage("test-finish"); + }); + browser.test.sendMessage("bookmark-created", newBookmark.id); + }, + }); +} + +add_task(async function test_organizer_contextmenu() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + let library = await promiseLibrary("BookmarksToolbar"); + + let menu = library.document.getElementById("placesContext"); + let mainTree = library.document.getElementById("placeContent"); + let leftTree = library.document.getElementById("placesList"); + + let tests = [ + [mainTree, bookmarkContextMenuExtension], + [mainTree, bookmarkFolderContextMenuExtension], + [leftTree, bookmarkFolderContextMenuExtension], + ]; + + if (AppConstants.DEBUG) { + // Avoid intermittent leak - bug 1520047 + tests.pop(); + } + + for (let [tree, makeExtension] of tests) { + let extension = makeExtension(); + await extension.startup(); + let bookmarkGuid = await extension.awaitMessage("bookmark-created"); + + tree.selectItems([bookmarkGuid]); + let shown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + synthesizeClickOnSelectedTreeCell(tree, {type: "contextmenu"}); + await shown; + + let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0]; + closeChromeContextMenu("placesContext", menuItem, library); + await extension.awaitMessage("test-finish"); + await extension.unload(); + } + + await promiseLibraryClosed(library); + BrowserTestUtils.removeTab(tab); +}); + add_task(async function test_bookmark_context_requires_permission() { const bookmarksToolbar = document.getElementById("PersonalToolbar"); setToolbarVisibility(bookmarksToolbar, true);