/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is the Places Command Controller. * * The Initial Developer of the Original Code is Google Inc. * Portions created by the Initial Developer are Copyright (C) 2005 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Ben Goodger * Myk Melez * Asaf Romano * Sungjoon Steve Won * Dietrich Ayala * Marco Bonardo * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ function LOG(str) { dump("*** " + str + "\n"); } var Ci = Components.interfaces; var Cc = Components.classes; var Cr = Components.results; __defineGetter__("PlacesUtils", function() { delete this.PlacesUtils var tmpScope = {}; Components.utils.import("resource://gre/modules/utils.js", tmpScope); return this.PlacesUtils = tmpScope.PlacesUtils; }); const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; const DESCRIPTION_ANNO = "bookmarkProperties/description"; const GUID_ANNO = "placesInternal/GUID"; const LMANNO_FEEDURI = "livemark/feedURI"; const LMANNO_SITEURI = "livemark/siteURI"; const ORGANIZER_FOLDER_ANNO = "PlacesOrganizer/OrganizerFolder"; const ORGANIZER_QUERY_ANNO = "PlacesOrganizer/OrganizerQuery"; const ORGANIZER_LEFTPANE_VERSION = 6; const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup"; #ifdef XP_MACOSX // On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we // really just want "\n". const NEWLINE= "\n"; #else // On other platforms, the transferable system converts "\r\n" to "\n". const NEWLINE = "\r\n"; #endif function QI_node(aNode, aIID) { return aNode.QueryInterface(aIID); } function asVisit(aNode) { return QI_node(aNode, Ci.nsINavHistoryVisitResultNode); } function asFullVisit(aNode){ return QI_node(aNode, Ci.nsINavHistoryFullVisitResultNode);} function asContainer(aNode){ return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);} function asQuery(aNode) { return QI_node(aNode, Ci.nsINavHistoryQueryResultNode); } var PlacesUIUtils = { /** * The Microsummary Service */ get microsummaries() { delete this.microsummaries; return this.microsummaries = Cc["@mozilla.org/microsummary/service;1"]. getService(Ci.nsIMicrosummaryService); }, get RDF() { delete this.RDF; return this.RDF = Cc["@mozilla.org/rdf/rdf-service;1"]. getService(Ci.nsIRDFService); }, get localStore() { delete this.localStore; return this.localStore = this.RDF.GetDataSource("rdf:local-store"); }, get ptm() { delete this.ptm; return this.ptm = Cc["@mozilla.org/browser/placesTransactionsService;1"]. getService(Ci.nsIPlacesTransactionsService); }, get clipboard() { delete this.clipboard; return this.clipboard = Cc["@mozilla.org/widget/clipboard;1"]. getService(Ci.nsIClipboard); }, get URIFixup() { delete this.URIFixup; return this.URIFixup = Cc["@mozilla.org/docshell/urifixup;1"]. getService(Ci.nsIURIFixup); }, get ellipsis() { delete this.ellipsis; var pref = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); return this.ellipsis = pref.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; }, get privateBrowsing() { delete this.privateBrowsing; return this.privateBrowsing = Cc["@mozilla.org/privatebrowsing;1"]. getService(Ci.nsIPrivateBrowsingService); }, /** * 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 PU_createFixedURI(aSpec) { return this.URIFixup.createFixupURI(aSpec, 0); }, /** * Wraps a string in a nsISupportsString wrapper * @param aString * The string to wrap * @returns A nsISupportsString object containing a string. */ _wrapString: function PU__wrapString(aString) { var s = Cc["@mozilla.org/supports-string;1"]. createInstance(Ci.nsISupportsString); s.data = aString; return s; }, /** * String bundle helpers */ get _bundle() { const PLACES_STRING_BUNDLE_URI = "chrome://browser/locale/places/places.properties"; delete this._bundle; return this._bundle = Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService). createBundle(PLACES_STRING_BUNDLE_URI); }, getFormattedString: function PU_getFormattedString(key, params) { return this._bundle.formatStringFromName(key, params, params.length); }, getString: function PU_getString(key) { return this._bundle.GetStringFromName(key); }, /** * Get a transaction for copying a uri item from one container to another * as a bookmark. * @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 * @returns A nsITransaction object that performs the copy. */ _getURIItemCopyTransaction: function (aData, aContainer, aIndex) { return this.ptm.createItem(PlacesUtils._uri(aData.uri), aContainer, aIndex, aData.title, ""); }, /** * Get a transaction for copying a bookmark item 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 * @param [optional] aExcludeAnnotations * Optional, array of annotations (listed by their names) to exclude * when copying the item. * @returns A nsITransaction object that performs the copy. */ _getBookmarkItemCopyTransaction: function PU__getBookmarkItemCopyTransaction(aData, aContainer, aIndex, aExcludeAnnotations) { var itemURL = PlacesUtils._uri(aData.uri); var itemTitle = aData.title; var keyword = aData.keyword || null; var annos = aData.annos || []; // always exclude GUID when copying any item var excludeAnnos = [GUID_ANNO]; if (aExcludeAnnotations) excludeAnnos = excludeAnnos.concat(aExcludeAnnotations); annos = annos.filter(function(aValue, aIndex, aArray) { return excludeAnnos.indexOf(aValue.name) == -1; }); var childTxns = []; if (aData.dateAdded) childTxns.push(this.ptm.editItemDateAdded(null, aData.dateAdded)); if (aData.lastModified) childTxns.push(this.ptm.editItemLastModified(null, aData.lastModified)); if (aData.tags) { var tags = aData.tags.split(", "); // filter out tags already present, so that undo doesn't remove them // from pre-existing bookmarks var storedTags = PlacesUtils.tagging.getTagsForURI(itemURL); tags = tags.filter(function (aTag) { return (storedTags.indexOf(aTag) == -1); }, this); if (tags.length) childTxns.push(this.ptm.tagURI(itemURL, tags)); } return this.ptm.createItem(itemURL, aContainer, aIndex, itemTitle, keyword, annos, childTxns); }, /** * 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 * @returns A nsITransaction object that will perform the copy. */ _getFolderCopyTransaction: function PU__getFolderCopyTransaction(aData, aContainer, aIndex) { var self = this; function getChildItemsTransactions(aChildren) { var childItemsTransactions = []; var cc = aChildren.length; var index = aIndex; for (var i = 0; i < cc; ++i) { var txn = null; var node = aChildren[i]; // 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 if index == 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) // node is a livemark txn = self._getLivemarkCopyTransaction(node, aContainer, index); else txn = self._getFolderCopyTransaction(node, aContainer, index); } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) txn = self.ptm.createSeparator(-1, index); else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) txn = self._getBookmarkItemCopyTransaction(node, -1, index); NS_ASSERT(txn, "Unexpected item under a bookmarks folder"); if (txn) childItemsTransactions.push(txn); } return childItemsTransactions; } // tag folders use tag transactions if (aContainer == PlacesUtils.tagsFolderId) { var txns = []; if (aData.children) { aData.children.forEach(function(aChild) { txns.push(this.ptm.tagURI(PlacesUtils._uri(aChild.uri), [aData.title])); }, this); } return this.ptm.aggregateTransactions("addTags", txns); } else if (aData.livemark && aData.annos) { // Place is a Livemark Container return this._getLivemarkCopyTransaction(aData, aContainer, aIndex); } else { var childItems = getChildItemsTransactions(aData.children); if (aData.dateAdded) childItems.push(this.ptm.editItemDateAdded(null, aData.dateAdded)); if (aData.lastModified) childItems.push(this.ptm.editItemLastModified(null, aData.lastModified)); var annos = aData.annos || []; annos = annos.filter(function(aAnno) { // always exclude GUID when copying any item return aAnno.name != GUID_ANNO; }); return this.ptm.createFolder(aData.title, aContainer, aIndex, annos, childItems); } }, _getLivemarkCopyTransaction: function PU__getLivemarkCopyTransaction(aData, aContainer, aIndex) { NS_ASSERT(aData.livemark && aData.annos, "node is not a livemark"); // Place is a Livemark Container var feedURI = null; var siteURI = null; aData.annos = aData.annos.filter(function(aAnno) { if (aAnno.name == LMANNO_FEEDURI) { feedURI = PlacesUtils._uri(aAnno.value); return false; } else if (aAnno.name == LMANNO_SITEURI) { siteURI = PlacesUtils._uri(aAnno.value); return false; } // always exclude GUID when copying any item return aAnno.name != GUID_ANNO; }); return this.ptm.createLivemark(feedURI, siteURI, aData.title, aContainer, aIndex, aData.annos); }, /** * 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 PU_makeTransaction(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 this.ptm.moveItem(data.id, container, index); break; case PlacesUtils.TYPE_X_MOZ_PLACE: if (data.id == -1) // Not bookmarked. return this._getURIItemCopyTransaction(data, container, index); if (copy) return this._getBookmarkItemCopyTransaction(data, container, index); // Otherwise move the item. return this.ptm.moveItem(data.id, container, index); break; case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: // There is no data in a separator, so copying it just amounts to // inserting a new separator. if (copy) return this.ptm.createSeparator(container, index); // Otherwise move the item. return this.ptm.moveItem(data.id, container, index); break; default: if (type == PlacesUtils.TYPE_X_MOZ_URL || type == PlacesUtils.TYPE_UNICODE || type == TAB_DROP_TYPE) { var title = (type != PlacesUtils.TYPE_UNICODE) ? data.title : data.uri; return this.ptm.createItem(PlacesUtils._uri(data.uri), container, index, title); } } return null; }, /** * Methods to show the bookmarkProperties dialog in its various modes. * * The showMinimalAdd* methods open the dialog by its alternative URI. Thus * they persist the dialog dimensions separately from the showAdd* methods. * Note these variants also do not return the dialog "performed" state since * they may not open the dialog modally. */ /** * Shows the "Add Bookmark" dialog. * * @param [optional] aURI * An nsIURI object for which the "add bookmark" dialog is * to be shown. * @param [optional] aTitle * The default title for the new bookmark. * @param [optional] aDescription The default description for the new bookmark * @param [optional] aDefaultInsertionPoint * The default insertion point for the new item. If set, the folder * picker would be hidden unless aShowPicker is set to true, in which * case the dialog only uses the folder identifier from the insertion * point as the initially selected item in the folder picker. * @param [optional] aShowPicker * see above * @param [optional] aLoadInSidebar * If true, the dialog will default to load the new item in the * sidebar (as a web panel). * @param [optional] aKeyword * The default keyword for the new bookmark. The keyword field * will be shown in the dialog if this is used. * @param [optional] aPostData * POST data for POST-style keywords. * @param [optional] aCharSet * The character set for the bookmarked page. * @return true if any transaction has been performed. * * Notes: * - the location, description and "loadInSidebar" fields are * visible only if there is no initial URI (aURI is null). * - When aDefaultInsertionPoint is not set, the dialog defaults to the * bookmarks root folder. */ showAddBookmarkUI: function PU_showAddBookmarkUI(aURI, aTitle, aDescription, aDefaultInsertionPoint, aShowPicker, aLoadInSidebar, aKeyword, aPostData, aCharSet) { var info = { action: "add", type: "bookmark" }; if (aURI) info.uri = aURI; // allow default empty title if (typeof(aTitle) == "string") info.title = aTitle; if (aDescription) info.description = aDescription; if (aDefaultInsertionPoint) { info.defaultInsertionPoint = aDefaultInsertionPoint; if (!aShowPicker) info.hiddenRows = ["folderPicker"]; } if (aLoadInSidebar) info.loadBookmarkInSidebar = true; if (typeof(aKeyword) == "string") { info.keyword = aKeyword; if (typeof(aPostData) == "string") info.postData = aPostData; if (typeof(aCharSet) == "string") info.charSet = aCharSet; } return this._showBookmarkDialog(info); }, /** * @see showAddBookmarkUI * This opens the dialog with only the name and folder pickers visible by * default. * * You can still pass in the various paramaters as the default properties * for the new bookmark. * * The keyword field will be visible only if the aKeyword parameter * was used. */ showMinimalAddBookmarkUI: function PU_showMinimalAddBookmarkUI(aURI, aTitle, aDescription, aDefaultInsertionPoint, aShowPicker, aLoadInSidebar, aKeyword, aPostData, aCharSet) { var info = { action: "add", type: "bookmark", hiddenRows: ["description"] }; if (aURI) info.uri = aURI; // allow default empty title if (typeof(aTitle) == "string") info.title = aTitle; if (aDescription) info.description = aDescription; if (aDefaultInsertionPoint) { info.defaultInsertionPoint = aDefaultInsertionPoint; if (!aShowPicker) info.hiddenRows.push("folderPicker"); } if (aLoadInSidebar) info.loadBookmarkInSidebar = true; else info.hiddenRows = info.hiddenRows.concat(["location", "loadInSidebar"]); if (typeof(aKeyword) == "string") { info.keyword = aKeyword; // Hide the Tags field if we are adding a keyword. info.hiddenRows.push("tags"); // Keyword related params. if (typeof(aPostData) == "string") info.postData = aPostData; if (typeof(aCharSet) == "string") info.charSet = aCharSet; } else info.hiddenRows.push("keyword"); this._showBookmarkDialog(info, true); }, /** * Shows the "Add Live Bookmark" dialog. * * @param [optional] aFeedURI * The feed URI for which the dialog is to be shown (nsIURI). * @param [optional] aSiteURI * The site URI for the new live-bookmark (nsIURI). * @param [optional] aDefaultInsertionPoint * The default insertion point for the new item. If set, the folder * picker would be hidden unless aShowPicker is set to true, in which * case the dialog only uses the folder identifier from the insertion * point as the initially selected item in the folder picker. * @param [optional] aShowPicker * see above * @return true if any transaction has been performed. * * Notes: * - the feedURI and description fields are visible only if there is no * initial feed URI (aFeedURI is null). * - When aDefaultInsertionPoint is not set, the dialog defaults to the * bookmarks root folder. */ showAddLivemarkUI: function PU_showAddLivemarkURI(aFeedURI, aSiteURI, aTitle, aDescription, aDefaultInsertionPoint, aShowPicker) { var info = { action: "add", type: "livemark" }; if (aFeedURI) info.feedURI = aFeedURI; if (aSiteURI) info.siteURI = aSiteURI; // allow default empty title if (typeof(aTitle) == "string") info.title = aTitle; if (aDescription) info.description = aDescription; if (aDefaultInsertionPoint) { info.defaultInsertionPoint = aDefaultInsertionPoint; if (!aShowPicker) info.hiddenRows = ["folderPicker"]; } return this._showBookmarkDialog(info); }, /** * @see showAddLivemarkUI * This opens the dialog with only the name and folder pickers visible by * default. * * You can still pass in the various paramaters as the default properties * for the new live-bookmark. */ showMinimalAddLivemarkUI: function PU_showMinimalAddLivemarkURI(aFeedURI, aSiteURI, aTitle, aDescription, aDefaultInsertionPoint, aShowPicker) { var info = { action: "add", type: "livemark", hiddenRows: ["feedLocation", "siteLocation", "description"] }; if (aFeedURI) info.feedURI = aFeedURI; if (aSiteURI) info.siteURI = aSiteURI; // allow default empty title if (typeof(aTitle) == "string") info.title = aTitle; if (aDescription) info.description = aDescription; if (aDefaultInsertionPoint) { info.defaultInsertionPoint = aDefaultInsertionPoint; if (!aShowPicker) info.hiddenRows.push("folderPicker"); } this._showBookmarkDialog(info, true); }, /** * Show an "Add Bookmarks" dialog to allow the adding of a folder full * of bookmarks corresponding to the objects in the uriList. This will * be called most often as the result of a "Bookmark All Tabs..." command. * * @param aURIList List of nsIURI objects representing the locations * to be bookmarked. * @return true if any transaction has been performed. */ showMinimalAddMultiBookmarkUI: function PU_showAddMultiBookmarkUI(aURIList) { NS_ASSERT(aURIList.length, "showAddMultiBookmarkUI expects a list of nsIURI objects"); var info = { action: "add", type: "folder", hiddenRows: ["description"], URIList: aURIList }; this._showBookmarkDialog(info, true); }, /** * Opens the properties dialog for a given item identifier. * * @param aItemId * item identifier for which the properties are to be shown * @param aType * item type, either "bookmark" or "folder" * @param [optional] aReadOnly * states if properties dialog should be readonly * @return true if any transaction has been performed. */ showItemProperties: function PU_showItemProperties(aItemId, aType, aReadOnly) { var info = { action: "edit", type: aType, itemId: aItemId, readOnly: aReadOnly }; return this._showBookmarkDialog(info); }, /** * Shows the "New Folder" dialog. * * @param [optional] aTitle * The default title for the new bookmark. * @param [optional] aDefaultInsertionPoint * The default insertion point for the new item. If set, the folder * picker would be hidden unless aShowPicker is set to true, in which * case the dialog only uses the folder identifier from the insertion * point as the initially selected item in the folder picker. * @param [optional] aShowPicker * see above * @return true if any transaction has been performed. */ showAddFolderUI: function PU_showAddFolderUI(aTitle, aDefaultInsertionPoint, aShowPicker) { var info = { action: "add", type: "folder", hiddenRows: [] }; // allow default empty title if (typeof(aTitle) == "string") info.title = aTitle; if (aDefaultInsertionPoint) { info.defaultInsertionPoint = aDefaultInsertionPoint; if (!aShowPicker) info.hiddenRows.push("folderPicker"); } return this._showBookmarkDialog(info); }, /** * 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 aMinimalUI * [optional] if true, the dialog is opened by its alternative * chrome: uri. * * @return true if any transaction has been performed, false otherwise. */ _showBookmarkDialog: function PU__showBookmarkDialog(aInfo, aMinimalUI) { var dialogURL = aMinimalUI ? "chrome://browser/content/places/bookmarkProperties2.xul" : "chrome://browser/content/places/bookmarkProperties.xul"; var features; if (aMinimalUI) features = "centerscreen,chrome,dialog,resizable,modal"; else features = "centerscreen,chrome,modal,resizable=no"; window.openDialog(dialogURL, "", features, aInfo); return ("performed" in aInfo && aInfo.performed); }, /** * 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 PU_getViewForNode(aNode) { var node = aNode; // the view for a of which its associated menupopup is a places view, // is the menupopup if (node.localName == "menu" && !node.node && node.firstChild.getAttribute("type") == "places") return node.firstChild; while (node) { // XXXmano: Use QueryInterface(nsIPlacesView) once we implement it... if (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 PU_markPageAsTyped(aURL) { PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory) .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 PU_markPageAsFollowedBookmark(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 PU_markPageAsUserClicked(aURL) { PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory) .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 * @return true if it's safe to open the node in the browser, false otherwise. * */ checkURLSecurity: function PU_checkURLSecurity(aURINode) { if (!PlacesUtils.nodeIsBookmark(aURINode)) { 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 promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]. getService(Ci.nsIPromptService); var errorStr = this.getString("load-js-data-url-error"); promptService.alert(window, 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 PU_getDescriptionFromDocument(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 PU_getItemDescription(aItemId) { if (PlacesUtils.annotations.itemHasAnnotation(aItemId, DESCRIPTION_ANNO)) return PlacesUtils.annotations.getItemAnnotation(aItemId, DESCRIPTION_ANNO); return ""; }, /** * Gives the user a chance to cancel loading lots of tabs at once */ _confirmOpenInTabs: function PU__confirmOpenInTabs(numTabsToOpen) { var pref = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); const kWarnOnOpenPref = "browser.tabs.warnOnOpen"; var reallyOpen = true; if (pref.getBoolPref(kWarnOnOpenPref)) { if (numTabsToOpen >= pref.getIntPref("browser.tabs.maxOpenBeforeWarn")) { var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]. getService(Ci.nsIPromptService); // 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 = promptService.confirmEx(window, this.getString("tabs.openWarningTitle"), this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]), (promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_0) + (promptService.BUTTON_TITLE_CANCEL * promptService.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) pref.setBoolPref(kWarnOnOpenPref, false); } } return reallyOpen; }, /** aItemsToOpen needs to be an array of objects of the form: * {uri: string, isBookmark: boolean} */ _openTabset: function PU__openTabset(aItemsToOpen, aEvent) { if (!aItemsToOpen.length) return; var urls = []; for (var i = 0; i < aItemsToOpen.length; i++) { var item = aItemsToOpen[i]; if (item.isBookmark) this.markPageAsFollowedBookmark(item.uri); else this.markPageAsTyped(item.uri); urls.push(item.uri); } var browserWindow = getTopWin(); var where = browserWindow ? whereToOpenLink(aEvent, false, true) : "window"; if (where == "window") { window.openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no", urls.join("|")); return; } var loadInBackground = where == "tabshifted" ? true : false; var replaceCurrentTab = where == "tab" ? false : true; browserWindow.gBrowser.loadTabs(urls, loadInBackground, replaceCurrentTab); }, openContainerNodeInTabs: function PU_openContainerInTabs(aNode, aEvent) { var urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode); if (!this._confirmOpenInTabs(urlsToOpen.length)) return; this._openTabset(urlsToOpen, aEvent); }, openURINodesInTabs: function PU_openURINodesInTabs(aNodes, aEvent) { var 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])}); } this._openTabset(urlsToOpen, aEvent); }, /** * 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. */ openNodeWithEvent: function PU_openNodeWithEvent(aNode, aEvent) { this.openNodeIn(aNode, whereToOpenLink(aEvent)); }, /** * Loads the node's URL in the appropriate tab or window or as a * web panel. * see also openUILinkIn */ openNodeIn: function PU_openNodeIn(aNode, aWhere) { if (aNode && PlacesUtils.nodeIsURI(aNode) && this.checkURLSecurity(aNode)) { var isBookmark = PlacesUtils.nodeIsBookmark(aNode); 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, LOAD_IN_SIDEBAR_ANNO)) { var w = getTopWin(); if (w) { w.openWebPanel(aNode.title, aNode.uri); return; } } } openUILinkIn(aNode.uri, aWhere); } }, /** * 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 PUU_guessUrlSchemeForUI(aUrlString) { return aUrlString.substr(0, aUrlString.indexOf(":")); }, /** * Helper for the toolbar and menu views */ createMenuItemForNode: function PUU_createMenuItemForNode(aNode) { var element; var type = aNode.type; if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) element = document.createElement("menuseparator"); else { if (PlacesUtils.uriTypes.indexOf(type) != -1) { element = document.createElement("menuitem"); element.className = "menuitem-iconic bookmark-item"; element.setAttribute("scheme", this.guessUrlSchemeForUI(aNode.uri)); } else if (PlacesUtils.containerTypes.indexOf(type) != -1) { element = document.createElement("menu"); element.setAttribute("container", "true"); if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) { element.setAttribute("query", "true"); if (PlacesUtils.nodeIsTagQuery(aNode)) element.setAttribute("tagContainer", "true"); else if (PlacesUtils.nodeIsDay(aNode)) element.setAttribute("dayContainer", "true"); else if (PlacesUtils.nodeIsHost(aNode)) element.setAttribute("hostContainer", "true"); } else if (aNode.itemId != -1) { if (PlacesUtils.nodeIsLivemarkContainer(aNode)) element.setAttribute("livemark", "true"); } var popup = document.createElement("menupopup"); popup.setAttribute("placespopup", "true"); popup._resultNode = asContainer(aNode); #ifdef XP_MACOSX // Binding on Mac native menus is lazy attached, so onPopupShowing, // in the capturing phase, fields are not yet initialized. // In that phase we have to ensure markers are not undefined to build // the popup correctly. popup._startMarker = -1; popup._endMarker = -1; #else // no context menu on mac popup.setAttribute("context", "placesContext"); #endif element.appendChild(popup); element.className = "menu-iconic bookmark-item"; } else throw "Unexpected node"; element.setAttribute("label", this.getBestTitle(aNode)); var icon = aNode.icon; if (icon) element.setAttribute("image", icon); } element.node = aNode; element.node._DOMElement = element; return element; }, cleanPlacesPopup: function PU_cleanPlacesPopup(aPopup) { // Remove places popup children and update markers to keep track of // their indices. var start = aPopup._startMarker != -1 ? aPopup._startMarker + 1 : 0; var end = aPopup._endMarker != -1 ? aPopup._endMarker : aPopup.childNodes.length; var items = []; var placesNodeFound = false; for (var i = start; i < end; ++i) { var item = aPopup.childNodes[i]; if (item.getAttribute("builder") == "end") { // we need to do this for menus that have static content at the end but // are initially empty, eg. the history menu, we need to know where to // start inserting new items. aPopup._endMarker = i; break; } if (item.node) { items.push(item); placesNodeFound = true; } else { // This is static content... if (!placesNodeFound) // ...at the start of the popup // Initialized in menu.xml, in the base binding aPopup._startMarker++; else { // ...after places nodes aPopup._endMarker = i; break; } } } for (var i = 0; i < items.length; ++i) { aPopup.removeChild(items[i]); if (aPopup._endMarker != -1) aPopup._endMarker--; } }, getBestTitle: function PU_getBestTitle(aNode) { var title; if (!aNode.title && PlacesUtils.uriTypes.indexOf(aNode.type) != -1) { // 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 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") }, "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 = 6; // Removes an item and associated annotations, ignoring eventual errors. function safeRemoveItem(aItemId) { try { if (as.itemHasAnnotation(aItemId, ORGANIZER_QUERY_ANNO) && !(as.getItemAnnotation(aItemId, 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, ORGANIZER_FOLDER_ANNO); as.removeItemAnnotation(aItemId, 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(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, ORGANIZER_FOLDER_ANNO); if (version != 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(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], 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 self = this; var callback = { // Helper to create an organizer special query. create_query: function CB_create_query(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, ORGANIZER_QUERY_ANNO, aQueryName, 0, as.EXPIRE_NEVER); // We should never backup this, since it changes between profiles. as.setItemAnnotation(itemId, EXCLUDE_FROM_BACKUP_ANNO, 1, 0, as.EXPIRE_NEVER); // Add to the queries map. self.leftPaneQueries[aQueryName] = itemId; return itemId; }, // Helper to create an organizer special folder. create_folder: function CB_create_folder(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, EXCLUDE_FROM_BACKUP_ANNO, 1, 0, as.EXPIRE_NEVER); // Disallow manipulating this folder within the organizer UI. bs.setFolderReadonly(folderId, true); if (aIsRoot) { // Mark as special left pane root. as.setItemAnnotation(folderId, ORGANIZER_FOLDER_ANNO, ORGANIZER_LEFTPANE_VERSION, 0, as.EXPIRE_NEVER); } else { // Mark as special organizer folder. as.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO, aFolderName, 0, as.EXPIRE_NEVER); self.leftPaneQueries[aFolderName] = folderId; } return folderId; }, runBatched: function CB_runBatched(aUserData) { delete self.leftPaneQueries; self.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); // XXX: Downloads. // 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); 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 PU_getLeftPaneQueryNameFromId(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 (this.__lookupGetter__("leftPaneFolderId")) { try { queryName = PlacesUtils.annotations. getItemAnnotation(aItemId, 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; }, /** * Add, update or remove the livemark status menuitem. * @param aPopup * The livemark container popup */ ensureLivemarkStatusMenuItem: function PU_ensureLivemarkStatusMenuItem(aPopup) { var itemId = aPopup._resultNode.itemId; var lmStatus = null; if (PlacesUtils.annotations .itemHasAnnotation(itemId, "livemark/loadfailed")) lmStatus = "bookmarksLivemarkFailed"; else if (PlacesUtils.annotations .itemHasAnnotation(itemId, "livemark/loading")) lmStatus = "bookmarksLivemarkLoading"; if (lmStatus && !aPopup._lmStatusMenuItem) { // Create the status menuitem and cache it in the popup object. aPopup._lmStatusMenuItem = document.createElement("menuitem"); aPopup._lmStatusMenuItem.setAttribute("lmStatus", lmStatus); aPopup._lmStatusMenuItem.setAttribute("label", this.getString(lmStatus)); aPopup._lmStatusMenuItem.setAttribute("disabled", true); aPopup.insertBefore(aPopup._lmStatusMenuItem, aPopup.childNodes.item(aPopup._startMarker + 1)); aPopup._startMarker++; } else if (lmStatus && aPopup._lmStatusMenuItem.getAttribute("lmStatus") != lmStatus) { // Status has changed, update the cached status menuitem. aPopup._lmStatusMenuItem.setAttribute("label", this.getString(lmStatus)); } else if (!lmStatus && aPopup._lmStatusMenuItem){ // No status, remove the cached menuitem. aPopup.removeChild(aPopup._lmStatusMenuItem); aPopup._lmStatusMenuItem = null; aPopup._startMarker--; } } };