/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */ this.EXPORTED_SYMBOLS = ["PlacesUIUtils"]; var Ci = Components.interfaces; var Cc = Components.classes; var Cr = Components.results; var Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() { Cu.import("resource://gre/modules/PlacesUtils.jsm"); return PlacesUtils; }); this.PlacesUIUtils = { ORGANIZER_LEFTPANE_VERSION: 7, ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder", ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery", LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar", DESCRIPTION_ANNO: "bookmarkProperties/description", TYPE_TAB_DROP: "application/x-moz-tabbrowser-tab", /** * Makes a URI from a spec, and do fixup * @param aSpec * The string spec of the URI * @returns A URI object for the spec. */ createFixedURI: function(aSpec) { return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE); }, getFormattedString: function(key, params) { return bundle.formatStringFromName(key, params, params.length); }, /** * 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(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(key) { return bundle.GetStringFromName(key); }, get _copyableAnnotations() [ this.DESCRIPTION_ANNO, this.LOAD_IN_SIDEBAR_ANNO, PlacesUtils.POST_DATA_ANNO, PlacesUtils.READ_ONLY_ANNO, ], /** * Get a transaction for copying a uri item (either a bookmark or a history * entry) from one container to another. * * @param aData * JSON object of dropped or pasted item properties * @param aContainer * The container being copied into * @param aIndex * The index within the container the item is copied to * @return A nsITransaction object that performs the copy. * * @note Since a copy creates a completely new item, only some internal * annotations are synced from the old one. * @see this._copyableAnnotations for the list of copyable annotations. */ _getURIItemCopyTransaction: function(aData, aContainer, aIndex) { let transactions = []; if (aData.dateAdded) { transactions.push( new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) ); } if (aData.lastModified) { transactions.push( new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) ); } let keyword = aData.keyword || null; let annos = []; if (aData.annos) { annos = aData.annos.filter(function(aAnno) { return this._copyableAnnotations.indexOf(aAnno.name) != -1; }, this); } return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri), aContainer, aIndex, aData.title, keyword, annos, transactions); }, /** * Gets a transaction for copying (recursively nesting to include children) * a folder (or container) and its contents from one folder to another. * * @param aData * Unwrapped dropped folder data - Obj containing folder and children * @param aContainer * The container we are copying into * @param aIndex * The index in the destination container to insert the new items * @return A nsITransaction object that will perform the copy. * * @note Since a copy creates a completely new item, only some internal * annotations are synced from the old one. * @see this._copyableAnnotations for the list of copyable annotations. */ _getFolderCopyTransaction(aData, aContainer, aIndex) { function getChildItemsTransactions(aRoot) { let transactions = []; let index = aIndex; for (let i = 0; i < aRoot.childCount; ++i) { let child = aRoot.getChild(i); // Temporary hacks until we switch to PlacesTransactions.jsm. let isLivemark = PlacesUtils.annotations.itemHasAnnotation(child.itemId, PlacesUtils.LMANNO_FEEDURI); let [node] = PlacesUtils.unwrapNodes( PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark), PlacesUtils.TYPE_X_MOZ_PLACE ); // Make sure that items are given the correct index, this will be // passed by the transaction manager to the backend for the insertion. // Insertion behaves differently for DEFAULT_INDEX (append). if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) { index = i; } if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { if (node.livemark && node.annos) { transactions.push( PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index) ); } else { transactions.push( PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index) ); } } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { transactions.push(new PlacesCreateSeparatorTransaction(-1, index)); } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) { transactions.push( PlacesUIUtils._getURIItemCopyTransaction(node, -1, index) ); } else { throw new Error("Unexpected item under a bookmarks folder"); } } return transactions; } if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder. let transactions = []; if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { let {root} = PlacesUtils.getFolderContents(aData.id, false, false); let urls = PlacesUtils.getURLsForContainerNode(root); root.containerOpen = false; for (let { uri } of urls) { transactions.push( new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title]) ); } } return new PlacesAggregatedTransaction("addTags", transactions); } if (aData.livemark && aData.annos) { // Copying a livemark. return this._getLivemarkCopyTransaction(aData, aContainer, aIndex); } let {root} = PlacesUtils.getFolderContents(aData.id, false, false); let transactions = getChildItemsTransactions(root); root.containerOpen = false; if (aData.dateAdded) { transactions.push( new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) ); } if (aData.lastModified) { transactions.push( new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) ); } let annos = []; if (aData.annos) { annos = aData.annos.filter(function(aAnno) { return this._copyableAnnotations.indexOf(aAnno.name) != -1; }, this); } return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex, annos, transactions); }, /** * Gets a transaction for copying a live bookmark item from one container to * another. * * @param aData * Unwrapped live bookmarkmark data * @param aContainer * The container we are copying into * @param aIndex * The index in the destination container to insert the new items * @return A nsITransaction object that will perform the copy. * * @note Since a copy creates a completely new item, only some internal * annotations are synced from the old one. * @see this._copyableAnnotations for the list of copyable annotations. */ _getLivemarkCopyTransaction: function(aData, aContainer, aIndex) { if (!aData.livemark || !aData.annos) { throw new Error("node is not a livemark"); } let feedURI, siteURI; let annos = []; if (aData.annos) { annos = aData.annos.filter(function(aAnno) { if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) { feedURI = PlacesUtils._uri(aAnno.value); } else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) { siteURI = PlacesUtils._uri(aAnno.value); } return this._copyableAnnotations.indexOf(aAnno.name) != -1 }, this); } return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title, aContainer, aIndex, annos); }, /** * Test if a bookmark item = a live bookmark item. * * @param aItemId * item identifier * @return true if a live bookmark item, false otherwise. * * @note Maybe this should be removed later, see bug 1072833. */ _isLivemark: function(aItemId) { // Since this check may be done on each dragover event, it's worth maintaining // a cache. let self = this._isLivemark; if (!("ids" in self)) { const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI; let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO); self.ids = new Set(idsVec); let obs = Object.freeze({ QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver), onItemAnnotationSet(itemId, annoName) { if (annoName == LIVEMARK_ANNO) self.ids.add(itemId); }, onItemAnnotationRemoved(itemId, annoName) { // If annoName is set to an empty string, the item is gone. if (annoName == LIVEMARK_ANNO || annoName == "") self.ids.delete(itemId); }, onPageAnnotationSet() { }, onPageAnnotationRemoved() { }, }); PlacesUtils.annotations.addObserver(obs); PlacesUtils.registerShutdownFunction(() => { PlacesUtils.annotations.removeObserver(obs); }); } return self.ids.has(aItemId); }, /** * Constructs a Transaction for the drop or paste of a blob of data into * a container. * @param data * The unwrapped data blob of dropped or pasted data. * @param type * The content type of the data * @param container * The container the data was dropped or pasted into * @param index * The index within the container the item was dropped or pasted at * @param copy * The drag action was copy, so don't move folders or links. * @returns An object implementing nsITransaction that can perform * the move/insert. */ makeTransaction: function(data, type, container, index, copy) { switch (data.type) { case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: if (copy) { return this._getFolderCopyTransaction(data, container, index); } // Otherwise move the item. return new PlacesMoveItemTransaction(data.id, container, index); break; case PlacesUtils.TYPE_X_MOZ_PLACE: if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked. return this._getURIItemCopyTransaction(data, container, index); } // Otherwise move the item. return new PlacesMoveItemTransaction(data.id, container, index); break; case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: if (copy) { // There is no data in a separator, so copying it just amounts to // inserting a new separator. return new PlacesCreateSeparatorTransaction(container, index); } // Otherwise move the item. return new PlacesMoveItemTransaction(data.id, container, index); break; default: if (type == PlacesUtils.TYPE_X_MOZ_URL || type == PlacesUtils.TYPE_UNICODE || type == this.TYPE_TAB_DROP) { let title = type != PlacesUtils.TYPE_UNICODE ? data.title : data.uri; return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri), container, index, title); } } return null; }, /** * Shows the bookmark dialog corresponding to the specified info. * * @param aInfo * Describes the item to be edited/added in the dialog. * See documentation at the top of bookmarkProperties.js * @param aWindow * Owner window for the new dialog. * * @see documentation at the top of bookmarkProperties.js * @return true if any transaction has been performed, false otherwise. */ showBookmarkDialog: function(aInfo, aParentWindow) { // 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.indexOf("folderPicker") == -1; // Use a different chrome url to persist different sizes. let dialogURL = hasFolderPicker ? "chrome://browser/content/places/bookmarkProperties2.xul" : "chrome://browser/content/places/bookmarkProperties.xul"; let features = "centerscreen,chrome,modal,resizable=yes"; aParentWindow.openDialog(dialogURL, "", features, aInfo); return ("performed" in aInfo && aInfo.performed); }, _getTopBrowserWin: function() { return RecentWindow.getMostRecentBrowserWindow(); }, /** * 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(aNode) { let node = aNode; // The view for a of which its associated menupopup is a places // view, is the menupopup. if (node.localName == "menu" && !node._placesNode && node.lastChild._placesView) return node.lastChild._placesView; while (node instanceof Ci.nsIDOMElement) { if (node._placesView) return node._placesView; if (node.localName == "tree" && node.getAttribute("type") == "places") return node; node = node.parentNode; } return null; }, /** * 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(aURL) { PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL)); }, /** * 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(aURL) { PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL)); }, /** * 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(aURL) { PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL)); }, /** * 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(aURINode, aWindow) { if (PlacesUtils.nodeIsBookmark(aURINode)) return true; var uri = PlacesUtils._uri(aURINode.uri); if (uri.schemeIs("javascript") || uri.schemeIs("data")) { const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService). 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; }, /** * Get the description associated with a document, as specified in a * element. * @param doc * A DOM Document to get a description for * @returns A description string if a META element was discovered with a * "description" or "httpequiv" attribute, empty string otherwise. */ getDescriptionFromDocument: function(doc) { var metaElements = doc.getElementsByTagName("META"); for (var i = 0; i < metaElements.length; ++i) { if (metaElements[i].name.toLowerCase() == "description" || metaElements[i].httpEquiv.toLowerCase() == "description") { return metaElements[i].content; } } return ""; }, /** * Retrieve the description of an item * @param aItemId * item identifier * @returns the description of the given item, or an empty string if it is * not set. */ getItemDescription: function(aItemId) { if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO)) return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO); return ""; }, /** * 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: function(aNode) { let parentNode = aNode.parent; if (!parentNode) throw new Error("canUserRemove doesn't accept root nodes"); // If it's not a bookmark, we can remove it unless it's a child of a // livemark. if (aNode.itemId == -1) { // Rather than executing a db query, checking the existence of the feedURI // annotation, detect livemark children by the fact that they are the only // direct non-bookmark children of bookmark folders. return !PlacesUtils.nodeIsFolder(parentNode); } // Generally it's always possible to remove children of a query. if (PlacesUtils.nodeIsQuery(parentNode)) return true; // Otherwise it has to be a child of an editable folder. return !this.isContentsReadOnly(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 node or item-id 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 item ids or folder nodes for aNodeOrItemId. * While this is only enforced for the node case (if an item id of a separator * or a bookmark is passed, false is returned), it's considered the caller's * job to ensure that it checks a folder. * Also note that folder-shortcuts should only be passed as result nodes. * Otherwise they are just treated as bookmarks (i.e. false is returned). * * @param aNodeOrItemId * any item id or result node. * @throws if aNodeOrItemId is neither an item id nor a folder result node. * @note livemark "folders" are considered read-only (but see bug 1072833). * @return true if aItemId points to a read-only folder, false otherwise. */ isContentsReadOnly: function(aNodeOrItemId) { let itemId; if (typeof(aNodeOrItemId) == "number") { itemId = aNodeOrItemId; } else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) { itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId); } else { throw new Error("invalid value for aNodeOrItemId"); } if (itemId == PlacesUtils.placesRootId || this._isLivemark(itemId)) return true; // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter // performing at least a synchronous DB query (and on its very first call // in a fresh profile, it also creates the entire structure). // Therefore we don't want to this function, which is called very often by // isCommandEnabled, to ever be the one that invokes it first, especially // because isCommandEnabled may be called way before the left pane folder is // even created (for example, if the user only uses the bookmarks menu or // toolbar for managing bookmarks). To do so, we avoid comparing to those // special folder if the lazy getter is still in place. This is safe merely // because the only way to access the left pane contents goes through // "resolving" the leftPaneFolderId getter. if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId")) return false; return itemId == this.leftPaneFolderId || itemId == this.allBookmarksFolderId; }, /** * Gives the user a chance to cancel loading lots of tabs at once */ _confirmOpenInTabs: function(numTabsToOpen, aWindow) { const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen"; var reallyOpen = true; if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) { if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) { // default to true: if it were false, we wouldn't get this far var warnOnOpen = { value: true }; var messageKey = "tabs.openWarningMultipleBranded"; var openKey = "tabs.openButtonMultiple"; const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService). createBundle(BRANDING_BUNDLE_URI). GetStringFromName("brandShortName"); var buttonPressed = Services.prompt.confirmEx( aWindow, this.getString("tabs.openWarningTitle"), this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]), (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), this.getString(openKey), null, null, this.getFormattedString("tabs.openWarningPromptMeBranded", [brandShortName]), warnOnOpen ); reallyOpen = (buttonPressed == 0); // don't set the pref unless they press OK and it's false if (reallyOpen && !warnOnOpen.value) Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false); } } return reallyOpen; }, /** aItemsToOpen needs to be an array of objects of the form: * {uri: string, isBookmark: boolean} */ _openTabset: function(aItemsToOpen, aEvent, aWindow) { if (!aItemsToOpen.length) return; // Prefer the caller window if it's a browser window, otherwise use // the top browser window. var browserWindow = null; browserWindow = aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ? aWindow : this._getTopBrowserWin(); 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. var uriList = PlacesUtils.toISupportsString(urls.join("|")); var args = Cc["@mozilla.org/supports-array;1"]. createInstance(Ci.nsISupportsArray); args.AppendElement(uriList); browserWindow = Services.ww.openWindow(aWindow, "chrome://browser/content/browser.xul", null, "chrome,dialog=no,all", args); return; } var loadInBackground = where == "tabshifted" ? true : false; // 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, loadInBackground, false); }, openLiveMarkNodesInTabs: function(aNode, aEvent, aView) { let window = aView.ownerWindow; PlacesUtils.livemarks.getLivemark({id: aNode.itemId}) .then(aLivemark => { urlsToOpen = []; let nodes = aLivemark.getNodesForContainer(aNode); for (let node of nodes) { urlsToOpen.push({uri: node.uri, isBookmark: false}); } if (this._confirmOpenInTabs(urlsToOpen.length, window)) { this._openTabset(urlsToOpen, aEvent, window); } }, Cu.reportError); }, openContainerNodeInTabs: function(aNode, aEvent, aView) { let window = aView.ownerWindow; let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode); if (this._confirmOpenInTabs(urlsToOpen.length, window)) { this._openTabset(urlsToOpen, aEvent, window); } }, openURINodesInTabs: function(aNodes, aEvent, aView) { let window = aView.ownerWindow; let urlsToOpen = []; for (var i=0; i < aNodes.length; i++) { // Skip over separators and folders. if (PlacesUtils.nodeIsURI(aNodes[i])) urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])}); } if (this._confirmOpenInTabs(urlsToOpen.length, window)) { this._openTabset(urlsToOpen, aEvent, window); } }, /** * Loads the node's URL in the appropriate tab or window or as a web * panel 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. * @param aView * The controller associated with aNode. */ openNodeWithEvent: function(aNode, aEvent, aView) { let window = aView.ownerWindow; this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window); }, /** * Loads the node's URL in the appropriate tab or window or as a * web panel. * see also openUILinkIn */ openNodeIn: function(aNode, aWhere, aView, aPrivate) { let window = aView.ownerWindow; this._openNodeIn(aNode, aWhere, window, aPrivate); }, _openNodeIn: function(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); } // Check whether the node is a bookmark which should be opened as // a web panel if (aWhere == "current" && isBookmark) { if (PlacesUtils.annotations .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) { let browserWin = this._getTopBrowserWin(); if (browserWin) { browserWin.openWebPanel(aNode.title, aNode.uri); return; } } } aWindow.openUILinkIn(aNode.uri, aWhere, { inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"), private: aPrivate, }); } }, /** * Helper for guessing scheme from an url string. * Used to avoid nsIURI overhead in frequently called UI functions. * * @param aUrlString 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: function(aUrlString) { return aUrlString.substr(0, aUrlString.indexOf(":")); }, getBestTitle: function(aNode, aDoNotCutTitle) { var title; if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) { // if node title is empty, try to set the label using host and filename // PlacesUtils._uri() will throw if aNode.uri is not a valid URI try { var uri = PlacesUtils._uri(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.path; } else { title = host + (fileName ? (host ? "/" + this.ellipsis + "/" : "") + fileName : uri.path); } } catch (e) { // Use (no title) for non-standard URIs (data:, javascript:, ...) title = ""; } } else title = aNode.title; return title || this.getString("noTitle"); }, get leftPaneQueries() { // build the map this.leftPaneFolderId; return this.leftPaneQueries; }, // Get the folder id for the organizer left-pane folder. get leftPaneFolderId() { let leftPaneRoot = -1; let allBookmarksId; // Shortcuts to services. let bs = PlacesUtils.bookmarks; let as = PlacesUtils.annotations; // This is the list of the left pane queries. let queries = { "PlacesRoot": { title: "" }, "History": { title: this.getString("OrganizerQueryHistory") }, "Downloads": { title: this.getString("OrganizerQueryDownloads") }, "Tags": { title: this.getString("OrganizerQueryTags") }, "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") }, "BookmarksToolbar": { title: null, concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"), concreteId: PlacesUtils.toolbarFolderId }, "BookmarksMenu": { title: null, concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"), concreteId: PlacesUtils.bookmarksMenuFolderId }, "UnfiledBookmarks": { title: null, concreteTitle: PlacesUtils.getString("UnsortedBookmarksFolderTitle"), concreteId: PlacesUtils.unfiledBookmarksFolderId }, }; // All queries but PlacesRoot. const EXPECTED_QUERY_COUNT = 7; // Removes an item and associated annotations, ignoring eventual errors. function safeRemoveItem(aItemId) { try { if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) && !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) { // Some extension annotated their roots with our query annotation, // so we should not delete them. return; } // removeItemAnnotation does not check if item exists, nor the anno, // so this is safe to do. as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO); as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO); // This will throw if the annotation is an orphan. bs.removeItem(aItemId); } catch(e) { /* orphan anno */ } } // Returns true if item really exists, false otherwise. function itemExists(aItemId) { try { bs.getItemIndex(aItemId); return true; } catch(e) { return false; } } // Get all items marked as being the left pane folder. let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO); if (items.length > 1) { // Something went wrong, we cannot have more than one left pane folder, // remove all left pane folders and continue. We will create a new one. items.forEach(safeRemoveItem); } else if (items.length == 1 && items[0] != -1) { leftPaneRoot = items[0]; // Check that organizer left pane root is valid. let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO); if (version != this.ORGANIZER_LEFTPANE_VERSION || !itemExists(leftPaneRoot)) { // Invalid root, we must rebuild the left pane. safeRemoveItem(leftPaneRoot); leftPaneRoot = -1; } } if (leftPaneRoot != -1) { // A valid left pane folder has been found. // Build the leftPaneQueries Map. This is used to quickly access them, // associating a mnemonic name to the real item ids. delete this.leftPaneQueries; this.leftPaneQueries = {}; let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO); // While looping through queries we will also check for their validity. let queriesCount = 0; for (let i = 0; i < items.length; i++) { let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO); // Some extension did use our annotation to decorate their items // with icons, so we should check only our elements, to avoid dataloss. if (!(queryName in queries)) continue; let query = queries[queryName]; query.itemId = items[i]; if (!itemExists(query.itemId)) { // Orphan annotation, bail out and create a new left pane root. break; } // Check that all queries have valid parents. let parentId = bs.getFolderIdForItem(query.itemId); if (items.indexOf(parentId) == -1 && parentId != leftPaneRoot) { // The parent is not part of the left pane, bail out and create a new // left pane root. break; } // Titles could have been corrupted or the user could have changed his // locale. Check title and eventually fix it. if (bs.getItemTitle(query.itemId) != query.title) bs.setItemTitle(query.itemId, query.title); if ("concreteId" in query) { if (bs.getItemTitle(query.concreteId) != query.concreteTitle) bs.setItemTitle(query.concreteId, query.concreteTitle); } // Add the query to our cache. this.leftPaneQueries[queryName] = query.itemId; queriesCount++; } if (queriesCount != EXPECTED_QUERY_COUNT) { // Queries number is wrong, so the left pane must be corrupt. // Note: we can't just remove the leftPaneRoot, because some query could // have a bad parent, so we have to remove all items one by one. items.forEach(safeRemoveItem); safeRemoveItem(leftPaneRoot); } else { // Everything is fine, return the current left pane folder. delete this.leftPaneFolderId; return this.leftPaneFolderId = leftPaneRoot; } } // Create a new left pane folder. var callback = { // Helper to create an organizer special query. create_query: function(aQueryName, aParentId, aQueryUrl) { let itemId = bs.insertBookmark(aParentId, PlacesUtils._uri(aQueryUrl), bs.DEFAULT_INDEX, queries[aQueryName].title); // Mark as special organizer query. as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName, 0, as.EXPIRE_NEVER); // We should never backup this, since it changes between profiles. as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0, as.EXPIRE_NEVER); // Add to the queries map. PlacesUIUtils.leftPaneQueries[aQueryName] = itemId; return itemId; }, // Helper to create an organizer special folder. create_folder: function(aFolderName, aParentId, aIsRoot) { // Left Pane Root Folder. let folderId = bs.createFolder(aParentId, queries[aFolderName].title, bs.DEFAULT_INDEX); // We should never backup this, since it changes between profiles. as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0, as.EXPIRE_NEVER); if (aIsRoot) { // Mark as special left pane root. as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, 0, as.EXPIRE_NEVER); } else { // Mark as special organizer folder. as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName, 0, as.EXPIRE_NEVER); PlacesUIUtils.leftPaneQueries[aFolderName] = folderId; } return folderId; }, runBatched: function(aUserData) { delete PlacesUIUtils.leftPaneQueries; PlacesUIUtils.leftPaneQueries = { }; // Left Pane Root Folder. leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true); // History Query. this.create_query("History", leftPaneRoot, "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); // Downloads. this.create_query("Downloads", leftPaneRoot, "place:transition=" + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); // Tags Query. this.create_query("Tags", leftPaneRoot, "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY + "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); // All Bookmarks Folder. allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false); // All Bookmarks->Bookmarks Toolbar Query. this.create_query("BookmarksToolbar", allBookmarksId, "place:folder=TOOLBAR"); // All Bookmarks->Bookmarks Menu Query. this.create_query("BookmarksMenu", allBookmarksId, "place:folder=BOOKMARKS_MENU"); // All Bookmarks->Unfiled Bookmarks Query. this.create_query("UnfiledBookmarks", allBookmarksId, "place:folder=UNFILED_BOOKMARKS"); } }; bs.runInBatchMode(callback, null); // Maybe: PlacesUtils.bookmarks.runInBatchMode(callback, null); ? delete this.leftPaneFolderId; return this.leftPaneFolderId = leftPaneRoot; }, /** * Get the folder id for the organizer left-pane folder. */ get allBookmarksFolderId() { // ensure the left-pane root is initialized; this.leftPaneFolderId; delete this.allBookmarksFolderId; return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"]; }, /** * If an item is a left-pane query, returns the name of the query * or an empty string if not. * * @param aItemId id of a container * @returns the name of the query, or empty string if not a left-pane query */ getLeftPaneQueryNameFromId: function(aItemId) { var queryName = ""; // If the let pane hasn't been built, use the annotation service // directly, to avoid building the left pane too early. if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) { try { queryName = PlacesUtils.annotations. getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO); } catch (ex) { // doesn't have the annotation queryName = ""; } } else { // If the left pane has already been built, use the name->id map // cached in PlacesUIUtils. for (let [name, id] in Iterator(this.leftPaneQueries)) { if (aItemId == id) queryName = name; } } return queryName; }, /** * Returns the passed URL with a #moz-resolution fragment * for the specified dimensions and devicePixelRatio. * * @param aWindow * A window from where we want to get the device * pixel Ratio * * @param aURL * The URL where we should add the fragment * * @param aWidth * The target image width * * @param aHeight * The target image height * * @return The URL with the fragment at the end */ getImageURLForResolution: function(aWindow, aURL, aWidth, aHeight) { return aURL; } }; XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF", "@mozilla.org/rdf/rdf-service;1", "nsIRDFService"); XPCOMUtils.defineLazyGetter(PlacesUIUtils, "localStore", function() { return PlacesUIUtils.RDF.GetDataSource("rdf:local-store"); }); XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { return Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; }); XPCOMUtils.defineLazyServiceGetter(this, "URIFixup", "@mozilla.org/docshell/urifixup;1", "nsIURIFixup"); XPCOMUtils.defineLazyGetter(this, "bundle", function() { const PLACES_STRING_BUNDLE_URI = "chrome://browser/locale/places/places.properties"; return Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService). createBundle(PLACES_STRING_BUNDLE_URI); }); XPCOMUtils.defineLazyServiceGetter(this, "focusManager", "@mozilla.org/focus-manager;1", "nsIFocusManager"); /** * This is a compatibility shim for old PUIU.ptm users. * * If you're looking for transactions and writing new code using them, directly * use the transactions objects exported by the PlacesUtils.jsm module. * * This object will be removed once enough users are converted to the new API. */ XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() { // Ensure PlacesUtils is imported in scope. PlacesUtils; return { aggregateTransactions: function(aName, aTransactions) new PlacesAggregatedTransaction(aName, aTransactions), createFolder: function(aName, aContainer, aIndex, aAnnotations, aChildItemsTransactions) new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations, aChildItemsTransactions), createItem: function(aURI, aContainer, aIndex, aTitle, aKeyword, aAnnotations, aChildTransactions) new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle, aKeyword, aAnnotations, aChildTransactions), createSeparator: function(aContainer, aIndex) new PlacesCreateSeparatorTransaction(aContainer, aIndex), createLivemark: function(aFeedURI, aSiteURI, aName, aContainer, aIndex, aAnnotations) new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer, aIndex, aAnnotations), moveItem: function(aItemId, aNewContainer, aNewIndex) new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex), removeItem: function(aItemId) new PlacesRemoveItemTransaction(aItemId), editItemTitle: function(aItemId, aNewTitle) new PlacesEditItemTitleTransaction(aItemId, aNewTitle), editBookmarkURI: function(aItemId, aNewURI) new PlacesEditBookmarkURITransaction(aItemId, aNewURI), setItemAnnotation: function(aItemId, aAnnotationObject) new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject), setPageAnnotation: function(aURI, aAnnotationObject) new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject), editBookmarkKeyword: function(aItemId, aNewKeyword) new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword), editBookmarkPostData: function(aItemId, aPostData) new PlacesEditBookmarkPostDataTransaction(aItemId, aPostData), editLivemarkSiteURI: function(aLivemarkId, aSiteURI) new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI), editLivemarkFeedURI: function(aLivemarkId, aFeedURI) new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI), editItemDateAdded: function(aItemId, aNewDateAdded) new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded), editItemLastModified: function(aItemId, aNewLastModified) new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified), sortFolderByName: function(aFolderId) new PlacesSortFolderByNameTransaction(aFolderId), tagURI: function(aURI, aTags) new PlacesTagURITransaction(aURI, aTags), untagURI: function(aURI, aTags) new PlacesUntagURITransaction(aURI, aTags), /** * Transaction for setting/unsetting Load-in-sidebar annotation. * * @param aBookmarkId * id of the bookmark where to set Load-in-sidebar annotation. * @param aLoadInSidebar * boolean value. * @returns nsITransaction object. */ setLoadInSidebar: function(aItemId, aLoadInSidebar) { let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, type: Ci.nsIAnnotationService.TYPE_INT32, flags: 0, value: aLoadInSidebar, expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); }, /** * Transaction for editing the description of a bookmark or a folder. * * @param aItemId * id of the item to edit. * @param aDescription * new description. * @returns nsITransaction object. */ editItemDescription: function(aItemId, aDescription) { let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO, type: Ci.nsIAnnotationService.TYPE_STRING, flags: 0, value: aDescription, expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); }, //////////////////////////////////////////////////////////////////////////// //// nsITransactionManager forwarders. beginBatch: function() PlacesUtils.transactionManager.beginBatch(null), endBatch: function() PlacesUtils.transactionManager.endBatch(false), doTransaction: function(txn) PlacesUtils.transactionManager.doTransaction(txn), undoTransaction: function() PlacesUtils.transactionManager.undoTransaction(), redoTransaction: function() PlacesUtils.transactionManager.redoTransaction(), get numberOfUndoItems() PlacesUtils.transactionManager.numberOfUndoItems, get numberOfRedoItems() PlacesUtils.transactionManager.numberOfRedoItems, get maxTransactionCount() PlacesUtils.transactionManager.maxTransactionCount, set maxTransactionCount(val) PlacesUtils.transactionManager.maxTransactionCount = val, clear: function() PlacesUtils.transactionManager.clear(), peekUndoStack: function() PlacesUtils.transactionManager.peekUndoStack(), peekRedoStack: function() PlacesUtils.transactionManager.peekRedoStack(), getUndoStack: function() PlacesUtils.transactionManager.getUndoStack(), getRedoStack: function() PlacesUtils.transactionManager.getRedoStack(), AddListener: function(aListener) PlacesUtils.transactionManager.AddListener(aListener), RemoveListener: function(aListener) PlacesUtils.transactionManager.RemoveListener(aListener) } });