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: };