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