summaryrefslogtreecommitdiffstats
path: root/browser/components/places/PlacesUIUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/places/PlacesUIUtils.jsm1849
1 files changed, 1849 insertions, 0 deletions
diff --git a/browser/components/places/PlacesUIUtils.jsm b/browser/components/places/PlacesUIUtils.jsm
new file mode 100644
index 0000000000..83b63d5d7b
--- /dev/null
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -0,0 +1,1849 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["PlacesUIUtils"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { clearTimeout, setTimeout } = ChromeUtils.import(
+ "resource://gre/modules/Timer.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["Element"]);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ CustomizableUI: "resource:///modules/CustomizableUI.jsm",
+ MigrationUtils: "resource:///modules/MigrationUtils.jsm",
+ OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ PluralForm: "resource://gre/modules/PluralForm.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ Weave: "resource://services-sync/main.js",
+});
+
+XPCOMUtils.defineLazyGetter(this, "bundle", function() {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/places/places.properties"
+ );
+});
+
+const gInContentProcess =
+ Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+const FAVICON_REQUEST_TIMEOUT = 60 * 1000;
+// Map from windows to arrays of data about pending favicon loads.
+let gFaviconLoadDataMap = new Map();
+
+const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10;
+
+// copied from utilityOverlay.js
+const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+const PREF_LOAD_BOOKMARKS_IN_BACKGROUND =
+ "browser.tabs.loadBookmarksInBackground";
+const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs";
+
+let InternalFaviconLoader = {
+ /**
+ * Actually cancel the request, and clear the timeout for cancelling it.
+ */
+ _cancelRequest({ uri, innerWindowID, timerID, callback }, reason) {
+ // Break cycle
+ let request = callback.request;
+ delete callback.request;
+ // Ensure we don't time out.
+ clearTimeout(timerID);
+ try {
+ request.cancel();
+ } catch (ex) {
+ Cu.reportError(
+ "When cancelling a request for " +
+ uri.spec +
+ " because " +
+ reason +
+ ", it was already canceled!"
+ );
+ }
+ },
+
+ /**
+ * Called for every inner that gets destroyed, only in the parent process.
+ */
+ removeRequestsForInner(innerID) {
+ for (let [window, loadDataForWindow] of gFaviconLoadDataMap) {
+ let newLoadDataForWindow = loadDataForWindow.filter(loadData => {
+ let innerWasDestroyed = loadData.innerWindowID == innerID;
+ if (innerWasDestroyed) {
+ this._cancelRequest(
+ loadData,
+ "the inner window was destroyed or a new favicon was loaded for it"
+ );
+ }
+ // Keep the items whose inner is still alive.
+ return !innerWasDestroyed;
+ });
+ // Map iteration with for...of is safe against modification, so
+ // now just replace the old value:
+ gFaviconLoadDataMap.set(window, newLoadDataForWindow);
+ }
+ },
+
+ /**
+ * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves,
+ * avoid leaks, and cancel any remaining requests. The last part should in theory be
+ * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side.
+ */
+ onUnload(win) {
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ if (loadDataForWindow) {
+ for (let loadData of loadDataForWindow) {
+ this._cancelRequest(loadData, "the chrome window went away");
+ }
+ }
+ gFaviconLoadDataMap.delete(win);
+ },
+
+ /**
+ * Remove a particular favicon load's loading data from our map tracking
+ * load data per chrome window.
+ *
+ * @param win
+ * the chrome window in which we should look for this load
+ * @param filterData ({innerWindowID, uri, callback})
+ * the data we should use to find this particular load to remove.
+ *
+ * @return the loadData object we removed, or null if we didn't find any.
+ */
+ _removeLoadDataFromWindowMap(win, { innerWindowID, uri, callback }) {
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ if (loadDataForWindow) {
+ let itemIndex = loadDataForWindow.findIndex(loadData => {
+ return (
+ loadData.innerWindowID == innerWindowID &&
+ loadData.uri.equals(uri) &&
+ loadData.callback.request == callback.request
+ );
+ });
+ if (itemIndex != -1) {
+ let loadData = loadDataForWindow[itemIndex];
+ loadDataForWindow.splice(itemIndex, 1);
+ return loadData;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling
+ * information when the request succeeds. Note that right now there are some edge-cases,
+ * such as about: URIs with chrome:// favicons where the success callback is not invoked.
+ * This is OK: we will 'cancel' the request after the timeout (or when the window goes
+ * away) but that will be a no-op in such cases.
+ */
+ _makeCompletionCallback(win, id) {
+ return {
+ onComplete(uri) {
+ let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, {
+ uri,
+ innerWindowID: id,
+ callback: this,
+ });
+ if (loadData) {
+ clearTimeout(loadData.timerID);
+ }
+ delete this.request;
+ },
+ };
+ },
+
+ ensureInitialized() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ Services.obs.addObserver(windowGlobal => {
+ this.removeRequestsForInner(windowGlobal.innerWindowId);
+ }, "window-global-destroyed");
+ },
+
+ loadFavicon(browser, principal, pageURI, uri, expiration, iconURI) {
+ this.ensureInitialized();
+ let { ownerGlobal: win, innerWindowID } = browser;
+ if (!gFaviconLoadDataMap.has(win)) {
+ gFaviconLoadDataMap.set(win, []);
+ let unloadHandler = event => {
+ let doc = event.target;
+ let eventWin = doc.defaultView;
+ if (eventWin == win) {
+ win.removeEventListener("unload", unloadHandler);
+ this.onUnload(win);
+ }
+ };
+ win.addEventListener("unload", unloadHandler, true);
+ }
+
+ // First we do the actual setAndFetch call:
+ let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
+ ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
+ : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
+ let callback = this._makeCompletionCallback(win, innerWindowID);
+
+ if (iconURI && iconURI.schemeIs("data")) {
+ expiration = PlacesUtils.toPRTime(expiration);
+ PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+ uri,
+ iconURI.spec,
+ expiration,
+ principal
+ );
+ }
+
+ let request = PlacesUtils.favicons.setAndFetchFaviconForPage(
+ pageURI,
+ uri,
+ false,
+ loadType,
+ callback,
+ principal
+ );
+
+ // Now register the result so we can cancel it if/when necessary.
+ if (!request) {
+ // The favicon service can return with success but no-op (and leave request
+ // as null) if the icon is the same as the page (e.g. for images) or if it is
+ // the favicon for an error page. In this case, we do not need to do anything else.
+ return;
+ }
+ callback.request = request;
+ let loadData = { innerWindowID, uri, callback };
+ loadData.timerID = setTimeout(() => {
+ this._cancelRequest(loadData, "it timed out");
+ this._removeLoadDataFromWindowMap(win, loadData);
+ }, FAVICON_REQUEST_TIMEOUT);
+ let loadDataForWindow = gFaviconLoadDataMap.get(win);
+ loadDataForWindow.push(loadData);
+ },
+};
+
+var PlacesUIUtils = {
+ _bookmarkToolbarTelemetryListening: false,
+ LAST_USED_FOLDERS_META_KEY: "bookmarks/lastusedfolders",
+
+ getFormattedString: function PUIU_getFormattedString(key, params) {
+ return bundle.formatStringFromName(key, params);
+ },
+
+ /**
+ * Get a localized plural string for the specified key name and numeric value
+ * substituting parameters.
+ *
+ * @param aKey
+ * String, key for looking up the localized string in the bundle
+ * @param aNumber
+ * Number based on which the final localized form is looked up
+ * @param aParams
+ * Array whose items will substitute #1, #2,... #n parameters
+ * in the string.
+ *
+ * @see https://developer.mozilla.org/en/Localization_and_Plurals
+ * @return The localized plural string.
+ */
+ getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) {
+ let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey));
+
+ // Replace #1 with aParams[0], #2 with aParams[1], and so on.
+ return str.replace(/\#(\d+)/g, function(matchedId, matchedNumber) {
+ let param = aParams[parseInt(matchedNumber, 10) - 1];
+ return param !== undefined ? param : matchedId;
+ });
+ },
+
+ getString: function PUIU_getString(key) {
+ return bundle.GetStringFromName(key);
+ },
+
+ /**
+ * Shows the bookmark dialog corresponding to the specified info.
+ *
+ * @param {object} aInfo
+ * Describes the item to be edited/added in the dialog.
+ * See documentation at the top of bookmarkProperties.js
+ * @param {DOMWindow} [aParentWindow]
+ * Owner window for the new dialog.
+ *
+ * @see documentation at the top of bookmarkProperties.js
+ * @return The guid of the item that was created or edited, undefined otherwise.
+ */
+ showBookmarkDialog(aInfo, aParentWindow = null) {
+ // Preserve size attributes differently based on the fact the dialog has
+ // a folder picker or not, since it needs more horizontal space than the
+ // other controls.
+ let hasFolderPicker =
+ !("hiddenRows" in aInfo) || !aInfo.hiddenRows.includes("folderPicker");
+ // Use a different chrome url to persist different sizes.
+ let dialogURL = hasFolderPicker
+ ? "chrome://browser/content/places/bookmarkProperties2.xhtml"
+ : "chrome://browser/content/places/bookmarkProperties.xhtml";
+
+ let features = "centerscreen,chrome,modal,resizable=yes";
+
+ let topUndoEntry;
+ let batchBlockingDeferred;
+
+ // Set the transaction manager into batching mode.
+ topUndoEntry = PlacesTransactions.topUndoEntry;
+ batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(async () => {
+ await batchBlockingDeferred.promise;
+ });
+
+ if (!aParentWindow) {
+ aParentWindow = Services.wm.getMostRecentWindow(null);
+ }
+
+ aParentWindow.openDialog(dialogURL, "", features, aInfo);
+
+ let bookmarkGuid =
+ ("bookmarkGuid" in aInfo && aInfo.bookmarkGuid) || undefined;
+
+ batchBlockingDeferred.resolve();
+
+ if (!bookmarkGuid && topUndoEntry != PlacesTransactions.topUndoEntry) {
+ PlacesTransactions.undo().catch(Cu.reportError);
+ }
+
+ return bookmarkGuid;
+ },
+
+ /**
+ * Bookmarks one or more pages. If there is more than one, this will create
+ * the bookmarks in a new folder.
+ *
+ * @param {array.<nsIURI>} URIList
+ * The list of URIs to bookmark.
+ * @param {array.<string>} [hiddenRows]
+ * An array of rows to be hidden.
+ * @param {DOMWindow} [window]
+ * The window to use as the parent to display the bookmark dialog.
+ */
+ showBookmarkPagesDialog(URIList, hiddenRows = [], win = null) {
+ if (!URIList.length) {
+ return;
+ }
+
+ const bookmarkDialogInfo = { action: "add", hiddenRows };
+ if (URIList.length > 1) {
+ bookmarkDialogInfo.type = "folder";
+ bookmarkDialogInfo.URIList = URIList;
+ } else {
+ bookmarkDialogInfo.type = "bookmark";
+ bookmarkDialogInfo.title = URIList[0].title;
+ bookmarkDialogInfo.uri = URIList[0].uri;
+ }
+
+ PlacesUIUtils.showBookmarkDialog(bookmarkDialogInfo, win);
+ },
+
+ /**
+ * set and fetch a favicon. Can only be used from the parent process.
+ * @param browser {Browser} The XUL browser element for which we're fetching a favicon.
+ * @param principal {Principal} The loading principal to use for the fetch.
+ * @pram pageURI {URI} The page URI associated to this favicon load.
+ * @param uri {URI} The URI to fetch.
+ * @param expiration {Number} An optional expiration time.
+ * @param iconURI {URI} An optional data: URI holding the icon's data.
+ */
+ loadFavicon(
+ browser,
+ principal,
+ pageURI,
+ uri,
+ expiration = 0,
+ iconURI = null
+ ) {
+ if (gInContentProcess) {
+ throw new Error("Can't track loads from within the child process!");
+ }
+ InternalFaviconLoader.loadFavicon(
+ browser,
+ principal,
+ pageURI,
+ uri,
+ expiration,
+ iconURI
+ );
+ },
+
+ /**
+ * Returns the closet ancestor places view for the given DOM node
+ * @param aNode
+ * a DOM node
+ * @return the closet ancestor places view if exists, null otherwsie.
+ */
+ getViewForNode: function PUIU_getViewForNode(aNode) {
+ let node = aNode;
+
+ if (Cu.isDeadWrapper(node)) {
+ return null;
+ }
+
+ if (node.localName == "panelview" && node._placesView) {
+ return node._placesView;
+ }
+
+ // The view for a <menu> of which its associated menupopup is a places
+ // view, is the menupopup.
+ if (
+ node.localName == "menu" &&
+ !node._placesNode &&
+ node.menupopup._placesView
+ ) {
+ return node.menupopup._placesView;
+ }
+
+ while (Element.isInstance(node)) {
+ if (node._placesView) {
+ return node._placesView;
+ }
+ if (
+ node.localName == "tree" &&
+ node.getAttribute("is") == "places-tree"
+ ) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns the active PlacesController for a given command.
+ *
+ * @param win The window containing the affected view
+ * @param command The command
+ * @return a PlacesController
+ */
+ getControllerForCommand(win, command) {
+ // A context menu may be built for non-focusable views. Thus, we first try
+ // to look for a view associated with document.popupNode
+ let popupNode;
+ try {
+ popupNode = win.document.popupNode;
+ } catch (e) {
+ // The document went away (bug 797307).
+ return null;
+ }
+ if (popupNode) {
+ let isManaged = !!popupNode.closest("#managed-bookmarks");
+ if (isManaged) {
+ return this.managedBookmarksController;
+ }
+ let view = this.getViewForNode(popupNode);
+ if (view && view._contextMenuShown) {
+ return view.controllers.getControllerForCommand(command);
+ }
+ }
+
+ // When we're not building a context menu, only focusable views
+ // are possible. Thus, we can safely use the command dispatcher.
+ let controller = win.top.document.commandDispatcher.getControllerForCommand(
+ command
+ );
+ return controller || null;
+ },
+
+ /**
+ * Update all the Places commands for the given window.
+ *
+ * @param win The window to update.
+ */
+ updateCommands(win) {
+ // Get the controller for one of the places commands.
+ let controller = this.getControllerForCommand(win, "placesCmd_open");
+ for (let command of [
+ "placesCmd_open",
+ "placesCmd_open:window",
+ "placesCmd_open:privatewindow",
+ "placesCmd_open:tab",
+ "placesCmd_new:folder",
+ "placesCmd_new:bookmark",
+ "placesCmd_new:separator",
+ "placesCmd_show:info",
+ "placesCmd_reload",
+ "placesCmd_sortBy:name",
+ "placesCmd_cut",
+ "placesCmd_copy",
+ "placesCmd_paste",
+ "placesCmd_delete",
+ ]) {
+ win.goSetCommandEnabled(
+ command,
+ controller && controller.isCommandEnabled(command)
+ );
+ }
+ },
+
+ /**
+ * Executes the given command on the currently active controller.
+ *
+ * @param win The window containing the affected view
+ * @param command The command to execute
+ */
+ doCommand(win, command) {
+ let controller = this.getControllerForCommand(win, command);
+ if (controller && controller.isCommandEnabled(command)) {
+ controller.doCommand(command);
+ }
+ },
+
+ /**
+ * By calling this before visiting an URL, the visit will be associated to a
+ * TRANSITION_TYPED transition (if there is no a referrer).
+ * This is used when visiting pages from the history menu, history sidebar,
+ * url bar, url autocomplete results, and history searches from the places
+ * organizer. If this is not called visits will be marked as
+ * TRANSITION_LINK.
+ */
+ markPageAsTyped: function PUIU_markPageAsTyped(aURL) {
+ PlacesUtils.history.markPageAsTyped(
+ Services.uriFixup.getFixupURIInfo(aURL).preferredURI
+ );
+ },
+
+ /**
+ * By calling this before visiting an URL, the visit will be associated to a
+ * TRANSITION_BOOKMARK transition.
+ * This is used when visiting pages from the bookmarks menu,
+ * personal toolbar, and bookmarks from within the places organizer.
+ * If this is not called visits will be marked as TRANSITION_LINK.
+ */
+ markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) {
+ PlacesUtils.history.markPageAsFollowedBookmark(
+ Services.uriFixup.getFixupURIInfo(aURL).preferredURI
+ );
+ },
+
+ /**
+ * By calling this before visiting an URL, any visit in frames will be
+ * associated to a TRANSITION_FRAMED_LINK transition.
+ * This is actually used to distinguish user-initiated visits in frames
+ * so automatic visits can be correctly ignored.
+ */
+ markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) {
+ PlacesUtils.history.markPageAsFollowedLink(
+ Services.uriFixup.getFixupURIInfo(aURL).preferredURI
+ );
+ },
+
+ /**
+ * Sets the character-set for a page. The character set will not be saved
+ * if the window is determined to be a private browsing window.
+ *
+ * @param {string|URL|nsIURI} url The URL of the page to set the charset on.
+ * @param {String} charset character-set value.
+ * @param {window} window The window that the charset is being set from.
+ * @return {Promise}
+ */
+ async setCharsetForPage(url, charset, window) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+
+ // UTF-8 is the default. If we are passed the value then set it to null,
+ // to ensure any charset is removed from the database.
+ if (charset.toLowerCase() == "utf-8") {
+ charset = null;
+ }
+
+ await PlacesUtils.history.update({
+ url,
+ annotations: new Map([[PlacesUtils.CHARSET_ANNO, charset]]),
+ });
+ },
+
+ /**
+ * Allows opening of javascript/data URI only if the given node is
+ * bookmarked (see bug 224521).
+ * @param aURINode
+ * a URI node
+ * @param aWindow
+ * a window on which a potential error alert is shown on.
+ * @return true if it's safe to open the node in the browser, false otherwise.
+ *
+ */
+ checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) {
+ if (PlacesUtils.nodeIsBookmark(aURINode)) {
+ return true;
+ }
+
+ var uri = Services.io.newURI(aURINode.uri);
+ if (uri.schemeIs("javascript") || uri.schemeIs("data")) {
+ const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
+ var brandShortName = Services.strings
+ .createBundle(BRANDING_BUNDLE_URI)
+ .GetStringFromName("brandShortName");
+
+ var errorStr = this.getString("load-js-data-url-error");
+ Services.prompt.alert(aWindow, brandShortName, errorStr);
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Check whether or not the given node represents a removable entry (either in
+ * history or in bookmarks).
+ *
+ * @param aNode
+ * a node, except the root node of a query.
+ * @return true if the aNode represents a removable entry, false otherwise.
+ */
+ canUserRemove(aNode) {
+ let parentNode = aNode.parent;
+ if (!parentNode) {
+ // canUserRemove doesn't accept root nodes.
+ return false;
+ }
+
+ // Is it a query pointing to one of the special root folders?
+ if (PlacesUtils.nodeIsQuery(parentNode)) {
+ if (PlacesUtils.nodeIsFolder(aNode)) {
+ let guid = PlacesUtils.getConcreteItemGuid(aNode);
+ // If the parent folder is not a folder, it must be a query, and so this node
+ // cannot be removed.
+ if (PlacesUtils.isRootItem(guid)) {
+ return false;
+ }
+ } else if (PlacesUtils.isVirtualLeftPaneItem(aNode.bookmarkGuid)) {
+ // If the item is a left-pane top-level item, it can't be removed.
+ return false;
+ }
+ }
+
+ // If it's not a bookmark, or it's child of a query, we can remove it.
+ if (aNode.itemId == -1 || PlacesUtils.nodeIsQuery(parentNode)) {
+ return true;
+ }
+
+ // Otherwise it has to be a child of an editable folder.
+ return !this.isFolderReadOnly(parentNode);
+ },
+
+ /**
+ * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
+ * TO GUIDS IS COMPLETE (BUG 1071511).
+ *
+ * Check whether or not the given Places node points to a folder which
+ * should not be modified by the user (i.e. its children should be unremovable
+ * and unmovable, new children should be disallowed, etc).
+ * These semantics are not inherited, meaning that read-only folder may
+ * contain editable items (for instance, the places root is read-only, but all
+ * of its direct children aren't).
+ *
+ * You should only pass folder nodes.
+ *
+ * @param placesNode
+ * any folder result node.
+ * @throws if placesNode is not a folder result node or views is invalid.
+ * @return true if placesNode is a read-only folder, false otherwise.
+ */
+ isFolderReadOnly(placesNode) {
+ if (
+ typeof placesNode != "object" ||
+ !PlacesUtils.nodeIsFolder(placesNode)
+ ) {
+ throw new Error("invalid value for placesNode");
+ }
+
+ return (
+ PlacesUtils.getConcreteItemId(placesNode) == PlacesUtils.placesRootId
+ );
+ },
+
+ /** aItemsToOpen needs to be an array of objects of the form:
+ * {uri: string, isBookmark: boolean}
+ */
+ openTabset(aItemsToOpen, aEvent, aWindow) {
+ if (!aItemsToOpen.length) {
+ return;
+ }
+
+ let browserWindow = getBrowserWindow(aWindow);
+ var urls = [];
+ let skipMarking =
+ browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow);
+ for (let item of aItemsToOpen) {
+ urls.push(item.uri);
+ if (skipMarking) {
+ continue;
+ }
+
+ if (item.isBookmark) {
+ this.markPageAsFollowedBookmark(item.uri);
+ } else {
+ this.markPageAsTyped(item.uri);
+ }
+ }
+
+ // whereToOpenLink doesn't return "window" when there's no browser window
+ // open (Bug 630255).
+ var where = browserWindow
+ ? browserWindow.whereToOpenLink(aEvent, false, true)
+ : "window";
+ if (where == "window") {
+ // There is no browser window open, thus open a new one.
+ let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ let stringsToLoad = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ urls.forEach(url =>
+ stringsToLoad.appendElement(PlacesUtils.toISupportsString(url))
+ );
+ args.appendElement(stringsToLoad);
+
+ browserWindow = Services.ww.openWindow(
+ aWindow,
+ AppConstants.BROWSER_CHROME_URL,
+ null,
+ "chrome,dialog=no,all",
+ args
+ );
+ return;
+ }
+
+ var loadInBackground = where == "tabshifted";
+ // For consistency, we want all the bookmarks to open in new tabs, instead
+ // of having one of them replace the currently focused tab. Hence we call
+ // loadTabs with aReplace set to false.
+ browserWindow.gBrowser.loadTabs(urls, {
+ inBackground: loadInBackground,
+ replace: false,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+
+ /**
+ * Loads a selected node's or nodes' URLs in tabs,
+ * warning the user when lots of URLs are being opened
+ *
+ * @param {object|array} nodeOrNodes
+ * Contains the node or nodes that we're opening in tabs
+ * @param {event} event
+ * The DOM mouse/key event with modifier keys set that track the
+ * user's preferred destination window or tab.
+ * @param {object} view
+ * The current view that contains the node or nodes selected for
+ * opening
+ */
+ openMultipleLinksInTabs(nodeOrNodes, event, view) {
+ let window = view.ownerWindow;
+ let urlsToOpen = [];
+
+ if (PlacesUtils.nodeIsContainer(nodeOrNodes)) {
+ urlsToOpen = PlacesUtils.getURLsForContainerNode(nodeOrNodes);
+ } else {
+ for (var i = 0; i < nodeOrNodes.length; i++) {
+ // Skip over separators and folders.
+ if (PlacesUtils.nodeIsURI(nodeOrNodes[i])) {
+ urlsToOpen.push({
+ uri: nodeOrNodes[i].uri,
+ isBookmark: PlacesUtils.nodeIsBookmark(nodeOrNodes[i]),
+ });
+ }
+ }
+ }
+ if (OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) {
+ this.openTabset(urlsToOpen, event, window);
+ }
+ },
+
+ /**
+ * Loads the node's URL in the appropriate tab or window given the
+ * user's preference specified by modifier keys tracked by a
+ * DOM mouse/key event.
+ * @param aNode
+ * An uri result node.
+ * @param aEvent
+ * The DOM mouse/key event with modifier keys set that track the
+ * user's preferred destination window or tab.
+ */
+ openNodeWithEvent: function PUIU_openNodeWithEvent(aNode, aEvent) {
+ let window = aEvent.target.ownerGlobal;
+
+ let browserWindow = getBrowserWindow(window);
+
+ let where = window.whereToOpenLink(aEvent, false, true);
+ if (this.loadBookmarksInTabs && PlacesUtils.nodeIsBookmark(aNode)) {
+ if (where == "current" && !aNode.uri.startsWith("javascript:")) {
+ where = "tab";
+ }
+ if (where == "tab" && browserWindow.gBrowser.selectedTab.isEmpty) {
+ where = "current";
+ }
+ }
+
+ this._openNodeIn(aNode, where, window);
+ },
+
+ /**
+ * Loads the node's URL in the appropriate tab or window.
+ * see also openUILinkIn
+ */
+ openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) {
+ let window = aView.ownerWindow;
+ this._openNodeIn(aNode, aWhere, window, aPrivate);
+ },
+
+ _openNodeIn: function PUIU__openNodeIn(
+ aNode,
+ aWhere,
+ aWindow,
+ aPrivate = false
+ ) {
+ if (
+ aNode &&
+ PlacesUtils.nodeIsURI(aNode) &&
+ this.checkURLSecurity(aNode, aWindow)
+ ) {
+ let isBookmark = PlacesUtils.nodeIsBookmark(aNode);
+
+ if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ if (isBookmark) {
+ this.markPageAsFollowedBookmark(aNode.uri);
+ } else {
+ this.markPageAsTyped(aNode.uri);
+ }
+ }
+
+ const isJavaScriptURL = aNode.uri.startsWith("javascript:");
+ aWindow.openTrustedLinkIn(aNode.uri, aWhere, {
+ allowPopups: isJavaScriptURL,
+ inBackground: this.loadBookmarksInBackground,
+ allowInheritPrincipal: isJavaScriptURL,
+ private: aPrivate,
+ });
+ }
+ },
+
+ /**
+ * Helper for guessing scheme from an url string.
+ * Used to avoid nsIURI overhead in frequently called UI functions.
+ *
+ * @param {string} href The url to guess the scheme from.
+ * @return guessed scheme for this url string.
+ * @note this is not supposed be perfect, so use it only for UI purposes.
+ */
+ guessUrlSchemeForUI(href) {
+ return href.substr(0, href.indexOf(":"));
+ },
+
+ getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) {
+ var title;
+ if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) {
+ // if node title is empty, try to set the label using host and filename
+ // Services.io.newURI will throw if aNode.uri is not a valid URI
+ try {
+ var uri = Services.io.newURI(aNode.uri);
+ var host = uri.host;
+ var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
+ // if fileName is empty, use path to distinguish labels
+ if (aDoNotCutTitle) {
+ title = host + uri.pathQueryRef;
+ } else {
+ title =
+ host +
+ (fileName
+ ? (host ? "/" + this.ellipsis + "/" : "") + fileName
+ : uri.pathQueryRef);
+ }
+ } catch (e) {
+ // Use (no title) for non-standard URIs (data:, javascript:, ...)
+ title = "";
+ }
+ } else {
+ title = aNode.title;
+ }
+
+ return title || this.getString("noTitle");
+ },
+
+ shouldShowTabsFromOtherComputersMenuitem() {
+ let weaveOK =
+ Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
+ Weave.Svc.Prefs.get("firstSync", "") != "notReady";
+ return weaveOK;
+ },
+
+ /**
+ * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A
+ * FUTURE RELEASE.
+ *
+ * Checks if a place: href represents a folder shortcut.
+ *
+ * @param queryString
+ * the query string to check (a place: href)
+ * @return whether or not queryString represents a folder shortcut.
+ * @throws if queryString is malformed.
+ */
+ isFolderShortcutQueryString(queryString) {
+ // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp.
+
+ let query = {},
+ options = {};
+ PlacesUtils.history.queryStringToQuery(queryString, query, options);
+ query = query.value;
+ options = options.value;
+ return (
+ query.folderCount == 1 &&
+ !query.hasBeginTime &&
+ !query.hasEndTime &&
+ !query.hasDomain &&
+ !query.hasURI &&
+ !query.hasSearchTerms &&
+ !query.tags.length == 0 &&
+ options.maxResults == 0
+ );
+ },
+
+ /**
+ * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+ *
+ * Given a bookmark object for either a url bookmark or a folder, returned by
+ * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for
+ * initialising the edit overlay with it.
+ *
+ * @param aFetchInfo
+ * a bookmark object returned by Bookmarks.fetch.
+ * @return a node-like object suitable for initialising editBookmarkOverlay.
+ * @throws if aFetchInfo is representing a separator.
+ */
+ async promiseNodeLikeFromFetchInfo(aFetchInfo) {
+ if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
+ throw new Error("promiseNodeLike doesn't support separators");
+ }
+
+ let parent = {
+ itemId: await PlacesUtils.promiseItemId(aFetchInfo.parentGuid),
+ bookmarkGuid: aFetchInfo.parentGuid,
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ };
+
+ return Object.freeze({
+ itemId: await PlacesUtils.promiseItemId(aFetchInfo.guid),
+ bookmarkGuid: aFetchInfo.guid,
+ title: aFetchInfo.title,
+ uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
+
+ get type() {
+ if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER) {
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+ }
+
+ if (!this.uri.length) {
+ throw new Error("Unexpected item type");
+ }
+
+ if (/^place:/.test(this.uri)) {
+ if (this.isFolderShortcutQueryString(this.uri)) {
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+ }
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+ }
+
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ },
+
+ get parent() {
+ return parent;
+ },
+ });
+ },
+
+ /**
+ * This function wraps potentially large places transaction operations
+ * with batch notifications to the result node, hence switching the views
+ * to batch mode.
+ *
+ * @param {nsINavHistoryResult} resultNode The result node to turn on batching.
+ * @note If resultNode is not supplied, the function will pass-through to
+ * functionToWrap.
+ * @param {Integer} itemsBeingChanged The count of items being changed. If the
+ * count is lower than a threshold, then
+ * batching won't be set.
+ * @param {Function} functionToWrap The function to
+ */
+ async batchUpdatesForNode(resultNode, itemsBeingChanged, functionToWrap) {
+ if (!resultNode) {
+ await functionToWrap();
+ return;
+ }
+
+ resultNode = resultNode.QueryInterface(Ci.nsINavBookmarkObserver);
+
+ if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
+ resultNode.onBeginUpdateBatch();
+ }
+
+ try {
+ await functionToWrap();
+ } finally {
+ if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
+ resultNode.onEndUpdateBatch();
+ }
+ }
+ },
+
+ /**
+ * Processes a set of transfer items that have been dropped or pasted.
+ * Batching will be applied where necessary.
+ *
+ * @param {Array} items A list of unwrapped nodes to process.
+ * @param {Object} insertionPoint The requested point for insertion.
+ * @param {Boolean} doCopy Set to true to copy the items, false will move them
+ * if possible.
+ * @paramt {Object} view The view that should be used for batching.
+ * @return {Array} Returns an empty array when the insertion point is a tag, else
+ * returns an array of copied or moved guids.
+ */
+ async handleTransferItems(items, insertionPoint, doCopy, view) {
+ let transactions;
+ let itemsCount;
+ if (insertionPoint.isTag) {
+ let urls = items.filter(item => "uri" in item).map(item => item.uri);
+ itemsCount = urls.length;
+ transactions = [
+ PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName }),
+ ];
+ } else {
+ let insertionIndex = await insertionPoint.getIndex();
+ itemsCount = items.length;
+ transactions = getTransactionsForTransferItems(
+ items,
+ insertionIndex,
+ insertionPoint.guid,
+ !doCopy
+ );
+ }
+
+ // Check if we actually have something to add, if we don't it probably wasn't
+ // valid, or it was moving to the same location, so just ignore it.
+ if (!transactions.length) {
+ return [];
+ }
+
+ let guidsToSelect = [];
+ let resultForBatching = getResultForBatching(view);
+
+ // If we're inserting into a tag, we don't get the guid, so we'll just
+ // pass the transactions direct to the batch function.
+ let batchingItem = transactions;
+ if (!insertionPoint.isTag) {
+ // If we're not a tag, then we need to get the ids of the items to select.
+ batchingItem = async () => {
+ for (let transaction of transactions) {
+ let result = await transaction.transact();
+ guidsToSelect = guidsToSelect.concat(result);
+ }
+ };
+ }
+
+ await this.batchUpdatesForNode(resultForBatching, itemsCount, async () => {
+ await PlacesTransactions.batch(batchingItem);
+ });
+
+ return guidsToSelect;
+ },
+
+ onSidebarTreeClick(event) {
+ // right-clicks are not handled here
+ if (event.button == 2) {
+ return;
+ }
+
+ let tree = event.target.parentNode;
+ let cell = tree.getCellAt(event.clientX, event.clientY);
+ if (cell.row == -1 || cell.childElt == "twisty") {
+ return;
+ }
+
+ // getCoordsForCellItem returns the x coordinate in logical coordinates
+ // (i.e., starting from the left and right sides in LTR and RTL modes,
+ // respectively.) Therefore, we make sure to exclude the blank area
+ // before the tree item icon (that is, to the left or right of it in
+ // LTR and RTL modes, respectively) from the click target area.
+ let win = tree.ownerGlobal;
+ let rect = tree.getCoordsForCellItem(cell.row, cell.col, "image");
+ let isRTL = win.getComputedStyle(tree).direction == "rtl";
+ let mouseInGutter = isRTL ? event.clientX > rect.x : event.clientX < rect.x;
+
+ let metaKey =
+ AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey;
+ let modifKey = metaKey || event.shiftKey;
+ let isContainer = tree.view.isContainer(cell.row);
+ let openInTabs =
+ isContainer &&
+ (event.button == 1 || (event.button == 0 && modifKey)) &&
+ PlacesUtils.hasChildURIs(tree.view.nodeForTreeIndex(cell.row));
+
+ if (event.button == 0 && isContainer && !openInTabs) {
+ tree.view.toggleOpenState(cell.row);
+ } else if (
+ !mouseInGutter &&
+ openInTabs &&
+ event.originalTarget.localName == "treechildren"
+ ) {
+ tree.view.selection.select(cell.row);
+ this.openMultipleLinksInTabs(tree.selectedNode, event, tree);
+ } else if (
+ !mouseInGutter &&
+ !isContainer &&
+ event.originalTarget.localName == "treechildren"
+ ) {
+ // Clear all other selection since we're loading a link now. We must
+ // do this *before* attempting to load the link since openURL uses
+ // selection as an indication of which link to load.
+ tree.view.selection.select(cell.row);
+ this.openNodeWithEvent(tree.selectedNode, event);
+ }
+ },
+
+ onSidebarTreeKeyPress(event) {
+ let node = event.target.selectedNode;
+ if (node) {
+ if (event.keyCode == event.DOM_VK_RETURN) {
+ this.openNodeWithEvent(node, event);
+ }
+ }
+ },
+
+ /**
+ * The following function displays the URL of a node that is being
+ * hovered over.
+ */
+ onSidebarTreeMouseMove(event) {
+ let treechildren = event.target;
+ if (treechildren.localName != "treechildren") {
+ return;
+ }
+
+ let tree = treechildren.parentNode;
+ let cell = tree.getCellAt(event.clientX, event.clientY);
+
+ // cell.row is -1 when the mouse is hovering an empty area within the tree.
+ // To avoid showing a URL from a previously hovered node for a currently
+ // hovered non-url node, we must clear the moused-over URL in these cases.
+ if (cell.row != -1) {
+ let node = tree.view.nodeForTreeIndex(cell.row);
+ if (PlacesUtils.nodeIsURI(node)) {
+ this.setMouseoverURL(node.uri, tree.ownerGlobal);
+ return;
+ }
+ }
+ this.setMouseoverURL("", tree.ownerGlobal);
+ },
+
+ setMouseoverURL(url, win) {
+ // When the browser window is closed with an open sidebar, the sidebar
+ // unload event happens after the browser's one. In this case
+ // top.XULBrowserWindow has been nullified already.
+ if (win.top.XULBrowserWindow) {
+ win.top.XULBrowserWindow.setOverLink(url);
+ }
+ },
+
+ ensureBookmarkToolbarTelemetryListening() {
+ if (this._bookmarkToolbarTelemetryListening) {
+ return;
+ }
+
+ // This listener is for counting new bookmarks
+ let placesUtilsObserversListener = events => {
+ for (let event of events) {
+ if (
+ event.type == "bookmark-added" &&
+ event.parentGuid == PlacesUtils.bookmarks.toolbarGuid
+ ) {
+ Services.telemetry.scalarAdd(
+ "browser.engagement.bookmarks_toolbar_bookmark_added",
+ 1
+ );
+ }
+ }
+ };
+
+ // This listener is for tracking bookmark moves
+ let placesUtilsBookmarksObserver = {
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemChanged() {},
+ onItemMoved(
+ aItemId,
+ aProperty,
+ aIsAnnotationProperty,
+ aNewValue,
+ aLastModified,
+ aItemType,
+ aGuid,
+ oldParentGuid,
+ newParentGuid
+ ) {
+ let hasMovedToToolbar =
+ newParentGuid == PlacesUtils.bookmarks.toolbarGuid &&
+ oldParentGuid != PlacesUtils.bookmarks.toolbarGuid;
+ if (hasMovedToToolbar) {
+ Services.telemetry.scalarAdd(
+ "browser.engagement.bookmarks_toolbar_bookmark_added",
+ 1
+ );
+ }
+ },
+ };
+
+ this._bookmarkToolbarTelemetryListening = true;
+ PlacesUtils.observers.addListener(
+ ["bookmark-added"],
+ placesUtilsObserversListener
+ );
+ PlacesUtils.bookmarks.addObserver(placesUtilsBookmarksObserver);
+ PlacesUtils.registerShutdownFunction(() => {
+ PlacesUtils.observers.removeListener(
+ ["bookmark-added"],
+ placesUtilsObserversListener
+ );
+ PlacesUtils.bookmarks.removeObserver(placesUtilsBookmarksObserver);
+ });
+ },
+
+ /**
+ * Uncollapses PersonalToolbar if its collapsed status is not
+ * persisted, and user customized it or changed default bookmarks.
+ *
+ * If the user does not have a persisted value for the toolbar's
+ * "collapsed" attribute, try to determine whether it's customized.
+ *
+ * @param {Boolean} aForceVisible Set to true to ignore if the user had
+ * previously collapsed the toolbar manually.
+ */
+ NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE: 3,
+ maybeToggleBookmarkToolbarVisibility(aForceVisible = false) {
+ const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
+ let xulStore = Services.xulStore;
+
+ if (
+ aForceVisible ||
+ !xulStore.hasValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed")
+ ) {
+ // We consider the toolbar customized if it has more than NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE
+ // children, or if it has a persisted currentset value.
+ let toolbarIsCustomized = xulStore.hasValue(
+ BROWSER_DOCURL,
+ "PersonalToolbar",
+ "currentset"
+ );
+
+ if (
+ aForceVisible ||
+ toolbarIsCustomized ||
+ PlacesUtils.getChildCountForFolder(PlacesUtils.bookmarks.toolbarGuid) >
+ this.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE
+ ) {
+ Services.obs.notifyObservers(
+ null,
+ "browser-set-toolbar-visibility",
+ JSON.stringify([CustomizableUI.AREA_BOOKMARKS, "true"])
+ );
+ }
+ }
+ },
+
+ maybeToggleBookmarkToolbarVisibilityAfterMigration() {
+ if (
+ Services.prefs.getBoolPref(
+ "browser.migrate.showBookmarksToolbarAfterMigration"
+ )
+ ) {
+ this.maybeToggleBookmarkToolbarVisibility(true);
+ }
+ },
+
+ async managedPlacesContextShowing(event) {
+ let menupopup = event.target;
+ let document = menupopup.ownerDocument;
+ let window = menupopup.ownerGlobal;
+ // We need to populate the submenus in order to have information
+ // to show the context menu.
+ if (
+ menupopup.triggerNode.id == "managed-bookmarks" &&
+ !menupopup.triggerNode.menupopup.hasAttribute("hasbeenopened")
+ ) {
+ await window.PlacesToolbarHelper.populateManagedBookmarks(
+ menupopup.triggerNode.menupopup
+ );
+ }
+ let linkItems = [
+ "placesContext_open:newtab",
+ "placesContext_open:newwindow",
+ "placesContext_open:newprivatewindow",
+ "placesContext_openSeparator",
+ "placesContext_copy",
+ ];
+ Array.from(menupopup.children).forEach(function(child) {
+ if (!(child.id in linkItems)) {
+ child.hidden = true;
+ }
+ });
+ // Store triggerNode in controller for checking if commands are enabled
+ this.managedBookmarksController.triggerNode = menupopup.triggerNode;
+ // Container in this context means a folder.
+ let isFolder = menupopup.triggerNode.hasAttribute("container");
+ let openContainerInTabs_menuitem = document.getElementById(
+ "placesContext_openContainer:tabs"
+ );
+ if (isFolder) {
+ // Disable the openContainerInTabs menuitem if there
+ // are no children of the menu that have links.
+ let menuitems = menupopup.triggerNode.menupopup.children;
+ let openContainerInTabs = Array.from(menuitems).some(
+ menuitem => menuitem.link
+ );
+ openContainerInTabs_menuitem.disabled = !openContainerInTabs;
+ } else {
+ document.getElementById(
+ "placesContext_open:newprivatewindow"
+ ).hidden = PrivateBrowsingUtils.isWindowPrivate(window);
+ }
+ openContainerInTabs_menuitem.hidden = !isFolder;
+ linkItems.forEach(id => (document.getElementById(id).hidden = isFolder));
+
+ event.target.ownerGlobal.updateCommands("places");
+ },
+
+ placesContextShowing(event) {
+ let menupopup = event.target;
+ if (menupopup.id != "placesContext") {
+ // Ignore any popupshowing events from submenus
+ return true;
+ }
+
+ let isManaged = !!menupopup.triggerNode.closest("#managed-bookmarks");
+ if (isManaged) {
+ this.managedPlacesContextShowing(event);
+ return true;
+ }
+ let document = menupopup.ownerDocument;
+ menupopup._view = this.getViewForNode(document.popupNode);
+ if (!menupopup._view) {
+ // This can happen if we try to invoke the context menu on
+ // an uninitialized places toolbar. Just bail out:
+ event.preventDefault();
+ return false;
+ }
+ if (!this.openInTabClosesMenu) {
+ document
+ .getElementById("placesContext_open:newtab")
+ .setAttribute("closemenu", "single");
+ }
+ return menupopup._view.buildContextMenu(menupopup);
+ },
+
+ placesContextHiding(event) {
+ let menupopup = event.target;
+ if (menupopup._view) {
+ menupopup._view.destroyContextMenu();
+ }
+ },
+
+ openSelectionInTabs(event) {
+ let isManaged = !!event.target.parentNode.triggerNode.closest(
+ "#managed-bookmarks"
+ );
+ let controller;
+ if (isManaged) {
+ controller = this.managedBookmarksController;
+ } else {
+ let document = event.target.ownerDocument;
+ controller = PlacesUIUtils.getViewForNode(document.popupNode).controller;
+ }
+ controller.openSelectionInTabs(event);
+ },
+
+ managedBookmarksController: {
+ triggerNode: null,
+
+ openSelectionInTabs(event) {
+ let window = event.target.ownerGlobal;
+ let menuitems = event.target.parentNode.triggerNode.menupopup.children;
+ let items = [];
+ for (let i = 0; i < menuitems.length; i++) {
+ if (menuitems[i].link) {
+ let item = {};
+ item.uri = menuitems[i].link;
+ item.isBookmark = true;
+ items.push(item);
+ }
+ }
+ PlacesUIUtils.openTabset(items, event, window);
+ },
+
+ isCommandEnabled(command) {
+ switch (command) {
+ case "placesCmd_copy":
+ case "placesCmd_open:window":
+ case "placesCmd_open:privatewindow":
+ case "placesCmd_open:tab": {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ doCommand(command) {
+ let window = this.triggerNode.ownerGlobal;
+ switch (command) {
+ case "placesCmd_copy":
+ // This is a little hacky, but there is a lot of code in Places that handles
+ // clipboard stuff, so it's easier to reuse.
+ let node = {};
+ node.type = 0;
+ node.title = this.triggerNode.label;
+ node.uri = this.triggerNode.link;
+
+ // Copied from _populateClipboard in controller.js
+
+ // This order is _important_! It controls how this and other applications
+ // select data to be inserted based on type.
+ let contents = [
+ { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
+ { type: PlacesUtils.TYPE_HTML, entries: [] },
+ { type: PlacesUtils.TYPE_UNICODE, entries: [] },
+ ];
+
+ contents.forEach(function(content) {
+ content.entries.push(PlacesUtils.wrapNode(node, content.type));
+ });
+
+ let xferable = Cc[
+ "@mozilla.org/widget/transferable;1"
+ ].createInstance(Ci.nsITransferable);
+ xferable.init(null);
+
+ function addData(type, data) {
+ xferable.addDataFlavor(type);
+ xferable.setTransferData(
+ type,
+ PlacesUtils.toISupportsString(data),
+ data.length * 2
+ );
+ }
+
+ contents.forEach(function(content) {
+ addData(content.type, content.entries.join(PlacesUtils.endl));
+ });
+
+ Services.clipboard.setData(
+ xferable,
+ null,
+ Ci.nsIClipboard.kGlobalClipboard
+ );
+ break;
+ case "placesCmd_open:privatewindow":
+ window.openUILinkIn(this.triggerNode.link, "window", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ private: true,
+ });
+ break;
+ case "placesCmd_open:window":
+ window.openUILinkIn(this.triggerNode.link, "window", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ private: false,
+ });
+ break;
+ case "placesCmd_open:tab": {
+ window.openUILinkIn(this.triggerNode.link, "tab", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+ }
+ },
+ },
+
+ async maybeAddImportButton() {
+ if (!Services.policies.isAllowed("profileImport")) {
+ return;
+ }
+ // Check if the experiment is running. If not, wait for it to run.
+ const kPref = "browser.toolbars.bookmarks.2h2020";
+ if (!Services.prefs.getBoolPref(kPref, false)) {
+ Services.prefs.addObserver(kPref, function obs() {
+ Services.prefs.removeObserver(kPref, obs);
+ Services.tm.dispatchToMainThread(() =>
+ PlacesUIUtils.maybeAddImportButton()
+ );
+ });
+ return;
+ }
+ let numberOfBookmarks = await PlacesUtils.withConnectionWrapper(
+ "PlacesUIUtils: maybeAddImportButton",
+ async db => {
+ let rows = await db.execute(
+ `SELECT COUNT(*) as n FROM moz_bookmarks b
+ WHERE b.parent = :parentId`,
+ { parentId: PlacesUtils.toolbarFolderId }
+ );
+ return rows[0].getResultByName("n");
+ }
+ ).catch(e => {
+ // We want to report errors, but we still want to add the button then:
+ Cu.reportError(e);
+ return 0;
+ });
+
+ if (numberOfBookmarks < 3) {
+ CustomizableUI.addWidgetToArea(
+ "import-button",
+ CustomizableUI.AREA_BOOKMARKS,
+ 0
+ );
+ Services.prefs.setBoolPref("browser.bookmarks.addedImportButton", true);
+ this.removeImportButtonWhenImportSucceeds();
+ }
+ },
+
+ removeImportButtonWhenImportSucceeds() {
+ // If the user (re)moved the button, clear the pref and stop worrying about
+ // moving the item.
+ let placement = CustomizableUI.getPlacementOfWidget("import-button");
+ if (placement?.area != CustomizableUI.AREA_BOOKMARKS) {
+ Services.prefs.clearUserPref("browser.bookmarks.addedImportButton");
+ return;
+ }
+ // Otherwise, wait for a successful migration:
+ let obs = (subject, topic, data) => {
+ if (
+ data == Ci.nsIBrowserProfileMigrator.BOOKMARKS &&
+ MigrationUtils.getImportedCount("bookmarks") > 0
+ ) {
+ CustomizableUI.removeWidgetFromArea("import-button");
+ Services.prefs.clearUserPref("browser.bookmarks.addedImportButton");
+ Services.obs.removeObserver(obs, "Migration:ItemAfterMigrate");
+ Services.obs.removeObserver(obs, "Migration:ItemError");
+ }
+ };
+ Services.obs.addObserver(obs, "Migration:ItemAfterMigrate");
+ Services.obs.addObserver(obs, "Migration:ItemError");
+ },
+
+ get _nonPrefDefaultParentGuid() {
+ let { unfiledGuid, toolbarGuid } = PlacesUtils.bookmarks;
+ return this._2020h2bookmarks ? toolbarGuid : unfiledGuid;
+ },
+
+ get defaultParentGuid() {
+ if (!PlacesUIUtils._2020h2bookmarks) {
+ return PlacesUtils.bookmarks.unfiledGuid;
+ }
+ // Defined via a lazy pref getter below, see the comment there about the
+ // reason for this (temporary) setup.
+ return this._defaultParentGuid;
+ },
+};
+
+/**
+ * Promise used by the toolbar view browser-places to determine whether we
+ * can start loading its content (which involves IO, and so is postponed
+ * during startup).
+ * This promise's resolution value indicates whether toolbar initialization
+ * waited on sessionstore-windows-restored and then an idle task, or happened
+ * immediately as the window was opened. This is used for telemetry.
+ */
+PlacesUIUtils.canLoadToolbarContentPromise = new Promise(resolve => {
+ PlacesUIUtils.unblockToolbars = () => {
+ resolve("waited-for-session-idle");
+ // Overwrite the property with the new promise, as the session has
+ // now been restored:
+ PlacesUIUtils.canLoadToolbarContentPromise = Promise.resolve("immediate");
+ };
+});
+
+// These are lazy getters to avoid importing PlacesUtils immediately.
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => {
+ return [
+ PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+ PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ ];
+});
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => {
+ return [PlacesUtils.TYPE_X_MOZ_URL, TAB_DROP_TYPE, PlacesUtils.TYPE_UNICODE];
+});
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => {
+ return [...PlacesUIUtils.PLACES_FLAVORS, ...PlacesUIUtils.URI_FLAVORS];
+});
+
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
+ return Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ PlacesUIUtils,
+ "loadBookmarksInBackground",
+ PREF_LOAD_BOOKMARKS_IN_BACKGROUND,
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ PlacesUIUtils,
+ "loadBookmarksInTabs",
+ PREF_LOAD_BOOKMARKS_IN_TABS,
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ PlacesUIUtils,
+ "openInTabClosesMenu",
+ "browser.bookmarks.openInTabClosesMenu",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ PlacesUIUtils,
+ "maxRecentFolders",
+ "browser.bookmarks.editDialog.maxRecentFolders",
+ 7
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ PlacesUIUtils,
+ "_2020h2bookmarks",
+ "browser.toolbars.bookmarks.2h2020",
+ false
+);
+
+/**
+ * This value should be accessed through the defaultParentGuid getter,
+ * which will only access this pref if the browser.toolbars.bookmarks.2h2020
+ * pref is true. We can't put that check directly in the pref transformation
+ * callback below, because then the resulting value doesn't update if the
+ * 2h2020 pref updates, breaking tests and potentially real-world behaviour
+ * if the 2h2020 pref is flipped at runtime.
+ */
+XPCOMUtils.defineLazyPreferenceGetter(
+ PlacesUIUtils,
+ "_defaultParentGuid",
+ "browser.bookmarks.defaultLocation",
+ "", // Avoid eagerly loading PlacesUtils.
+ null,
+ prefValue => {
+ if (!prefValue) {
+ return PlacesUIUtils._nonPrefDefaultParentGuid;
+ }
+ if (["toolbar", "menu", "unfiled"].includes(prefValue)) {
+ return PlacesUtils.bookmarks[prefValue + "Guid"];
+ }
+ return PlacesUtils.bookmarks
+ .fetch({ guid: prefValue })
+ .then(bm => bm.guid)
+ .catch(() => PlacesUIUtils._nonPrefDefaultParentGuid);
+ }
+);
+
+/**
+ * Determines if an unwrapped node can be moved.
+ *
+ * @param unwrappedNode
+ * A node unwrapped by PlacesUtils.unwrapNodes().
+ * @return True if the node can be moved, false otherwise.
+ */
+function canMoveUnwrappedNode(unwrappedNode) {
+ if (
+ (unwrappedNode.concreteGuid &&
+ PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
+ (unwrappedNode.guid && PlacesUtils.isRootItem(unwrappedNode.guid))
+ ) {
+ return false;
+ }
+
+ let parentGuid = unwrappedNode.parentGuid;
+ if (parentGuid == PlacesUtils.bookmarks.rootGuid) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * This gets the most appropriate item for using for batching. In the case of multiple
+ * views being related, the method returns the most expensive result to batch.
+ * For example, if it detects the left-hand library pane, then it will look for
+ * and return the reference to the right-hand pane.
+ *
+ * @param {Object} viewOrElement The item to check.
+ * @return {Object} Will return the best result node to batch, or null
+ * if one could not be found.
+ */
+function getResultForBatching(viewOrElement) {
+ if (
+ viewOrElement &&
+ Element.isInstance(viewOrElement) &&
+ viewOrElement.id === "placesList"
+ ) {
+ // Note: fall back to the existing item if we can't find the right-hane pane.
+ viewOrElement =
+ viewOrElement.ownerDocument.getElementById("placeContent") ||
+ viewOrElement;
+ }
+
+ if (viewOrElement && viewOrElement.result) {
+ return viewOrElement.result;
+ }
+
+ return null;
+}
+
+/**
+ * Processes a set of transfer items and returns transactions to insert or
+ * move them.
+ *
+ * @param {Array} items A list of unwrapped nodes to get transactions for.
+ * @param {Integer} insertionIndex The requested index for insertion.
+ * @param {String} insertionParentGuid The guid of the parent folder to insert
+ * or move the items to.
+ * @param {Boolean} doMove Set to true to MOVE the items if possible, false will
+ * copy them.
+ * @return {Array} Returns an array of created PlacesTransactions.
+ */
+function getTransactionsForTransferItems(
+ items,
+ insertionIndex,
+ insertionParentGuid,
+ doMove
+) {
+ let canMove = true;
+ for (let item of items) {
+ if (!PlacesUIUtils.SUPPORTED_FLAVORS.includes(item.type)) {
+ throw new Error(`Unsupported '${item.type}' data type`);
+ }
+
+ // Work out if this is data from the same app session we're running in.
+ if (!("instanceId" in item) || item.instanceId != PlacesUtils.instanceId) {
+ if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
+ throw new Error(
+ "Can't copy a container from a legacy-transactions build"
+ );
+ }
+ // Only log if this is one of "our" types as external items, e.g. drag from
+ // url bar to toolbar, shouldn't complain.
+ if (PlacesUIUtils.PLACES_FLAVORS.includes(item.type)) {
+ Cu.reportError(
+ "Tried to move an unmovable Places " +
+ "node, reverting to a copy operation."
+ );
+ }
+
+ // We can never move from an external copy.
+ canMove = false;
+ }
+
+ if (doMove && canMove) {
+ canMove = canMoveUnwrappedNode(item);
+ }
+ }
+
+ if (doMove && !canMove) {
+ doMove = false;
+ }
+
+ if (doMove) {
+ // Move is simple, we pass the transaction a list of GUIDs and where to move
+ // them to.
+ return [
+ PlacesTransactions.Move({
+ guids: items.map(item => item.itemGuid),
+ newParentGuid: insertionParentGuid,
+ newIndex: insertionIndex,
+ }),
+ ];
+ }
+
+ return getTransactionsForCopy(items, insertionIndex, insertionParentGuid);
+}
+
+/**
+ * Processes a set of transfer items and returns an array of transactions.
+ *
+ * @param {Array} items A list of unwrapped nodes to get transactions for.
+ * @param {Integer} insertionIndex The requested index for insertion.
+ * @param {String} insertionParentGuid The guid of the parent folder to insert
+ * or move the items to.
+ * @return {Array} Returns an array of created PlacesTransactions.
+ */
+function getTransactionsForCopy(items, insertionIndex, insertionParentGuid) {
+ let transactions = [];
+ let index = insertionIndex;
+
+ for (let item of items) {
+ let transaction;
+ let guid = item.itemGuid;
+
+ if (
+ PlacesUIUtils.PLACES_FLAVORS.includes(item.type) &&
+ // For anything that is comming from within this session, we do a
+ // direct copy, otherwise we fallback and form a new item below.
+ "instanceId" in item &&
+ item.instanceId == PlacesUtils.instanceId &&
+ // If the Item doesn't have a guid, this could be a virtual tag query or
+ // other item, so fallback to inserting a new bookmark with the URI.
+ guid &&
+ // For virtual root items, we fallback to creating a new bookmark, as
+ // we want a shortcut to be created, not a full tree copy.
+ !PlacesUtils.bookmarks.isVirtualRootItem(guid) &&
+ !PlacesUtils.isVirtualLeftPaneItem(guid)
+ ) {
+ transaction = PlacesTransactions.Copy({
+ guid,
+ newIndex: index,
+ newParentGuid: insertionParentGuid,
+ });
+ } else if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
+ transaction = PlacesTransactions.NewSeparator({
+ index,
+ parentGuid: insertionParentGuid,
+ });
+ } else {
+ let title = item.type != PlacesUtils.TYPE_UNICODE ? item.title : item.uri;
+ transaction = PlacesTransactions.NewBookmark({
+ index,
+ parentGuid: insertionParentGuid,
+ title,
+ url: item.uri,
+ });
+ }
+
+ transactions.push(transaction);
+
+ if (index != -1) {
+ index++;
+ }
+ }
+ return transactions;
+}
+
+function getBrowserWindow(aWindow) {
+ // Prefer the caller window if it's a browser window, otherwise use
+ // the top browser window.
+ return aWindow &&
+ aWindow.document.documentElement.getAttribute("windowtype") ==
+ "navigator:browser"
+ ? aWindow
+ : BrowserWindowTracker.getTopWindow();
+}