tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 9cbe62581b7f907b2e597173e12dd75b10fce17c
parent 7dde23bd00d6e08168eae48aefd9970f89944330
Author: Marco Bonardo <mbonardo@mozilla.com>
Date:   Thu, 23 Oct 2025 12:25:54 +0000

Bug 1995696 - Don't populate Places menus if they create a recursive structure r=places-reviewers,Standard8

Differential Revision: https://phabricator.services.mozilla.com/D269599

Diffstat:
Mbrowser/components/places/content/browserPlacesViews.js | 36++++++++++++++++++++++++++++++++++++
Mbrowser/components/places/tests/browser/browser.toml | 2++
Abrowser/components/places/tests/browser/browser_recursive_hierarchies.js | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 198 insertions(+), 0 deletions(-)

diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js @@ -795,6 +795,15 @@ class PlacesViewBase { } if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) { + if (this.#isPopupForRecursiveFolderShortcut(popup)) { + // Show as an empty container for now. We may want to show a better + // message in the future, but since we are likely to remove recursive + // shortcuts in maintenance at a certain point, this should be enough. + this._setEmptyPopupStatus(popup, true); + popup._built = true; + return; + } + if (!popup._placesNode.containerOpen) { popup._placesNode.containerOpen = true; } @@ -817,6 +826,33 @@ class PlacesViewBase { aObject.removeEventListener(aEventNames[i], this, aCapturing); } } + + /** + * Walks up the parent chain to detect whether a folder shortcut resolves to + * a folder already present in the ancestry. + * + * @param {DOMElement} popup + * @returns {boolean} Whether this popup is for a recursive folder shortcut. + */ + #isPopupForRecursiveFolderShortcut(popup) { + if ( + !popup._placesNode || + !PlacesUtils.nodeIsFolderOrShortcut(popup._placesNode) + ) { + return false; + } + let guid = PlacesUtils.getConcreteItemGuid(popup._placesNode); + for ( + let parentView = popup.parentNode?.parentNode; + parentView?._placesNode; + parentView = parentView.parentNode?.parentNode + ) { + if (PlacesUtils.getConcreteItemGuid(parentView._placesNode) == guid) { + return true; + } + } + return false; + } } /** diff --git a/browser/components/places/tests/browser/browser.toml b/browser/components/places/tests/browser/browser.toml @@ -211,6 +211,8 @@ https_first_disabled = true ["browser_paste_resets_cut_highlights.js"] +["browser_recursive_hierarchies.js"] + ["browser_remove_bookmarks.js"] ["browser_sidebar_bookmarks_telemetry.js"] diff --git a/browser/components/places/tests/browser/browser_recursive_hierarchies.js b/browser/components/places/tests/browser/browser_recursive_hierarchies.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that menu popups are not populated when doing so would create a + * recursive hierarchy. Some consumers may walk menu trees and could enter an + * infinite loop if recursion were allowed. + */ + +"use strict"; + +async function openMenuAndWaitForPopup(menu) { + let popup = menu.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + // This seems necessary, at least for the toolbar. + EventUtils.synthesizeNativeMouseEvent({ + type: "mousemove", + target: menu, + atCenter: true, + }); + menu.open = true; + await popupShown; + return popup; +} + +function findNode(guid, view) { + for (let node of view.childNodes) { + console.log("visiting node", node, node._placesNode?.bookmarkGuid); + if (node._placesNode?.bookmarkGuid == guid) { + return node; + } + } + return null; +} + +function fakeOpenMenu(popup) { + popup.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true })); + popup.dispatchEvent(new MouseEvent("popupshown", { bubbles: true })); +} + +function findPlacesNode(guid, container) { + for (let i = 0; i < container.childCount; i++) { + let node = container.getChild(i); + if (node.bookmarkGuid == guid) { + return node; + } + } + return null; +} + +add_task(async function test() { + // Make sure the bookmarks bar is visible and restore its state on cleanup. + let toolbar = document.getElementById("PersonalToolbar"); + ok(toolbar, "PersonalToolbar should not be null"); + if (toolbar.collapsed) { + await promiseSetToolbarVisibility(toolbar, true); + registerCleanupFunction(function () { + return promiseSetToolbarVisibility(toolbar, false); + }); + } + + let menubar = document.getElementById("toolbar-menubar"); + // Force the menu to be shown. + const kAutohide = menubar.getAttribute("autohide"); + menubar.removeAttribute("autohide"); + registerCleanupFunction(function () { + menubar.setAttribute("autohide", kAutohide); + }); + + registerCleanupFunction(PlacesUtils.bookmarks.eraseEverything); + + // Create a folder structure with a recursive shortcut. + let folderA = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Folder A", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let selfShortcut = await PlacesUtils.bookmarks.insert({ + parentGuid: folderA.guid, + title: "Shortcut to A", + url: `place:parent=${folderA.guid}`, + }); + let toolbarShortcut = await PlacesUtils.bookmarks.insert({ + parentGuid: folderA.guid, + title: "Shortcut to toolbar", + url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`, + }); + + // This also ensures that toolbar content is updated. + Assert.ok(!(await PlacesToolbarHelper.getIsEmpty()), "Toolbar is not empty"); + + // Open the popup for the shortcut from the bookmarks toolbar. + let toolbarItems = document.getElementById("PlacesToolbarItems"); + let folderANode = findNode(folderA.guid, toolbarItems); + let folderAPopup = await openMenuAndWaitForPopup(folderANode); + let selfShortcutNode = findNode(selfShortcut.guid, folderAPopup); + let selfShortcutPopup = await openMenuAndWaitForPopup(selfShortcutNode); + + Assert.ok( + !selfShortcutPopup._placesNode.containerOpen, + "Self shortcut container is not open" + ); + Assert.ok( + selfShortcutPopup.hasAttribute("emptyplacesresult"), + "Self shortcut popup is empty" + ); + + let toolbarShortcutNode = findNode( + toolbarShortcut.guid, + folderANode.menupopup + ); + let toolbarShortcutPopup = await openMenuAndWaitForPopup(toolbarShortcutNode); + + Assert.ok( + !toolbarShortcutPopup._placesNode.containerOpen, + "Toolbar shortcut container is not open" + ); + Assert.ok( + toolbarShortcutPopup.hasAttribute("emptyplacesresult"), + "Toolbar shortcut popup is empty" + ); + + // Also insert a toolbar shortcut in the bookmarks menu and check the + // previously inserted recursive toolbar shortcut there. + info("Test native bookmarks menu"); + let shortcutInMenu = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "Shortcut to menu", + url: `place:parent=${PlacesUtils.bookmarks.menuGuid}`, + }); + + if (Services.appinfo.nativeMenubar) { + // With native menubar we can only simulate popupshowing as we cannot + // directly open menus. + let bookmarksMenuPopup = document.getElementById("bookmarksMenuPopup"); + fakeOpenMenu(bookmarksMenuPopup); + let shortcutInMenuNode = findNode(shortcutInMenu.guid, bookmarksMenuPopup); + fakeOpenMenu(shortcutInMenuNode.menupopup); + Assert.ok( + !shortcutInMenuNode.menupopup._placesNode.containerOpen, + "menu shortcut container is not open" + ); + } else { + let bookmarksMenu = document.getElementById("bookmarksMenu"); + let bookmarksPopup = await openMenuAndWaitForPopup(bookmarksMenu); + let shortcutInMenuNode = findNode(shortcutInMenu.guid, bookmarksPopup); + let shortcutInMenuPopup = await openMenuAndWaitForPopup( + shortcutInMenuNode, + true + ); + Assert.ok( + !shortcutInMenuPopup._placesNode.containerOpen, + "Toolbar shortcut container is not open" + ); + Assert.ok( + shortcutInMenuPopup.hasAttribute("emptyplacesresult"), + "Toolbar shortcut popup is empty" + ); + } +});