diff -r 000000000000 -r 6474c204b198 toolkit/components/places/PlacesUtils.jsm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/components/places/PlacesUtils.jsm Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,3000 @@
+/* -*- Mode: C++; tab-width: 2; 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 = [
+ "PlacesUtils"
+, "PlacesAggregatedTransaction"
+, "PlacesCreateFolderTransaction"
+, "PlacesCreateBookmarkTransaction"
+, "PlacesCreateSeparatorTransaction"
+, "PlacesCreateLivemarkTransaction"
+, "PlacesMoveItemTransaction"
+, "PlacesRemoveItemTransaction"
+, "PlacesEditItemTitleTransaction"
+, "PlacesEditBookmarkURITransaction"
+, "PlacesSetItemAnnotationTransaction"
+, "PlacesSetPageAnnotationTransaction"
+, "PlacesEditBookmarkKeywordTransaction"
+, "PlacesEditBookmarkPostDataTransaction"
+, "PlacesEditItemDateAddedTransaction"
+, "PlacesEditItemLastModifiedTransaction"
+, "PlacesSortFolderByNameTransaction"
+, "PlacesTagURITransaction"
+, "PlacesUntagURITransaction"
+];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
+ "resource://gre/modules/BookmarkJSONUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+
+// The minimum amount of transactions before starting a batch. Usually we do
+// do incremental updates, a batch will cause views to completely
+// refresh instead.
+const MIN_TRANSACTIONS_FOR_BATCH = 5;
+
+#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) {
+ var result = null;
+ try {
+ result = aNode.QueryInterface(aIID);
+ }
+ catch (e) {
+ }
+ return result;
+}
+function asContainer(aNode) QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
+function asQuery(aNode) QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
+
+this.PlacesUtils = {
+ // Place entries that are containers, e.g. bookmark folders or queries.
+ TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
+ // Place entries that are bookmark separators.
+ TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
+ // Place entries that are not containers or separators
+ TYPE_X_MOZ_PLACE: "text/x-moz-place",
+ // Place entries in shortcut url format (url\ntitle)
+ TYPE_X_MOZ_URL: "text/x-moz-url",
+ // Place entries formatted as HTML anchors
+ TYPE_HTML: "text/html",
+ // Place entries as raw URL text
+ TYPE_UNICODE: "text/unicode",
+ // Used to track the action that populated the clipboard.
+ TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
+
+ EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
+ LMANNO_FEEDURI: "livemark/feedURI",
+ LMANNO_SITEURI: "livemark/siteURI",
+ POST_DATA_ANNO: "bookmarkProperties/POSTData",
+ READ_ONLY_ANNO: "placesInternal/READ_ONLY",
+ CHARSET_ANNO: "URIProperties/characterSet",
+
+ TOPIC_SHUTDOWN: "places-shutdown",
+ TOPIC_INIT_COMPLETE: "places-init-complete",
+ TOPIC_DATABASE_LOCKED: "places-database-locked",
+ TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
+ TOPIC_FEEDBACK_UPDATED: "places-autocomplete-feedback-updated",
+ TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
+ TOPIC_VACUUM_STARTING: "places-vacuum-starting",
+ TOPIC_BOOKMARKS_RESTORE_BEGIN: "bookmarks-restore-begin",
+ TOPIC_BOOKMARKS_RESTORE_SUCCESS: "bookmarks-restore-success",
+ TOPIC_BOOKMARKS_RESTORE_FAILED: "bookmarks-restore-failed",
+
+ asContainer: function(aNode) asContainer(aNode),
+ asQuery: function(aNode) asQuery(aNode),
+
+ endl: NEWLINE,
+
+ /**
+ * Makes a URI from a spec.
+ * @param aSpec
+ * The string spec of the URI
+ * @returns A URI object for the spec.
+ */
+ _uri: function PU__uri(aSpec) {
+ return NetUtil.newURI(aSpec);
+ },
+
+ /**
+ * Wraps a string in a nsISupportsString wrapper.
+ * @param aString
+ * The string to wrap.
+ * @returns A nsISupportsString object containing a string.
+ */
+ toISupportsString: function PU_toISupportsString(aString) {
+ let s = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ s.data = aString;
+ return s;
+ },
+
+ getFormattedString: function PU_getFormattedString(key, params) {
+ return bundle.formatStringFromName(key, params, params.length);
+ },
+
+ getString: function PU_getString(key) {
+ return bundle.GetStringFromName(key);
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a Bookmark folder.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a Bookmark folder, false otherwise
+ */
+ nodeIsFolder: function PU_nodeIsFolder(aNode) {
+ return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
+ aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
+ },
+
+ /**
+ * Determines whether or not a ResultNode represents a bookmarked URI.
+ * @param aNode
+ * A result node
+ * @returns true if the node represents a bookmarked URI, false otherwise
+ */
+ nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
+ aNode.itemId != -1;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a Bookmark separator.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a Bookmark separator, false otherwise
+ */
+ nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a URL item.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a URL item, false otherwise
+ */
+ nodeIsURI: function PU_nodeIsURI(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a Query item.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a Query item, false otherwise
+ */
+ nodeIsQuery: function PU_nodeIsQuery(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+ },
+
+ /**
+ * Generator for a node's ancestors.
+ * @param aNode
+ * A result node
+ */
+ nodeAncestors: function PU_nodeAncestors(aNode) {
+ let node = aNode.parent;
+ while (node) {
+ yield node;
+ node = node.parent;
+ }
+ },
+
+ /**
+ * Cache array of read-only item IDs.
+ *
+ * The first time this property is called:
+ * - the cache is filled with all ids with the RO annotation
+ * - an annotation observer is added
+ * - a shutdown observer is added
+ *
+ * When the annotation observer detects annotations added or
+ * removed that are the RO annotation name, it adds/removes
+ * the ids from the cache.
+ *
+ * At shutdown, the annotation and shutdown observers are removed.
+ */
+ get _readOnly() {
+ // Add annotations observer.
+ this.annotations.addObserver(this, false);
+ this.registerShutdownFunction(function () {
+ this.annotations.removeObserver(this);
+ });
+
+ var readOnly = this.annotations.getItemsWithAnnotation(this.READ_ONLY_ANNO);
+ this.__defineGetter__("_readOnly", function() readOnly);
+ return this._readOnly;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAnnotationObserver
+ , Ci.nsIObserver
+ , Ci.nsITransactionListener
+ ]),
+
+ _shutdownFunctions: [],
+ registerShutdownFunction: function PU_registerShutdownFunction(aFunc)
+ {
+ // If this is the first registered function, add the shutdown observer.
+ if (this._shutdownFunctions.length == 0) {
+ Services.obs.addObserver(this, this.TOPIC_SHUTDOWN, false);
+ }
+ this._shutdownFunctions.push(aFunc);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+ observe: function PU_observe(aSubject, aTopic, aData)
+ {
+ switch (aTopic) {
+ case this.TOPIC_SHUTDOWN:
+ Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
+ while (this._shutdownFunctions.length > 0) {
+ this._shutdownFunctions.shift().apply(this);
+ }
+ if (this._bookmarksServiceObserversQueue.length > 0) {
+ // Since we are shutting down, there's no reason to add the observers.
+ this._bookmarksServiceObserversQueue.length = 0;
+ }
+ break;
+ case "bookmarks-service-ready":
+ this._bookmarksServiceReady = true;
+ while (this._bookmarksServiceObserversQueue.length > 0) {
+ let observerInfo = this._bookmarksServiceObserversQueue.shift();
+ this.bookmarks.addObserver(observerInfo.observer, observerInfo.weak);
+ }
+ break;
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIAnnotationObserver
+
+ onItemAnnotationSet: function PU_onItemAnnotationSet(aItemId, aAnnotationName)
+ {
+ if (aAnnotationName == this.READ_ONLY_ANNO &&
+ this._readOnly.indexOf(aItemId) == -1)
+ this._readOnly.push(aItemId);
+ },
+
+ onItemAnnotationRemoved:
+ function PU_onItemAnnotationRemoved(aItemId, aAnnotationName)
+ {
+ var index = this._readOnly.indexOf(aItemId);
+ if (aAnnotationName == this.READ_ONLY_ANNO && index > -1)
+ delete this._readOnly[index];
+ },
+
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsITransactionListener
+
+ didDo: function PU_didDo(aManager, aTransaction, aDoResult)
+ {
+ updateCommandsOnActiveWindow();
+ },
+
+ didUndo: function PU_didUndo(aManager, aTransaction, aUndoResult)
+ {
+ updateCommandsOnActiveWindow();
+ },
+
+ didRedo: function PU_didRedo(aManager, aTransaction, aRedoResult)
+ {
+ updateCommandsOnActiveWindow();
+ },
+
+ didBeginBatch: function PU_didBeginBatch(aManager, aResult)
+ {
+ // A no-op transaction is pushed to the stack, in order to make safe and
+ // easy to implement "Undo" an unknown number of transactions (including 0),
+ // "above" beginBatch and endBatch. Otherwise,implementing Undo that way
+ // head to dataloss: for example, if no changes were done in the
+ // edit-item panel, the last transaction on the undo stack would be the
+ // initial createItem transaction, or even worse, the batched editing of
+ // some other item.
+ // DO NOT MOVE this to the window scope, that would leak (bug 490068)!
+ this.transactionManager.doTransaction({ doTransaction: function() {},
+ undoTransaction: function() {},
+ redoTransaction: function() {},
+ isTransient: false,
+ merge: function() { return false; }
+ });
+ },
+
+ willDo: function PU_willDo() {},
+ willUndo: function PU_willUndo() {},
+ willRedo: function PU_willRedo() {},
+ willBeginBatch: function PU_willBeginBatch() {},
+ willEndBatch: function PU_willEndBatch() {},
+ didEndBatch: function PU_didEndBatch() {},
+ willMerge: function PU_willMerge() {},
+ didMerge: function PU_didMerge() {},
+
+
+ /**
+ * Determines if a node is read only (children cannot be inserted, sometimes
+ * they cannot be removed depending on the circumstance)
+ * @param aNode
+ * A result node
+ * @returns true if the node is readonly, false otherwise
+ */
+ nodeIsReadOnly: function PU_nodeIsReadOnly(aNode) {
+ let itemId = aNode.itemId;
+ if (itemId != -1) {
+ return this._readOnly.indexOf(itemId) != -1;
+ }
+
+ if (this.nodeIsQuery(aNode) &&
+ asQuery(aNode).queryOptions.resultType !=
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS)
+ return aNode.childrenReadOnly;
+ return false;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a host container.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a host container, false otherwise
+ */
+ nodeIsHost: function PU_nodeIsHost(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
+ aNode.parent &&
+ asQuery(aNode.parent).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a day container.
+ * @param node
+ * A NavHistoryResultNode
+ * @returns true if the node is a day container, false otherwise
+ */
+ nodeIsDay: function PU_nodeIsDay(aNode) {
+ var resultType;
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
+ aNode.parent &&
+ ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
+ },
+
+ /**
+ * Determines whether or not a result-node is a tag container.
+ * @param aNode
+ * A result-node
+ * @returns true if the node is a tag container, false otherwise
+ */
+ nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
+ asQuery(aNode).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a container.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a container item, false otherwise
+ */
+ containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY],
+ nodeIsContainer: function PU_nodeIsContainer(aNode) {
+ return this.containerTypes.indexOf(aNode.type) != -1;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is an history related container.
+ * @param node
+ * A result node
+ * @returns true if the node is an history related container, false otherwise
+ */
+ nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
+ var resultType;
+ return this.nodeIsQuery(aNode) &&
+ ((resultType = asQuery(aNode).queryOptions.resultType) ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
+ resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
+ this.nodeIsDay(aNode) ||
+ this.nodeIsHost(aNode));
+ },
+
+ /**
+ * Determines whether or not a node is a readonly folder.
+ * @param aNode
+ * The node to test.
+ * @returns true if the node is a readonly folder.
+ */
+ isReadonlyFolder: function(aNode) {
+ return this.nodeIsFolder(aNode) &&
+ this._readOnly.indexOf(asQuery(aNode).folderItemId) != -1;
+ },
+
+ /**
+ * Gets the concrete item-id for the given node. Generally, this is just
+ * node.itemId, but for folder-shortcuts that's node.folderItemId.
+ */
+ getConcreteItemId: function PU_getConcreteItemId(aNode) {
+ if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
+ return asQuery(aNode).folderItemId;
+ else if (PlacesUtils.nodeIsTagQuery(aNode)) {
+ // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
+ // so we can still get the concrete itemId for them.
+ var queries = aNode.getQueries();
+ var folders = queries[0].getFolders();
+ return folders[0];
+ }
+ return aNode.itemId;
+ },
+
+ /**
+ * String-wraps a result node according to the rules of the specified
+ * content type.
+ * @param aNode
+ * The Result node to wrap (serialize)
+ * @param aType
+ * The content type to serialize as
+ * @param [optional] aOverrideURI
+ * Used instead of the node's URI if provided.
+ * This is useful for wrapping a container as TYPE_X_MOZ_URL,
+ * TYPE_HTML or TYPE_UNICODE.
+ * @return A string serialization of the node
+ */
+ wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI) {
+ // when wrapping a node, we want all the items, even if the original
+ // query options are excluding them.
+ // this can happen when copying from the left hand pane of the bookmarks
+ // organizer
+ // @return [node, shouldClose]
+ function convertNode(cNode) {
+ if (PlacesUtils.nodeIsFolder(cNode) &&
+ cNode.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT &&
+ asQuery(cNode).queryOptions.excludeItems) {
+ return [PlacesUtils.getFolderContents(cNode.itemId, false, true).root, true];
+ }
+
+ // If we didn't create our own query, do not alter the node's open state.
+ return [cNode, false];
+ }
+
+ function gatherLivemarkUrl(aNode) {
+ try {
+ return PlacesUtils.annotations
+ .getItemAnnotation(aNode.itemId,
+ PlacesUtils.LMANNO_SITEURI);
+ } catch (ex) {
+ return PlacesUtils.annotations
+ .getItemAnnotation(aNode.itemId,
+ PlacesUtils.LMANNO_FEEDURI);
+ }
+ }
+
+ function isLivemark(aNode) {
+ return PlacesUtils.nodeIsFolder(aNode) &&
+ PlacesUtils.annotations
+ .itemHasAnnotation(aNode.itemId,
+ PlacesUtils.LMANNO_FEEDURI);
+ }
+
+ switch (aType) {
+ case this.TYPE_X_MOZ_PLACE:
+ case this.TYPE_X_MOZ_PLACE_SEPARATOR:
+ case this.TYPE_X_MOZ_PLACE_CONTAINER: {
+ let writer = {
+ value: "",
+ write: function PU_wrapNode__write(aStr, aLen) {
+ this.value += aStr;
+ }
+ };
+
+ let [node, shouldClose] = convertNode(aNode);
+ this._serializeNodeAsJSONToOutputStream(node, writer);
+ if (shouldClose)
+ node.containerOpen = false;
+
+ return writer.value;
+ }
+ case this.TYPE_X_MOZ_URL: {
+ function gatherDataUrl(bNode) {
+ if (isLivemark(bNode)) {
+ return gatherLivemarkUrl(bNode) + NEWLINE + bNode.title;
+ }
+
+ if (PlacesUtils.nodeIsURI(bNode))
+ return (aOverrideURI || bNode.uri) + NEWLINE + bNode.title;
+ // ignore containers and separators - items without valid URIs
+ return "";
+ }
+
+ let [node, shouldClose] = convertNode(aNode);
+ let dataUrl = gatherDataUrl(node);
+ if (shouldClose)
+ node.containerOpen = false;
+
+ return dataUrl;
+ }
+ case this.TYPE_HTML: {
+ function gatherDataHtml(bNode) {
+ function htmlEscape(s) {
+ s = s.replace(/&/g, "&");
+ s = s.replace(/>/g, ">");
+ s = s.replace(/" + escapedTitle + "" + NEWLINE;
+ }
+
+ if (PlacesUtils.nodeIsContainer(bNode)) {
+ asContainer(bNode);
+ let wasOpen = bNode.containerOpen;
+ if (!wasOpen)
+ bNode.containerOpen = true;
+
+ let childString = "
- " + escapedTitle + "
" + NEWLINE;
+ let cc = bNode.childCount;
+ for (let i = 0; i < cc; ++i)
+ childString += "- "
+ + NEWLINE
+ + gatherDataHtml(bNode.getChild(i))
+ + "
"
+ + NEWLINE;
+ bNode.containerOpen = wasOpen;
+ return childString + "
" + NEWLINE;
+ }
+ if (PlacesUtils.nodeIsURI(bNode))
+ return "" + escapedTitle + "" + NEWLINE;
+ if (PlacesUtils.nodeIsSeparator(bNode))
+ return "
" + NEWLINE;
+ return "";
+ }
+
+ let [node, shouldClose] = convertNode(aNode);
+ let dataHtml = gatherDataHtml(node);
+ if (shouldClose)
+ node.containerOpen = false;
+
+ return dataHtml;
+ }
+ }
+
+ // Otherwise, we wrap as TYPE_UNICODE.
+ function gatherDataText(bNode) {
+ if (isLivemark(bNode)) {
+ return gatherLivemarkUrl(bNode);
+ }
+
+ if (PlacesUtils.nodeIsContainer(bNode)) {
+ asContainer(bNode);
+ let wasOpen = bNode.containerOpen;
+ if (!wasOpen)
+ bNode.containerOpen = true;
+
+ let childString = bNode.title + NEWLINE;
+ let cc = bNode.childCount;
+ for (let i = 0; i < cc; ++i) {
+ let child = bNode.getChild(i);
+ let suffix = i < (cc - 1) ? NEWLINE : "";
+ childString += gatherDataText(child) + suffix;
+ }
+ bNode.containerOpen = wasOpen;
+ return childString;
+ }
+ if (PlacesUtils.nodeIsURI(bNode))
+ return (aOverrideURI || bNode.uri);
+ if (PlacesUtils.nodeIsSeparator(bNode))
+ return "--------------------";
+ return "";
+ }
+
+ let [node, shouldClose] = convertNode(aNode);
+ let dataText = gatherDataText(node);
+ // Convert node could pass an open container node.
+ if (shouldClose)
+ node.containerOpen = false;
+
+ return dataText;
+ },
+
+ /**
+ * Unwraps data from the Clipboard or the current Drag Session.
+ * @param blob
+ * A blob (string) of data, in some format we potentially know how
+ * to parse.
+ * @param type
+ * The content type of the blob.
+ * @returns An array of objects representing each item contained by the source.
+ */
+ unwrapNodes: function PU_unwrapNodes(blob, type) {
+ // We split on "\n" because the transferable system converts "\r\n" to "\n"
+ var nodes = [];
+ switch(type) {
+ case this.TYPE_X_MOZ_PLACE:
+ case this.TYPE_X_MOZ_PLACE_SEPARATOR:
+ case this.TYPE_X_MOZ_PLACE_CONTAINER:
+ nodes = JSON.parse("[" + blob + "]");
+ break;
+ case this.TYPE_X_MOZ_URL:
+ var parts = blob.split("\n");
+ // data in this type has 2 parts per entry, so if there are fewer
+ // than 2 parts left, the blob is malformed and we should stop
+ // but drag and drop of files from the shell has parts.length = 1
+ if (parts.length != 1 && parts.length % 2)
+ break;
+ for (var i = 0; i < parts.length; i=i+2) {
+ var uriString = parts[i];
+ var titleString = "";
+ if (parts.length > i+1)
+ titleString = parts[i+1];
+ else {
+ // for drag and drop of files, try to use the leafName as title
+ try {
+ titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
+ .fileName;
+ }
+ catch (e) {}
+ }
+ // note: this._uri() will throw if uriString is not a valid URI
+ if (this._uri(uriString)) {
+ nodes.push({ uri: uriString,
+ title: titleString ? titleString : uriString ,
+ type: this.TYPE_X_MOZ_URL });
+ }
+ }
+ break;
+ case this.TYPE_UNICODE:
+ var parts = blob.split("\n");
+ for (var i = 0; i < parts.length; i++) {
+ var uriString = parts[i];
+ // text/uri-list is converted to TYPE_UNICODE but it could contain
+ // comments line prepended by #, we should skip them
+ if (uriString.substr(0, 1) == '\x23')
+ continue;
+ // note: this._uri() will throw if uriString is not a valid URI
+ if (uriString != "" && this._uri(uriString))
+ nodes.push({ uri: uriString,
+ title: uriString,
+ type: this.TYPE_X_MOZ_URL });
+ }
+ break;
+ default:
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ return nodes;
+ },
+
+ /**
+ * Generates a nsINavHistoryResult for the contents of a folder.
+ * @param folderId
+ * The folder to open
+ * @param [optional] excludeItems
+ * True to hide all items (individual bookmarks). This is used on
+ * the left places pane so you just get a folder hierarchy.
+ * @param [optional] expandQueries
+ * True to make query items expand as new containers. For managing,
+ * you want this to be false, for menus and such, you want this to
+ * be true.
+ * @returns A nsINavHistoryResult containing the contents of the
+ * folder. The result.root is guaranteed to be open.
+ */
+ getFolderContents:
+ function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
+ var query = this.history.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ var options = this.history.getNewQueryOptions();
+ options.excludeItems = aExcludeItems;
+ options.expandQueries = aExpandQueries;
+
+ var result = this.history.executeQuery(query, options);
+ result.root.containerOpen = true;
+ return result;
+ },
+
+ /**
+ * Fetch all annotations for a URI, including all properties of each
+ * annotation which would be required to recreate it.
+ * @param aURI
+ * The URI for which annotations are to be retrieved.
+ * @return Array of objects, each containing the following properties:
+ * name, flags, expires, value
+ */
+ getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
+ var annosvc = this.annotations;
+ var annos = [], val = null;
+ var annoNames = annosvc.getPageAnnotationNames(aURI);
+ for (var i = 0; i < annoNames.length; i++) {
+ var flags = {}, exp = {}, storageType = {};
+ annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, storageType);
+ val = annosvc.getPageAnnotation(aURI, annoNames[i]);
+ annos.push({name: annoNames[i],
+ flags: flags.value,
+ expires: exp.value,
+ value: val});
+ }
+ return annos;
+ },
+
+ /**
+ * Fetch all annotations for an item, including all properties of each
+ * annotation which would be required to recreate it.
+ * @param aItemId
+ * The identifier of the itme for which annotations are to be
+ * retrieved.
+ * @return Array of objects, each containing the following properties:
+ * name, flags, expires, mimeType, type, value
+ */
+ getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
+ var annosvc = this.annotations;
+ var annos = [], val = null;
+ var annoNames = annosvc.getItemAnnotationNames(aItemId);
+ for (var i = 0; i < annoNames.length; i++) {
+ var flags = {}, exp = {}, storageType = {};
+ annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, storageType);
+ val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
+ annos.push({name: annoNames[i],
+ flags: flags.value,
+ expires: exp.value,
+ value: val});
+ }
+ return annos;
+ },
+
+ /**
+ * Annotate a URI with a batch of annotations.
+ * @param aURI
+ * The URI for which annotations are to be set.
+ * @param aAnnotations
+ * Array of objects, each containing the following properties:
+ * name, flags, expires.
+ * If the value for an annotation is not set it will be removed.
+ */
+ setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
+ var annosvc = this.annotations;
+ aAnnos.forEach(function(anno) {
+ if (anno.value === undefined || anno.value === null) {
+ annosvc.removePageAnnotation(aURI, anno.name);
+ }
+ else {
+ let flags = ("flags" in anno) ? anno.flags : 0;
+ let expires = ("expires" in anno) ?
+ anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
+ annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
+ }
+ });
+ },
+
+ /**
+ * Annotate an item with a batch of annotations.
+ * @param aItemId
+ * The identifier of the item for which annotations are to be set
+ * @param aAnnotations
+ * Array of objects, each containing the following properties:
+ * name, flags, expires.
+ * If the value for an annotation is not set it will be removed.
+ */
+ setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos) {
+ var annosvc = this.annotations;
+
+ aAnnos.forEach(function(anno) {
+ if (anno.value === undefined || anno.value === null) {
+ annosvc.removeItemAnnotation(aItemId, anno.name);
+ }
+ else {
+ let flags = ("flags" in anno) ? anno.flags : 0;
+ let expires = ("expires" in anno) ?
+ anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
+ annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
+ expires);
+ }
+ });
+ },
+
+ // Identifier getters for special folders.
+ // You should use these everywhere PlacesUtils is available to avoid XPCOM
+ // traversal just to get roots' ids.
+ get placesRootId() {
+ delete this.placesRootId;
+ return this.placesRootId = this.bookmarks.placesRoot;
+ },
+
+ get bookmarksMenuFolderId() {
+ delete this.bookmarksMenuFolderId;
+ return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
+ },
+
+ get toolbarFolderId() {
+ delete this.toolbarFolderId;
+ return this.toolbarFolderId = this.bookmarks.toolbarFolder;
+ },
+
+ get tagsFolderId() {
+ delete this.tagsFolderId;
+ return this.tagsFolderId = this.bookmarks.tagsFolder;
+ },
+
+ get unfiledBookmarksFolderId() {
+ delete this.unfiledBookmarksFolderId;
+ return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
+ },
+
+ /**
+ * Checks if aItemId is a root.
+ *
+ * @param aItemId
+ * item id to look for.
+ * @returns true if aItemId is a root, false otherwise.
+ */
+ isRootItem: function PU_isRootItem(aItemId) {
+ return aItemId == PlacesUtils.bookmarksMenuFolderId ||
+ aItemId == PlacesUtils.toolbarFolderId ||
+ aItemId == PlacesUtils.unfiledBookmarksFolderId ||
+ aItemId == PlacesUtils.tagsFolderId ||
+ aItemId == PlacesUtils.placesRootId;
+ },
+
+ /**
+ * Set the POST data associated with a bookmark, if any.
+ * Used by POST keywords.
+ * @param aBookmarkId
+ * @returns string of POST data
+ */
+ setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
+ const annos = this.annotations;
+ if (aPostData)
+ annos.setItemAnnotation(aBookmarkId, this.POST_DATA_ANNO, aPostData,
+ 0, Ci.nsIAnnotationService.EXPIRE_NEVER);
+ else if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
+ annos.removeItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
+ },
+
+ /**
+ * Get the POST data associated with a bookmark, if any.
+ * @param aBookmarkId
+ * @returns string of POST data if set for aBookmarkId. null otherwise.
+ */
+ getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
+ const annos = this.annotations;
+ if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
+ return annos.getItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
+
+ return null;
+ },
+
+ /**
+ * Get the URI (and any associated POST data) for a given keyword.
+ * @param aKeyword string keyword
+ * @returns an array containing a string URL and a string of POST data
+ */
+ getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
+ var url = null, postdata = null;
+ try {
+ var uri = this.bookmarks.getURIForKeyword(aKeyword);
+ if (uri) {
+ url = uri.spec;
+ var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri);
+ for (let i = 0; i < bookmarks.length; i++) {
+ var bookmark = bookmarks[i];
+ var kw = this.bookmarks.getKeywordForBookmark(bookmark);
+ if (kw == aKeyword) {
+ postdata = this.getPostDataForBookmark(bookmark);
+ break;
+ }
+ }
+ }
+ } catch(ex) {}
+ return [url, postdata];
+ },
+
+ /**
+ * Get all bookmarks for a URL, excluding items under tags.
+ */
+ getBookmarksForURI:
+ function PU_getBookmarksForURI(aURI) {
+ var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
+
+ // filter the ids list
+ return bmkIds.filter(function(aID) {
+ var parentId = this.bookmarks.getFolderIdForItem(aID);
+ var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
+ // item under a tag container
+ if (grandparentId == this.tagsFolderId)
+ return false;
+ return true;
+ }, this);
+ },
+
+ /**
+ * Get the most recently added/modified bookmark for a URL, excluding items
+ * under tags.
+ *
+ * @param aURI
+ * nsIURI of the page we will look for.
+ * @returns itemId of the found bookmark, or -1 if nothing is found.
+ */
+ getMostRecentBookmarkForURI:
+ function PU_getMostRecentBookmarkForURI(aURI) {
+ var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
+ for (var i = 0; i < bmkIds.length; i++) {
+ // Find the first folder which isn't a tag container
+ var itemId = bmkIds[i];
+ var parentId = this.bookmarks.getFolderIdForItem(itemId);
+ // Optimization: if this is a direct child of a root we don't need to
+ // check if its grandparent is a tag.
+ if (parentId == this.unfiledBookmarksFolderId ||
+ parentId == this.toolbarFolderId ||
+ parentId == this.bookmarksMenuFolderId)
+ return itemId;
+
+ var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
+ if (grandparentId != this.tagsFolderId)
+ return itemId;
+ }
+ return -1;
+ },
+
+ /**
+ * Returns a nsNavHistoryContainerResultNode with forced excludeItems and
+ * expandQueries.
+ * @param aNode
+ * The node to convert
+ * @param [optional] excludeItems
+ * True to hide all items (individual bookmarks). This is used on
+ * the left places pane so you just get a folder hierarchy.
+ * @param [optional] expandQueries
+ * True to make query items expand as new containers. For managing,
+ * you want this to be false, for menus and such, you want this to
+ * be true.
+ * @returns A nsINavHistoryContainerResultNode containing the unfiltered
+ * contents of the container.
+ * @note The returned container node could be open or closed, we don't
+ * guarantee its status.
+ */
+ getContainerNodeWithOptions:
+ function PU_getContainerNodeWithOptions(aNode, aExcludeItems, aExpandQueries) {
+ if (!this.nodeIsContainer(aNode))
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ // excludeItems is inherited by child containers in an excludeItems view.
+ var excludeItems = asQuery(aNode).queryOptions.excludeItems ||
+ asQuery(aNode.parentResult.root).queryOptions.excludeItems;
+ // expandQueries is inherited by child containers in an expandQueries view.
+ var expandQueries = asQuery(aNode).queryOptions.expandQueries &&
+ asQuery(aNode.parentResult.root).queryOptions.expandQueries;
+
+ // If our options are exactly what we expect, directly return the node.
+ if (excludeItems == aExcludeItems && expandQueries == aExpandQueries)
+ return aNode;
+
+ // Otherwise, get contents manually.
+ var queries = {}, options = {};
+ this.history.queryStringToQueries(aNode.uri, queries, {}, options);
+ options.value.excludeItems = aExcludeItems;
+ options.value.expandQueries = aExpandQueries;
+ return this.history.executeQueries(queries.value,
+ queries.value.length,
+ options.value).root;
+ },
+
+ /**
+ * Returns true if a container has uri nodes in its first level.
+ * Has better performance than (getURLsForContainerNode(node).length > 0).
+ * @param aNode
+ * The container node to search through.
+ * @returns true if the node contains uri nodes, false otherwise.
+ */
+ hasChildURIs: function PU_hasChildURIs(aNode) {
+ if (!this.nodeIsContainer(aNode))
+ return false;
+
+ let root = this.getContainerNodeWithOptions(aNode, false, true);
+ let result = root.parentResult;
+ let didSuppressNotifications = false;
+ let wasOpen = root.containerOpen;
+ if (!wasOpen) {
+ didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ root.containerOpen = true;
+ }
+
+ let found = false;
+ for (let i = 0; i < root.childCount && !found; i++) {
+ let child = root.getChild(i);
+ if (this.nodeIsURI(child))
+ found = true;
+ }
+
+ if (!wasOpen) {
+ root.containerOpen = false;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ return found;
+ },
+
+ /**
+ * Returns an array containing all the uris in the first level of the
+ * passed in container.
+ * If you only need to know if the node contains uris, use hasChildURIs.
+ * @param aNode
+ * The container node to search through
+ * @returns array of uris in the first level of the container.
+ */
+ getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
+ let urls = [];
+ if (!this.nodeIsContainer(aNode))
+ return urls;
+
+ let root = this.getContainerNodeWithOptions(aNode, false, true);
+ let result = root.parentResult;
+ let wasOpen = root.containerOpen;
+ let didSuppressNotifications = false;
+ if (!wasOpen) {
+ didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ root.containerOpen = true;
+ }
+
+ for (let i = 0; i < root.childCount; ++i) {
+ let child = root.getChild(i);
+ if (this.nodeIsURI(child))
+ urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
+ }
+
+ if (!wasOpen) {
+ root.containerOpen = false;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ return urls;
+ },
+
+ /**
+ * Serializes the given node (and all its descendents) as JSON
+ * and writes the serialization to the given output stream.
+ *
+ * @param aNode
+ * An nsINavHistoryResultNode
+ * @param aStream
+ * An nsIOutputStream. NOTE: it only uses the write(str, len)
+ * method of nsIOutputStream. The caller is responsible for
+ * closing the stream.
+ */
+ _serializeNodeAsJSONToOutputStream: function (aNode, aStream) {
+ function addGenericProperties(aPlacesNode, aJSNode) {
+ aJSNode.title = aPlacesNode.title;
+ aJSNode.id = aPlacesNode.itemId;
+ if (aJSNode.id != -1) {
+ var parent = aPlacesNode.parent;
+ if (parent) {
+ aJSNode.parent = parent.itemId;
+ aJSNode.parentReadOnly = PlacesUtils.nodeIsReadOnly(parent);
+ }
+ var dateAdded = aPlacesNode.dateAdded;
+ if (dateAdded)
+ aJSNode.dateAdded = dateAdded;
+ var lastModified = aPlacesNode.lastModified;
+ if (lastModified)
+ aJSNode.lastModified = lastModified;
+
+ // XXX need a hasAnnos api
+ var annos = [];
+ try {
+ annos = PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
+ // XXX should whitelist this instead, w/ a pref for
+ // backup/restore of non-whitelisted annos
+ // XXX causes JSON encoding errors, so utf-8 encode
+ //anno.value = unescape(encodeURIComponent(anno.value));
+ if (anno.name == PlacesUtils.LMANNO_FEEDURI)
+ aJSNode.livemark = 1;
+ return true;
+ });
+ } catch(ex) {}
+ if (annos.length != 0)
+ aJSNode.annos = annos;
+ }
+ // XXXdietrich - store annos for non-bookmark items
+ }
+
+ function addURIProperties(aPlacesNode, aJSNode) {
+ aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ aJSNode.uri = aPlacesNode.uri;
+ if (aJSNode.id && aJSNode.id != -1) {
+ // harvest bookmark-specific properties
+ var keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
+ if (keyword)
+ aJSNode.keyword = keyword;
+ }
+
+ if (aPlacesNode.tags)
+ aJSNode.tags = aPlacesNode.tags;
+
+ // last character-set
+ var uri = PlacesUtils._uri(aPlacesNode.uri);
+ try {
+ var lastCharset = PlacesUtils.annotations.getPageAnnotation(
+ uri, PlacesUtils.CHARSET_ANNO);
+ aJSNode.charset = lastCharset;
+ } catch (e) {}
+ }
+
+ function addSeparatorProperties(aPlacesNode, aJSNode) {
+ aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+ }
+
+ function addContainerProperties(aPlacesNode, aJSNode) {
+ var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
+ if (concreteId != -1) {
+ // This is a bookmark or a tag container.
+ if (PlacesUtils.nodeIsQuery(aPlacesNode) ||
+ concreteId != aPlacesNode.itemId) {
+ aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ aJSNode.uri = aPlacesNode.uri;
+ // folder shortcut
+ aJSNode.concreteId = concreteId;
+ }
+ else { // Bookmark folder or a shortcut we should convert to folder.
+ aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+
+ // Mark root folders.
+ if (aJSNode.id == PlacesUtils.placesRootId)
+ aJSNode.root = "placesRoot";
+ else if (aJSNode.id == PlacesUtils.bookmarksMenuFolderId)
+ aJSNode.root = "bookmarksMenuFolder";
+ else if (aJSNode.id == PlacesUtils.tagsFolderId)
+ aJSNode.root = "tagsFolder";
+ else if (aJSNode.id == PlacesUtils.unfiledBookmarksFolderId)
+ aJSNode.root = "unfiledBookmarksFolder";
+ else if (aJSNode.id == PlacesUtils.toolbarFolderId)
+ aJSNode.root = "toolbarFolder";
+ }
+ }
+ else {
+ // This is a grouped container query, generated on the fly.
+ aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ aJSNode.uri = aPlacesNode.uri;
+ }
+ }
+
+ function appendConvertedComplexNode(aNode, aSourceNode, aArray) {
+ var repr = {};
+
+ for (let [name, value] in Iterator(aNode))
+ repr[name] = value;
+
+ // write child nodes
+ var children = repr.children = [];
+ if (!aNode.livemark) {
+ asContainer(aSourceNode);
+ var wasOpen = aSourceNode.containerOpen;
+ if (!wasOpen)
+ aSourceNode.containerOpen = true;
+ var cc = aSourceNode.childCount;
+ for (var i = 0; i < cc; ++i) {
+ var childNode = aSourceNode.getChild(i);
+ appendConvertedNode(aSourceNode.getChild(i), i, children);
+ }
+ if (!wasOpen)
+ aSourceNode.containerOpen = false;
+ }
+
+ aArray.push(repr);
+ return true;
+ }
+
+ function appendConvertedNode(bNode, aIndex, aArray) {
+ var node = {};
+
+ // set index in order received
+ // XXX handy shortcut, but are there cases where we don't want
+ // to export using the sorting provided by the query?
+ if (aIndex)
+ node.index = aIndex;
+
+ addGenericProperties(bNode, node);
+
+ var parent = bNode.parent;
+ var grandParent = parent ? parent.parent : null;
+ if (grandParent)
+ node.grandParentId = grandParent.itemId;
+
+ if (PlacesUtils.nodeIsURI(bNode)) {
+ // Tag root accept only folder nodes
+ if (parent && parent.itemId == PlacesUtils.tagsFolderId)
+ return false;
+
+ // Check for url validity, since we can't halt while writing a backup.
+ // This will throw if we try to serialize an invalid url and it does
+ // not make sense saving a wrong or corrupt uri node.
+ try {
+ PlacesUtils._uri(bNode.uri);
+ } catch (ex) {
+ return false;
+ }
+
+ addURIProperties(bNode, node);
+ }
+ else if (PlacesUtils.nodeIsContainer(bNode)) {
+ // Tag containers accept only uri nodes
+ if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
+ return false;
+
+ addContainerProperties(bNode, node);
+ }
+ else if (PlacesUtils.nodeIsSeparator(bNode)) {
+ // Tag root accept only folder nodes
+ // Tag containers accept only uri nodes
+ if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
+ (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
+ return false;
+
+ addSeparatorProperties(bNode, node);
+ }
+
+ if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
+ return appendConvertedComplexNode(node, bNode, aArray);
+
+ aArray.push(node);
+ return true;
+ }
+
+ // serialize to stream
+ var array = [];
+ if (appendConvertedNode(aNode, null, array)) {
+ var json = JSON.stringify(array[0]);
+ aStream.write(json, json.length);
+ }
+ else {
+ throw Cr.NS_ERROR_UNEXPECTED;
+ }
+ },
+
+ /**
+ * Given a uri returns list of itemIds associated to it.
+ *
+ * @param aURI
+ * nsIURI or spec of the page.
+ * @param aCallback
+ * Function to be called when done.
+ * The function will receive an array of itemIds associated to aURI and
+ * aURI itself.
+ *
+ * @return A object with a .cancel() method allowing to cancel the request.
+ *
+ * @note Children of live bookmarks folders are excluded. The callback function is
+ * not invoked if the request is cancelled or hits an error.
+ */
+ asyncGetBookmarkIds: function PU_asyncGetBookmarkIds(aURI, aCallback)
+ {
+ if (!this._asyncGetBookmarksStmt) {
+ let db = this.history.DBConnection;
+ this._asyncGetBookmarksStmt = db.createAsyncStatement(
+ "SELECT b.id "
+ + "FROM moz_bookmarks b "
+ + "JOIN moz_places h on h.id = b.fk "
+ + "WHERE h.url = :url "
+ );
+ this.registerShutdownFunction(function () {
+ this._asyncGetBookmarksStmt.finalize();
+ });
+ }
+
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ this._asyncGetBookmarksStmt.params.url = url;
+
+ // Storage does not guarantee that invoking cancel() on a statement
+ // will cause a REASON_CANCELED. Thus we wrap the statement.
+ let stmt = new AsyncStatementCancelWrapper(this._asyncGetBookmarksStmt);
+ return stmt.executeAsync({
+ _callback: aCallback,
+ _itemIds: [],
+ handleResult: function(aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ this._itemIds.push(row.getResultByIndex(0));
+ }
+ },
+ handleCompletion: function(aReason)
+ {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ this._callback(this._itemIds, aURI);
+ }
+ }
+ });
+ },
+
+ /**
+ * Lazily adds a bookmarks observer, waiting for the bookmarks service to be
+ * alive before registering the observer. This is especially useful in the
+ * startup path, to avoid initializing the service just to add an observer.
+ *
+ * @param aObserver
+ * Object implementing nsINavBookmarkObserver
+ * @param [optional]aWeakOwner
+ * Whether to use weak ownership.
+ *
+ * @note Correct functionality of lazy observers relies on the fact Places
+ * notifies categories before real observers, and uses
+ * PlacesCategoriesStarter component to kick-off the registration.
+ */
+ _bookmarksServiceReady: false,
+ _bookmarksServiceObserversQueue: [],
+ addLazyBookmarkObserver:
+ function PU_addLazyBookmarkObserver(aObserver, aWeakOwner) {
+ if (this._bookmarksServiceReady) {
+ this.bookmarks.addObserver(aObserver, aWeakOwner === true);
+ return;
+ }
+ this._bookmarksServiceObserversQueue.push({ observer: aObserver,
+ weak: aWeakOwner === true });
+ },
+
+ /**
+ * Removes a bookmarks observer added through addLazyBookmarkObserver.
+ *
+ * @param aObserver
+ * Object implementing nsINavBookmarkObserver
+ */
+ removeLazyBookmarkObserver:
+ function PU_removeLazyBookmarkObserver(aObserver) {
+ if (this._bookmarksServiceReady) {
+ this.bookmarks.removeObserver(aObserver);
+ return;
+ }
+ let index = -1;
+ for (let i = 0;
+ i < this._bookmarksServiceObserversQueue.length && index == -1; i++) {
+ if (this._bookmarksServiceObserversQueue[i].observer === aObserver)
+ index = i;
+ }
+ if (index != -1) {
+ this._bookmarksServiceObserversQueue.splice(index, 1);
+ }
+ },
+
+ /**
+ * Sets the character-set for a URI.
+ *
+ * @param aURI nsIURI
+ * @param aCharset character-set value.
+ * @return {Promise}
+ */
+ setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
+ let deferred = Promise.defer();
+
+ // Delaying to catch issues with asynchronous behavior while waiting
+ // to implement asynchronous annotations in bug 699844.
+ Services.tm.mainThread.dispatch(function() {
+ if (aCharset && aCharset.length > 0) {
+ PlacesUtils.annotations.setPageAnnotation(
+ aURI, PlacesUtils.CHARSET_ANNO, aCharset, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+ } else {
+ PlacesUtils.annotations.removePageAnnotation(
+ aURI, PlacesUtils.CHARSET_ANNO);
+ }
+ deferred.resolve();
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Gets the last saved character-set for a URI.
+ *
+ * @param aURI nsIURI
+ * @return {Promise}
+ * @resolve a character-set or null.
+ */
+ getCharsetForURI: function PU_getCharsetForURI(aURI) {
+ let deferred = Promise.defer();
+
+ Services.tm.mainThread.dispatch(function() {
+ let charset = null;
+
+ try {
+ charset = PlacesUtils.annotations.getPageAnnotation(aURI,
+ PlacesUtils.CHARSET_ANNO);
+ } catch (ex) { }
+
+ deferred.resolve(charset);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Promised wrapper for mozIAsyncHistory::updatePlaces for a single place.
+ *
+ * @param aPlaces
+ * a single mozIPlaceInfo object
+ * @resolves {Promise}
+ */
+ promiseUpdatePlace: function PU_promiseUpdatePlaces(aPlace) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.updatePlaces(aPlace, {
+ _placeInfo: null,
+ handleResult: function handleResult(aPlaceInfo) {
+ this._placeInfo = aPlaceInfo;
+ },
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ deferred.reject(new Components.Exception("Error", aResultCode));
+ },
+ handleCompletion: function() {
+ deferred.resolve(this._placeInfo);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Promised wrapper for mozIAsyncHistory::getPlacesInfo for a single place.
+ *
+ * @param aPlaceIdentifier
+ * either an nsIURI or a GUID (@see getPlacesInfo)
+ * @resolves to the place info object handed to handleResult.
+ */
+ promisePlaceInfo: function PU_promisePlaceInfo(aPlaceIdentifier) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.getPlacesInfo(aPlaceIdentifier, {
+ _placeInfo: null,
+ handleResult: function handleResult(aPlaceInfo) {
+ this._placeInfo = aPlaceInfo;
+ },
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ deferred.reject(new Components.Exception("Error", aResultCode));
+ },
+ handleCompletion: function() {
+ deferred.resolve(this._placeInfo);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Gets favicon data for a given page url.
+ *
+ * @param aPageUrl url of the page to look favicon for.
+ * @resolves to an object representing a favicon entry, having the following
+ * properties: { uri, dataLen, data, mimeType }
+ * @rejects JavaScript exception if the given url has no associated favicon.
+ */
+ promiseFaviconData: function (aPageUrl) {
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.getFaviconDataForPage(NetUtil.newURI(aPageUrl),
+ function (aURI, aDataLen, aData, aMimeType) {
+ if (aURI) {
+ deferred.resolve({ uri: aURI,
+ dataLen: aDataLen,
+ data: aData,
+ mimeType: aMimeType });
+ } else {
+ deferred.reject();
+ }
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Get the unique id for an item (a bookmark, a folder or a separator) given
+ * its item id.
+ *
+ * @param aItemId
+ * an item id
+ * @return {Promise}
+ * @resolves to the GUID.
+ * @rejects if aItemId is invalid.
+ */
+ promiseItemGUID: function (aItemId) GUIDHelper.getItemGUID(aItemId),
+
+ /**
+ * Get the item id for an item (a bookmark, a folder or a separator) given
+ * its unique id.
+ *
+ * @param aGUID
+ * an item GUID
+ * @retrun {Promise}
+ * @resolves to the GUID.
+ * @rejects if there's no item for the given GUID.
+ */
+ promiseItemId: function (aGUID) GUIDHelper.getItemId(aGUID)
+};
+
+/**
+ * Wraps the provided statement so that invoking cancel() on the pending
+ * statement object will always cause a REASON_CANCELED.
+ */
+function AsyncStatementCancelWrapper(aStmt) {
+ this._stmt = aStmt;
+}
+AsyncStatementCancelWrapper.prototype = {
+ _canceled: false,
+ _cancel: function() {
+ this._canceled = true;
+ this._pendingStmt.cancel();
+ },
+ handleResult: function(aResultSet) {
+ this._callback.handleResult(aResultSet);
+ },
+ handleError: function(aError) {
+ Cu.reportError("Async statement execution returned (" + aError.result +
+ "): " + aError.message);
+ },
+ handleCompletion: function(aReason)
+ {
+ let reason = this._canceled ?
+ Ci.mozIStorageStatementCallback.REASON_CANCELED :
+ aReason;
+ this._callback.handleCompletion(reason);
+ },
+ executeAsync: function(aCallback) {
+ this._pendingStmt = this._stmt.executeAsync(this);
+ this._callback = aCallback;
+ let self = this;
+ return { cancel: function () { self._cancel(); } }
+ }
+}
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "history", function() {
+ return Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService)
+ .QueryInterface(Ci.nsIBrowserHistory)
+ .QueryInterface(Ci.nsPIPlacesDatabase);
+});
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "asyncHistory",
+ "@mozilla.org/browser/history;1",
+ "mozIAsyncHistory");
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() {
+ return PlacesUtils.history;
+});
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons",
+ "@mozilla.org/browser/favicon-service;1",
+ "mozIAsyncFavicons");
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks",
+ "@mozilla.org/browser/nav-bookmarks-service;1",
+ "nsINavBookmarksService");
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
+ "@mozilla.org/browser/tagging-service;1",
+ "nsITaggingService");
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "livemarks", function() {
+ return Cc["@mozilla.org/browser/livemark-service;2"].
+ getService(Ci.mozIAsyncLivemarks);
+});
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
+ let tm = Cc["@mozilla.org/transactionmanager;1"].
+ createInstance(Ci.nsITransactionManager);
+ tm.AddListener(PlacesUtils);
+ this.registerShutdownFunction(function () {
+ // Clear all references to local transactions in the transaction manager,
+ // this prevents from leaking it.
+ this.transactionManager.RemoveListener(this);
+ this.transactionManager.clear();
+ });
+
+ // Bug 750269
+ // The transaction manager keeps strong references to transactions, and by
+ // that, also to the global for each transaction. A transaction, however,
+ // could be either the transaction itself (for which the global is this
+ // module) or some js-proxy in another global, usually a window. The later
+ // would leak because the transaction lifetime (in the manager's stacks)
+ // is independent of the global from which doTransaction was called.
+ // To avoid such a leak, we hide the native doTransaction from callers,
+ // and let each doTransaction call go through this module.
+ // Doing so ensures that, as long as the transaction is any of the
+ // PlacesXXXTransaction objects declared in this module, the object
+ // referenced by the transaction manager has the module itself as global.
+ return Object.create(tm, {
+ "doTransaction": {
+ value: function(aTransaction) {
+ tm.doTransaction(aTransaction);
+ }
+ }
+ });
+});
+
+XPCOMUtils.defineLazyGetter(this, "bundle", function() {
+ const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/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");
+
+// Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
+// itemIds will be deprecated in favour of GUIDs, which play much better
+// with multiple undo/redo operations. Because these GUIDs are already stored,
+// and because we don't want to revise the transactions API once more when this
+// happens, transactions are set to work with GUIDs exclusively, in the sense
+// that they may never expose itemIds, nor do they accept them as input.
+// More importantly, transactions which add or remove items guarantee to
+// restore the guids on undo/redo, so that the following transactions that may
+// done or undo can assume the items they're interested in are stil accessible
+// through the same GUID.
+// The current bookmarks API, however, doesn't expose the necessary means for
+// working with GUIDs. So, until it does, this helper object accesses the
+// Places database directly in order to switch between GUIDs and itemIds, and
+// "restore" GUIDs on items re-created items.
+const REASON_FINISHED = Ci.mozIStorageStatementCallback.REASON_FINISHED;
+let GUIDHelper = {
+ // Cache for guid<->itemId paris.
+ GUIDsForIds: new Map(),
+ idsForGUIDs: new Map(),
+
+ getItemId: function (aGUID) {
+ if (this.idsForGUIDs.has(aGUID))
+ return Promise.resolve(this.idsForGUIDs.get(aGUID));
+
+ let deferred = Promise.defer();
+ let itemId = -1;
+
+ this._getIDStatement.params.guid = aGUID;
+ this._getIDStatement.executeAsync({
+ handleResult: function (aResultSet) {
+ let row = aResultSet.getNextRow();
+ if (row)
+ itemId = row.getResultByIndex(0);
+ },
+ handleCompletion: aReason => {
+ if (aReason == REASON_FINISHED && itemId != -1) {
+ this.ensureObservingRemovedItems();
+ this.idsForGUIDs.set(aGUID, itemId);
+
+ deferred.resolve(itemId);
+ }
+ else if (itemId != -1) {
+ deferred.reject("no item found for the given guid");
+ }
+ else {
+ deferred.reject("SQLite Error: " + aReason);
+ }
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ getItemGUID: function (aItemId) {
+ if (this.GUIDsForIds.has(aItemId))
+ return Promise.resolve(this.GUIDsForIds.get(aItemId));
+
+ let deferred = Promise.defer();
+ let guid = "";
+
+ this._getGUIDStatement.params.id = aItemId;
+ this._getGUIDStatement.executeAsync({
+ handleResult: function (aResultSet) {
+ let row = aResultSet.getNextRow();
+ if (row) {
+ guid = row.getResultByIndex(1);
+ }
+ },
+ handleCompletion: aReason => {
+ if (aReason == REASON_FINISHED && guid) {
+ this.ensureObservingRemovedItems();
+ this.GUIDsForIds.set(aItemId, guid);
+
+ deferred.resolve(guid);
+ }
+ else if (!guid) {
+ deferred.reject("no item found for the given itemId");
+ }
+ else {
+ deferred.reject("SQLite Error: " + aReason);
+ }
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ ensureObservingRemovedItems: function () {
+ if (!("observer" in this)) {
+ /**
+ * This observers serves two purposes:
+ * (1) Invalidate cached id<->guid paris on when items are removed.
+ * (2) Cache GUIDs given us free of charge by onItemAdded/onItemRemoved.
+ * So, for exmaple, when the NewBookmark needs the new GUID, we already
+ * have it cached.
+ */
+ this.observer = {
+ onItemAdded: (aItemId, aParentId, aIndex, aItemType, aURI, aTitle,
+ aDateAdded, aGUID, aParentGUID) => {
+ this.GUIDsForIds.set(aItemId, aGUID);
+ this.GUIDsForIds.set(aParentId, aParentGUID);
+ },
+ onItemRemoved:
+ (aItemId, aParentId, aIndex, aItemTyep, aURI, aGUID, aParentGUID) => {
+ this.GUIDsForIds.delete(aItemId);
+ this.idsForGUIDs.delete(aGUID);
+ this.GUIDsForIds.set(aParentId, aParentGUID);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
+ __noSuchMethod__: () => {}, // Catch all all onItem* methods.
+ };
+ PlacesUtils.bookmarks.addObserver(this.observer, false);
+ PlacesUtils.registerShutdownFunction(() => {
+ PlacesUtils.bookmarks.removeObserver(this.observer);
+ });
+ }
+ }
+};
+XPCOMUtils.defineLazyGetter(GUIDHelper, "_getIDStatement", () => {
+ let s = PlacesUtils.history.DBConnection.createAsyncStatement(
+ "SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid");
+ PlacesUtils.registerShutdownFunction( () => s.finalize() );
+ return s;
+});
+XPCOMUtils.defineLazyGetter(GUIDHelper, "_getGUIDStatement", () => {
+ let s = PlacesUtils.history.DBConnection.createAsyncStatement(
+ "SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id");
+ PlacesUtils.registerShutdownFunction( () => s.finalize() );
+ return s;
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Transactions handlers.
+
+/**
+ * Updates commands in the undo group of the active window commands.
+ * Inactive windows commands will be updated on focus.
+ */
+function updateCommandsOnActiveWindow()
+{
+ let win = focusManager.activeWindow;
+ if (win && win instanceof Ci.nsIDOMWindow) {
+ // Updating "undo" will cause a group update including "redo".
+ win.updateCommands("undo");
+ }
+}
+
+
+/**
+ * Used to cache bookmark information in transactions.
+ *
+ * @note To avoid leaks any non-primitive property should be copied.
+ * @note Used internally, DO NOT EXPORT.
+ */
+function TransactionItemCache()
+{
+}
+
+TransactionItemCache.prototype = {
+ set id(v)
+ this._id = (parseInt(v) > 0 ? v : null),
+ get id()
+ this._id || -1,
+ set parentId(v)
+ this._parentId = (parseInt(v) > 0 ? v : null),
+ get parentId()
+ this._parentId || -1,
+ keyword: null,
+ title: null,
+ dateAdded: null,
+ lastModified: null,
+ postData: null,
+ itemType: null,
+ set uri(v)
+ this._uri = (v instanceof Ci.nsIURI ? v.clone() : null),
+ get uri()
+ this._uri || null,
+ set feedURI(v)
+ this._feedURI = (v instanceof Ci.nsIURI ? v.clone() : null),
+ get feedURI()
+ this._feedURI || null,
+ set siteURI(v)
+ this._siteURI = (v instanceof Ci.nsIURI ? v.clone() : null),
+ get siteURI()
+ this._siteURI || null,
+ set index(v)
+ this._index = (parseInt(v) >= 0 ? v : null),
+ // Index can be 0.
+ get index()
+ this._index != null ? this._index : PlacesUtils.bookmarks.DEFAULT_INDEX,
+ set annotations(v)
+ this._annotations = Array.isArray(v) ? Cu.cloneInto(v, {}) : null,
+ get annotations()
+ this._annotations || null,
+ set tags(v)
+ this._tags = (v && Array.isArray(v) ? Array.slice(v) : null),
+ get tags()
+ this._tags || null,
+};
+
+
+/**
+ * Base transaction implementation.
+ *
+ * @note used internally, DO NOT EXPORT.
+ */
+function BaseTransaction()
+{
+}
+
+BaseTransaction.prototype = {
+ name: null,
+ set childTransactions(v)
+ this._childTransactions = (Array.isArray(v) ? Array.slice(v) : null),
+ get childTransactions()
+ this._childTransactions || null,
+ doTransaction: function BTXN_doTransaction() {},
+ redoTransaction: function BTXN_redoTransaction() this.doTransaction(),
+ undoTransaction: function BTXN_undoTransaction() {},
+ merge: function BTXN_merge() false,
+ get isTransient() false,
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsITransaction
+ ]),
+};
+
+
+/**
+ * Transaction for performing several Places Transactions in a single batch.
+ *
+ * @param aName
+ * title of the aggregate transactions
+ * @param aTransactions
+ * an array of transactions to perform
+ *
+ * @return nsITransaction object
+ */
+this.PlacesAggregatedTransaction =
+ function PlacesAggregatedTransaction(aName, aTransactions)
+{
+ // Copy the transactions array to decouple it from its prototype, which
+ // otherwise keeps alive its associated global object.
+ this.childTransactions = aTransactions;
+ this.name = aName;
+ this.item = new TransactionItemCache();
+
+ // Check child transactions number. We will batch if we have more than
+ // MIN_TRANSACTIONS_FOR_BATCH total number of transactions.
+ let countTransactions = function(aTransactions, aTxnCount)
+ {
+ for (let i = 0;
+ i < aTransactions.length && aTxnCount < MIN_TRANSACTIONS_FOR_BATCH;
+ ++i, ++aTxnCount) {
+ let txn = aTransactions[i];
+ if (txn.childTransactions && txn.childTransactions.length > 0)
+ aTxnCount = countTransactions(txn.childTransactions, aTxnCount);
+ }
+ return aTxnCount;
+ }
+
+ let txnCount = countTransactions(this.childTransactions, 0);
+ this._useBatch = txnCount >= MIN_TRANSACTIONS_FOR_BATCH;
+}
+
+PlacesAggregatedTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function ATXN_doTransaction()
+ {
+ this._isUndo = false;
+ if (this._useBatch)
+ PlacesUtils.bookmarks.runInBatchMode(this, null);
+ else
+ this.runBatched(false);
+ },
+
+ undoTransaction: function ATXN_undoTransaction()
+ {
+ this._isUndo = true;
+ if (this._useBatch)
+ PlacesUtils.bookmarks.runInBatchMode(this, null);
+ else
+ this.runBatched(true);
+ },
+
+ runBatched: function ATXN_runBatched()
+ {
+ // Use a copy of the transactions array, so we won't reverse the original
+ // one on undoing.
+ let transactions = this.childTransactions.slice(0);
+ if (this._isUndo)
+ transactions.reverse();
+ for (let i = 0; i < transactions.length; ++i) {
+ let txn = transactions[i];
+ if (this.item.parentId != -1)
+ txn.item.parentId = this.item.parentId;
+ if (this._isUndo)
+ txn.undoTransaction();
+ else
+ txn.doTransaction();
+ }
+ }
+};
+
+
+/**
+ * Transaction for creating a new folder.
+ *
+ * @param aTitle
+ * the title for the new folder
+ * @param aParentId
+ * the id of the parent folder in which the new folder should be added
+ * @param [optional] aIndex
+ * the index of the item in aParentId
+ * @param [optional] aAnnotations
+ * array of annotations to set for the new folder
+ * @param [optional] aChildTransactions
+ * array of transactions for items to be created in the new folder
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateFolderTransaction =
+ function PlacesCreateFolderTransaction(aTitle, aParentId, aIndex, aAnnotations,
+ aChildTransactions)
+{
+ this.item = new TransactionItemCache();
+ this.item.title = aTitle;
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+ this.item.annotations = aAnnotations;
+ this.childTransactions = aChildTransactions;
+}
+
+PlacesCreateFolderTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CFTXN_doTransaction()
+ {
+ this.item.id = PlacesUtils.bookmarks.createFolder(this.item.parentId,
+ this.item.title,
+ this.item.index);
+ if (this.item.annotations && this.item.annotations.length > 0)
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ // Set the new parent id into child transactions.
+ for (let i = 0; i < this.childTransactions.length; ++i) {
+ this.childTransactions[i].item.parentId = this.item.id;
+ }
+
+ let txn = new PlacesAggregatedTransaction("Create folder childTxn",
+ this.childTransactions);
+ txn.doTransaction();
+ }
+ },
+
+ undoTransaction: function CFTXN_undoTransaction()
+ {
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ let txn = new PlacesAggregatedTransaction("Create folder childTxn",
+ this.childTransactions);
+ txn.undoTransaction();
+ }
+
+ // Remove item only after all child transactions have been reverted.
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }
+};
+
+
+/**
+ * Transaction for creating a new bookmark.
+ *
+ * @param aURI
+ * the nsIURI of the new bookmark
+ * @param aParentId
+ * the id of the folder in which the bookmark should be added.
+ * @param [optional] aIndex
+ * the index of the item in aParentId
+ * @param [optional] aTitle
+ * the title of the new bookmark
+ * @param [optional] aKeyword
+ * the keyword for the new bookmark
+ * @param [optional] aAnnotations
+ * array of annotations to set for the new bookmark
+ * @param [optional] aChildTransactions
+ * child transactions to commit after creating the bookmark. Prefer
+ * using any of the arguments above if possible. In general, a child
+ * transations should be used only if the change it does has to be
+ * reverted manually when removing the bookmark item.
+ * a child transaction must support setting its bookmark-item
+ * identifier via an "id" js setter.
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateBookmarkTransaction =
+ function PlacesCreateBookmarkTransaction(aURI, aParentId, aIndex, aTitle,
+ aKeyword, aAnnotations,
+ aChildTransactions)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+ this.item.title = aTitle;
+ this.item.keyword = aKeyword;
+ this.item.annotations = aAnnotations;
+ this.childTransactions = aChildTransactions;
+}
+
+PlacesCreateBookmarkTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CITXN_doTransaction()
+ {
+ this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
+ this.item.uri,
+ this.item.index,
+ this.item.title);
+ if (this.item.keyword) {
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
+ this.item.keyword);
+ }
+ if (this.item.annotations && this.item.annotations.length > 0)
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ // Set the new item id into child transactions.
+ for (let i = 0; i < this.childTransactions.length; ++i) {
+ this.childTransactions[i].item.id = this.item.id;
+ }
+ let txn = new PlacesAggregatedTransaction("Create item childTxn",
+ this.childTransactions);
+ txn.doTransaction();
+ }
+ },
+
+ undoTransaction: function CITXN_undoTransaction()
+ {
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ // Undo transactions should always be done in reverse order.
+ let txn = new PlacesAggregatedTransaction("Create item childTxn",
+ this.childTransactions);
+ txn.undoTransaction();
+ }
+
+ // Remove item only after all child transactions have been reverted.
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }
+};
+
+
+/**
+ * Transaction for creating a new separator.
+ *
+ * @param aParentId
+ * the id of the folder in which the separator should be added
+ * @param [optional] aIndex
+ * the index of the item in aParentId
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateSeparatorTransaction =
+ function PlacesCreateSeparatorTransaction(aParentId, aIndex)
+{
+ this.item = new TransactionItemCache();
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+}
+
+PlacesCreateSeparatorTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CSTXN_doTransaction()
+ {
+ this.item.id =
+ PlacesUtils.bookmarks.insertSeparator(this.item.parentId, this.item.index);
+ },
+
+ undoTransaction: function CSTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }
+};
+
+
+/**
+ * Transaction for creating a new livemark item.
+ *
+ * @see mozIAsyncLivemarks for documentation regarding the arguments.
+ *
+ * @param aFeedURI
+ * nsIURI of the feed
+ * @param [optional] aSiteURI
+ * nsIURI of the page serving the feed
+ * @param aTitle
+ * title for the livemark
+ * @param aParentId
+ * the id of the folder in which the livemark should be added
+ * @param [optional] aIndex
+ * the index of the livemark in aParentId
+ * @param [optional] aAnnotations
+ * array of annotations to set for the new livemark.
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateLivemarkTransaction =
+ function PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aTitle, aParentId,
+ aIndex, aAnnotations)
+{
+ this.item = new TransactionItemCache();
+ this.item.feedURI = aFeedURI;
+ this.item.siteURI = aSiteURI;
+ this.item.title = aTitle;
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+ this.item.annotations = aAnnotations;
+}
+
+PlacesCreateLivemarkTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CLTXN_doTransaction()
+ {
+ PlacesUtils.livemarks.addLivemark(
+ { title: this.item.title
+ , feedURI: this.item.feedURI
+ , parentId: this.item.parentId
+ , index: this.item.index
+ , siteURI: this.item.siteURI
+ }).then(aLivemark => {
+ this.item.id = aLivemark.id;
+ if (this.item.annotations && this.item.annotations.length > 0) {
+ PlacesUtils.setAnnotationsForItem(this.item.id,
+ this.item.annotations);
+ }
+ }, Cu.reportError);
+ },
+
+ undoTransaction: function CLTXN_undoTransaction()
+ {
+ // The getLivemark callback may fail, but it is used just to serialize,
+ // so it doesn't matter.
+ PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+ .then(null, null).then( () => {
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ });
+ }
+};
+
+
+/**
+ * Transaction for removing a livemark item.
+ *
+ * @param aLivemarkId
+ * the identifier of the folder for the livemark.
+ *
+ * @return nsITransaction object
+ * @note used internally by PlacesRemoveItemTransaction, DO NOT EXPORT.
+ */
+function PlacesRemoveLivemarkTransaction(aLivemarkId)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aLivemarkId;
+ this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
+ this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
+
+ let annos = PlacesUtils.getAnnotationsForItem(this.item.id);
+ // Exclude livemark service annotations, those will be recreated automatically
+ let annosToExclude = [PlacesUtils.LMANNO_FEEDURI,
+ PlacesUtils.LMANNO_SITEURI];
+ this.item.annotations = annos.filter(function(aValue, aIndex, aArray) {
+ return annosToExclude.indexOf(aValue.name) == -1;
+ });
+ this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
+ this.item.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(this.item.id);
+}
+
+PlacesRemoveLivemarkTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function RLTXN_doTransaction()
+ {
+ PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+ .then(aLivemark => {
+ this.item.feedURI = aLivemark.feedURI;
+ this.item.siteURI = aLivemark.siteURI;
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }, Cu.reportError);
+ },
+
+ undoTransaction: function RLTXN_undoTransaction()
+ {
+ // Undo work must be serialized, otherwise won't be able to know the
+ // feedURI and siteURI of the livemark.
+ // The getLivemark callback is expected to receive a failure status but it
+ // is used just to serialize, so doesn't matter.
+ PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+ .then(null, () => {
+ PlacesUtils.livemarks.addLivemark({ parentId: this.item.parentId
+ , title: this.item.title
+ , siteURI: this.item.siteURI
+ , feedURI: this.item.feedURI
+ , index: this.item.index
+ , lastModified: this.item.lastModified
+ }).then(
+ aLivemark => {
+ let itemId = aLivemark.id;
+ PlacesUtils.bookmarks.setItemDateAdded(itemId, this.item.dateAdded);
+ PlacesUtils.setAnnotationsForItem(itemId, this.item.annotations);
+ }, Cu.reportError);
+ });
+ }
+};
+
+
+/**
+ * Transaction for moving an Item.
+ *
+ * @param aItemId
+ * the id of the item to move
+ * @param aNewParentId
+ * id of the new parent to move to
+ * @param aNewIndex
+ * index of the new position to move to
+ *
+ * @return nsITransaction object
+ */
+this.PlacesMoveItemTransaction =
+ function PlacesMoveItemTransaction(aItemId, aNewParentId, aNewIndex)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
+ this.new = new TransactionItemCache();
+ this.new.parentId = aNewParentId;
+ this.new.index = aNewIndex;
+}
+
+PlacesMoveItemTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function MITXN_doTransaction()
+ {
+ this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
+ PlacesUtils.bookmarks.moveItem(this.item.id,
+ this.new.parentId, this.new.index);
+ this._undoIndex = PlacesUtils.bookmarks.getItemIndex(this.item.id);
+ },
+
+ undoTransaction: function MITXN_undoTransaction()
+ {
+ // moving down in the same parent takes in count removal of the item
+ // so to revert positions we must move to oldIndex + 1
+ if (this.new.parentId == this.item.parentId &&
+ this.item.index > this._undoIndex) {
+ PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
+ this.item.index + 1);
+ }
+ else {
+ PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
+ this.item.index);
+ }
+ }
+};
+
+
+/**
+ * Transaction for removing an Item
+ *
+ * @param aItemId
+ * id of the item to remove
+ *
+ * @return nsITransaction object
+ */
+this.PlacesRemoveItemTransaction =
+ function PlacesRemoveItemTransaction(aItemId)
+{
+ if (PlacesUtils.isRootItem(aItemId))
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ // if the item lives within a tag container, use the tagging transactions
+ let parent = PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
+ let grandparent = PlacesUtils.bookmarks.getFolderIdForItem(parent);
+ if (grandparent == PlacesUtils.tagsFolderId) {
+ let uri = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
+ return new PlacesUntagURITransaction(uri, [parent]);
+ }
+
+ // if the item is a livemark container we will not save its children.
+ if (PlacesUtils.annotations.itemHasAnnotation(aItemId,
+ PlacesUtils.LMANNO_FEEDURI))
+ return new PlacesRemoveLivemarkTransaction(aItemId);
+
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.item.itemType = PlacesUtils.bookmarks.getItemType(this.item.id);
+ if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
+ this.childTransactions = this._getFolderContentsTransactions();
+ // Remove this folder itself.
+ let txn = PlacesUtils.bookmarks.getRemoveFolderTransaction(this.item.id);
+ this.childTransactions.push(txn);
+ }
+ else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
+ this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
+ this.item.keyword =
+ PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
+ }
+
+ if (this.item.itemType != Ci.nsINavBookmarksService.TYPE_SEPARATOR)
+ this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
+
+ this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
+ this.item.annotations = PlacesUtils.getAnnotationsForItem(this.item.id);
+ this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
+ this.item.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(this.item.id);
+}
+
+PlacesRemoveItemTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function RITXN_doTransaction()
+ {
+ this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
+
+ if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
+ let txn = new PlacesAggregatedTransaction("Remove item childTxn",
+ this.childTransactions);
+ txn.doTransaction();
+ }
+ else {
+ // Before removing the bookmark, save its tags.
+ let tags = this.item.uri ?
+ PlacesUtils.tagging.getTagsForURI(this.item.uri) : null;
+
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+
+ // If this was the last bookmark (excluding tag-items) for this url,
+ // persist the tags.
+ if (tags && PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
+ this.item.tags = tags;
+ }
+ }
+ },
+
+ undoTransaction: function RITXN_undoTransaction()
+ {
+ if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
+ this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
+ this.item.uri,
+ this.item.index,
+ this.item.title);
+ if (this.item.tags && this.item.tags.length > 0)
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ if (this.item.keyword) {
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
+ this.item.keyword);
+ }
+ }
+ else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
+ let txn = new PlacesAggregatedTransaction("Remove item childTxn",
+ this.childTransactions);
+ txn.undoTransaction();
+ }
+ else { // TYPE_SEPARATOR
+ this.item.id = PlacesUtils.bookmarks.insertSeparator(this.item.parentId,
+ this.item.index);
+ }
+
+ if (this.item.annotations && this.item.annotations.length > 0)
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+
+ PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
+ PlacesUtils.bookmarks.setItemLastModified(this.item.id,
+ this.item.lastModified);
+ },
+
+ /**
+ * Returns a flat, ordered list of transactions for a depth-first recreation
+ * of items within this folder.
+ */
+ _getFolderContentsTransactions:
+ function RITXN__getFolderContentsTransactions()
+ {
+ let transactions = [];
+ let contents =
+ PlacesUtils.getFolderContents(this.item.id, false, false).root;
+ for (let i = 0; i < contents.childCount; ++i) {
+ let txn = new PlacesRemoveItemTransaction(contents.getChild(i).itemId);
+ transactions.push(txn);
+ }
+ contents.containerOpen = false;
+ // Reverse transactions to preserve parent-child relationship.
+ return transactions.reverse();
+ }
+};
+
+
+/**
+ * Transaction for editting a bookmark's title.
+ *
+ * @param aItemId
+ * id of the item to edit
+ * @param aNewTitle
+ * new title for the item to edit
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditItemTitleTransaction =
+ function PlacesEditItemTitleTransaction(aItemId, aNewTitle)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.title = aNewTitle;
+}
+
+PlacesEditItemTitleTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EITTXN_doTransaction()
+ {
+ this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
+ PlacesUtils.bookmarks.setItemTitle(this.item.id, this.new.title);
+ },
+
+ undoTransaction: function EITTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setItemTitle(this.item.id, this.item.title);
+ }
+};
+
+
+/**
+ * Transaction for editing a bookmark's uri.
+ *
+ * @param aItemId
+ * id of the bookmark to edit
+ * @param aNewURI
+ * new uri for the bookmark
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditBookmarkURITransaction =
+ function PlacesEditBookmarkURITransaction(aItemId, aNewURI) {
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.uri = aNewURI;
+}
+
+PlacesEditBookmarkURITransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EBUTXN_doTransaction()
+ {
+ this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
+ PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.new.uri);
+ // move tags from old URI to new URI
+ this.item.tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
+ if (this.item.tags.length != 0) {
+ // only untag the old URI if this is the only bookmark
+ if (PlacesUtils.getBookmarksForURI(this.item.uri, {}).length == 0)
+ PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
+ PlacesUtils.tagging.tagURI(this.new.uri, this.item.tags);
+ }
+ },
+
+ undoTransaction: function EBUTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.item.uri);
+ // move tags from new URI to old URI
+ if (this.item.tags.length != 0) {
+ // only untag the new URI if this is the only bookmark
+ if (PlacesUtils.getBookmarksForURI(this.new.uri, {}).length == 0)
+ PlacesUtils.tagging.untagURI(this.new.uri, this.item.tags);
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ }
+ }
+};
+
+
+/**
+ * Transaction for setting/unsetting an item annotation
+ *
+ * @param aItemId
+ * id of the item where to set annotation
+ * @param aAnnotationObject
+ * Object representing an annotation, containing the following
+ * properties: name, flags, expires, value.
+ * If value is null the annotation will be removed
+ *
+ * @return nsITransaction object
+ */
+this.PlacesSetItemAnnotationTransaction =
+ function PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.annotations = [aAnnotationObject];
+}
+
+PlacesSetItemAnnotationTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function SIATXN_doTransaction()
+ {
+ let annoName = this.new.annotations[0].name;
+ if (PlacesUtils.annotations.itemHasAnnotation(this.item.id, annoName)) {
+ // fill the old anno if it is set
+ let flags = {}, expires = {}, type = {};
+ PlacesUtils.annotations.getItemAnnotationInfo(this.item.id, annoName, flags,
+ expires, type);
+ let value = PlacesUtils.annotations.getItemAnnotation(this.item.id,
+ annoName);
+ this.item.annotations = [{ name: annoName,
+ type: type.value,
+ flags: flags.value,
+ value: value,
+ expires: expires.value }];
+ }
+ else {
+ // create an empty old anno
+ this.item.annotations = [{ name: annoName,
+ flags: 0,
+ value: null,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+ }
+
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.new.annotations);
+ },
+
+ undoTransaction: function SIATXN_undoTransaction()
+ {
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+ }
+};
+
+
+/**
+ * Transaction for setting/unsetting a page annotation
+ *
+ * @param aURI
+ * URI of the page where to set annotation
+ * @param aAnnotationObject
+ * Object representing an annotation, containing the following
+ * properties: name, flags, expires, value.
+ * If value is null the annotation will be removed
+ *
+ * @return nsITransaction object
+ */
+this.PlacesSetPageAnnotationTransaction =
+ function PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ this.new = new TransactionItemCache();
+ this.new.annotations = [aAnnotationObject];
+}
+
+PlacesSetPageAnnotationTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function SPATXN_doTransaction()
+ {
+ let annoName = this.new.annotations[0].name;
+ if (PlacesUtils.annotations.pageHasAnnotation(this.item.uri, annoName)) {
+ // fill the old anno if it is set
+ let flags = {}, expires = {}, type = {};
+ PlacesUtils.annotations.getPageAnnotationInfo(this.item.uri, annoName, flags,
+ expires, type);
+ let value = PlacesUtils.annotations.getPageAnnotation(this.item.uri,
+ annoName);
+ this.item.annotations = [{ name: annoName,
+ flags: flags.value,
+ value: value,
+ expires: expires.value }];
+ }
+ else {
+ // create an empty old anno
+ this.item.annotations = [{ name: annoName,
+ type: Ci.nsIAnnotationService.TYPE_STRING,
+ flags: 0,
+ value: null,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+ }
+
+ PlacesUtils.setAnnotationsForURI(this.item.uri, this.new.annotations);
+ },
+
+ undoTransaction: function SPATXN_undoTransaction()
+ {
+ PlacesUtils.setAnnotationsForURI(this.item.uri, this.item.annotations);
+ }
+};
+
+
+/**
+ * Transaction for editing a bookmark's keyword.
+ *
+ * @param aItemId
+ * id of the bookmark to edit
+ * @param aNewKeyword
+ * new keyword for the bookmark
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditBookmarkKeywordTransaction =
+ function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.keyword = aNewKeyword;
+}
+
+PlacesEditBookmarkKeywordTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EBKTXN_doTransaction()
+ {
+ this.item.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.new.keyword);
+ },
+
+ undoTransaction: function EBKTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword);
+ }
+};
+
+
+/**
+ * Transaction for editing the post data associated with a bookmark.
+ *
+ * @param aItemId
+ * id of the bookmark to edit
+ * @param aPostData
+ * post data
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditBookmarkPostDataTransaction =
+ function PlacesEditBookmarkPostDataTransaction(aItemId, aPostData)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.postData = aPostData;
+}
+
+PlacesEditBookmarkPostDataTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EBPDTXN_doTransaction()
+ {
+ this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
+ PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
+ },
+
+ undoTransaction: function EBPDTXN_undoTransaction()
+ {
+ PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
+ }
+};
+
+
+/**
+ * Transaction for editing an item's date added property.
+ *
+ * @param aItemId
+ * id of the item to edit
+ * @param aNewDateAdded
+ * new date added for the item
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditItemDateAddedTransaction =
+ function PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.dateAdded = aNewDateAdded;
+}
+
+PlacesEditItemDateAddedTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EIDATXN_doTransaction()
+ {
+ // Child transactions have the id set as parentId.
+ if (this.item.id == -1 && this.item.parentId != -1)
+ this.item.id = this.item.parentId;
+ this.item.dateAdded =
+ PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
+ PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.new.dateAdded);
+ },
+
+ undoTransaction: function EIDATXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
+ }
+};
+
+
+/**
+ * Transaction for editing an item's last modified time.
+ *
+ * @param aItemId
+ * id of the item to edit
+ * @param aNewLastModified
+ * new last modified date for the item
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditItemLastModifiedTransaction =
+ function PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.lastModified = aNewLastModified;
+}
+
+PlacesEditItemLastModifiedTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction:
+ function EILMTXN_doTransaction()
+ {
+ // Child transactions have the id set as parentId.
+ if (this.item.id == -1 && this.item.parentId != -1)
+ this.item.id = this.item.parentId;
+ this.item.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(this.item.id);
+ PlacesUtils.bookmarks.setItemLastModified(this.item.id,
+ this.new.lastModified);
+ },
+
+ undoTransaction:
+ function EILMTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setItemLastModified(this.item.id,
+ this.item.lastModified);
+ }
+};
+
+
+/**
+ * Transaction for sorting a folder by name
+ *
+ * @param aFolderId
+ * id of the folder to sort
+ *
+ * @return nsITransaction object
+ */
+this.PlacesSortFolderByNameTransaction =
+ function PlacesSortFolderByNameTransaction(aFolderId)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aFolderId;
+}
+
+PlacesSortFolderByNameTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function SFBNTXN_doTransaction()
+ {
+ this._oldOrder = [];
+
+ let contents =
+ PlacesUtils.getFolderContents(this.item.id, false, false).root;
+ let count = contents.childCount;
+
+ // sort between separators
+ let newOrder = [];
+ let preSep = []; // temporary array for sorting each group of items
+ let sortingMethod =
+ function (a, b) {
+ if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
+ return -1;
+ if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
+ return 1;
+ return a.title.localeCompare(b.title);
+ };
+
+ for (let i = 0; i < count; ++i) {
+ let item = contents.getChild(i);
+ this._oldOrder[item.itemId] = i;
+ if (PlacesUtils.nodeIsSeparator(item)) {
+ if (preSep.length > 0) {
+ preSep.sort(sortingMethod);
+ newOrder = newOrder.concat(preSep);
+ preSep.splice(0, preSep.length);
+ }
+ newOrder.push(item);
+ }
+ else
+ preSep.push(item);
+ }
+ contents.containerOpen = false;
+
+ if (preSep.length > 0) {
+ preSep.sort(sortingMethod);
+ newOrder = newOrder.concat(preSep);
+ }
+
+ // set the nex indexes
+ let callback = {
+ runBatched: function() {
+ for (let i = 0; i < newOrder.length; ++i) {
+ PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
+ }
+ }
+ };
+ PlacesUtils.bookmarks.runInBatchMode(callback, null);
+ },
+
+ undoTransaction: function SFBNTXN_undoTransaction()
+ {
+ let callback = {
+ _self: this,
+ runBatched: function() {
+ for (item in this._self._oldOrder)
+ PlacesUtils.bookmarks.setItemIndex(item, this._self._oldOrder[item]);
+ }
+ };
+ PlacesUtils.bookmarks.runInBatchMode(callback, null);
+ }
+};
+
+
+/**
+ * Transaction for tagging a URL with the given set of tags. Current tags set
+ * for the URL persist. It's the caller's job to check whether or not aURI
+ * was already tagged by any of the tags in aTags, undoing this tags
+ * transaction removes them all from aURL!
+ *
+ * @param aURI
+ * the URL to tag.
+ * @param aTags
+ * Array of tags to set for the given URL.
+ */
+this.PlacesTagURITransaction =
+ function PlacesTagURITransaction(aURI, aTags)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ this.item.tags = aTags;
+}
+
+PlacesTagURITransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function TUTXN_doTransaction()
+ {
+ if (PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
+ // There is no bookmark for this uri, but we only allow to tag bookmarks.
+ // Force an unfiled bookmark first.
+ this.item.id =
+ PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ this.item.uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ PlacesUtils.history.getPageTitle(this.item.uri));
+ }
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ },
+
+ undoTransaction: function TUTXN_undoTransaction()
+ {
+ if (this.item.id != -1) {
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ this.item.id = -1;
+ }
+ PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
+ }
+};
+
+
+/**
+ * Transaction for removing tags from a URL. It's the caller's job to check
+ * whether or not aURI isn't tagged by any of the tags in aTags, undoing this
+ * tags transaction adds them all to aURL!
+ *
+ * @param aURI
+ * the URL to un-tag.
+ * @param aTags
+ * Array of tags to unset. pass null to remove all tags from the given
+ * url.
+ */
+this.PlacesUntagURITransaction =
+ function PlacesUntagURITransaction(aURI, aTags)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ if (aTags) {
+ // Within this transaction, we cannot rely on tags given by itemId
+ // since the tag containers may be gone after we call untagURI.
+ // Thus, we convert each tag given by its itemId to name.
+ let tags = [];
+ for (let i = 0; i < aTags.length; ++i) {
+ if (typeof(aTags[i]) == "number")
+ tags.push(PlacesUtils.bookmarks.getItemTitle(aTags[i]));
+ else
+ tags.push(aTags[i]);
+ }
+ this.item.tags = tags;
+ }
+}
+
+PlacesUntagURITransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function UTUTXN_doTransaction()
+ {
+ // Filter tags existing on the bookmark, otherwise on undo we may try to
+ // set nonexistent tags.
+ let tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
+ this.item.tags = this.item.tags.filter(function (aTag) {
+ return tags.indexOf(aTag) != -1;
+ });
+ PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
+ },
+
+ undoTransaction: function UTUTXN_undoTransaction()
+ {
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ }
+};