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: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["PlacesTransactions"]; michael@0: michael@0: /** michael@0: * Overview michael@0: * -------- michael@0: * This modules serves as the transactions manager for Places, and implements michael@0: * all the standard transactions for its UI commands (creating items, editing michael@0: * various properties, etc.). It shares most of its semantics with common michael@0: * command pattern implementations, the HTML5 Undo Manager in particular. michael@0: * However, the asynchronous design of [future] Places APIs, combined with the michael@0: * commitment to serialize all UI operations, makes things a little bit michael@0: * different. For example, when |undo| is called in order to undo the top undo michael@0: * entry, the caller cannot tell for sure what entry would it be because the michael@0: * execution of some transaction is either in process, or queued. michael@0: * michael@0: * GUIDs and item-ids michael@0: * ------------------- michael@0: * The Bookmarks API still relies heavily on item-ids, but since those do not michael@0: * play nicely with the concept of undo and redo (especially not in an michael@0: * asynchronous environment), this API only accepts bookmark GUIDs, both for michael@0: * input (e.g. for specifying the parent folder for a new bookmark) and for michael@0: * output (when the GUID for such a bookmark is propagated). michael@0: * michael@0: * GUIDs are readily available when dealing with the "output" of this API and michael@0: * when result nodes are used (see nsINavHistoryResultNode::bookmarkGUID). michael@0: * If you only have item-ids in hand, use PlacesUtils.promiseItemGUID for michael@0: * converting them. Should you need to convert them back into itemIds, use michael@0: * PlacesUtils.promiseItemId. michael@0: * michael@0: * The Standard Transactions michael@0: * ------------------------- michael@0: * At the bottom of this module you will find implementations for all Places UI michael@0: * commands (One should almost never fallback to raw Places APIs. Please file michael@0: * a bug if you find anything uncovered). The transactions' constructors are michael@0: * set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder). michael@0: * The input for this constructors is taken in the form of a single argument michael@0: * plain object. Input properties may be either required (e.g. the |keyword| michael@0: * property for the EditKeyword transaction) or optional (e.g. the |keyword| michael@0: * property for NewBookmark). Once a transaction is created, you may pass it michael@0: * to |transact| or use it in the for batching (see next section). michael@0: * michael@0: * The constructors throw right away when any required input is missing or when michael@0: * some input is invalid "on the surface" (e.g. GUID values are validated to be michael@0: * 12-characters strings, but are not validated to point to existing item. Such michael@0: * an error will reveal when the transaction is executed). michael@0: * michael@0: * To make things simple, a given input property has the same basic meaning and michael@0: * valid values across all transactions which accept it in the input object. michael@0: * Here is a list of all supported input properties along with their expected michael@0: * values: michael@0: * - uri: an nsIURI object. michael@0: * - feedURI: an nsIURI object, holding the url for a live bookmark. michael@0: * - siteURI: an nsIURI object, holding the url for the site with which michael@0: * a live bookmark is associated. michael@0: * - GUID, parentGUID, newParentGUID: a valid places GUID string. michael@0: * - title: a string michael@0: * - index, newIndex: the position of an item in its containing folder, michael@0: * starting from 0. michael@0: * integer and PlacesUtils.bookmarks.DEFAULT_INDEX michael@0: * - annotationObject: see PlacesUtils.setAnnotationsForItem michael@0: * - annotations: an array of annotation objects as above. michael@0: * - tags: an array of strings. michael@0: * michael@0: * Batching transactions michael@0: * --------------------- michael@0: * Sometimes it is useful to "batch" or "merge" transactions. For example, michael@0: * "Bookmark All Tabs" may be implemented as one NewFolder transaction followed michael@0: * by numerous NewBookmark transactions - all to be undone or redone in a single michael@0: * command. The |transact| method makes this possible using a generator michael@0: * function as an input. These generators have the same semantics as in michael@0: * Task.jsm except that when you yield a transaction, it's executed, and the michael@0: * resolution (e.g. the new bookmark GUID) is sent to the generator so you can michael@0: * use it as the input for another transaction. See |transact| for the details. michael@0: * michael@0: * "Custom" transactions michael@0: * --------------------- michael@0: * In the legacy transactions API it was possible to pass-in transactions michael@0: * implemented "externally". For various reason this isn't allowed anymore: michael@0: * transact throws right away if one attempts to pass a transaction that was not michael@0: * created by this module. However, it's almost always possible to achieve the michael@0: * same functionality with the batching technique described above. michael@0: * michael@0: * The transactions-history structure michael@0: * ---------------------------------- michael@0: * The transactions-history is a two-dimensional stack of transactions: the michael@0: * transactions are ordered in reverse to the order they were committed. michael@0: * It's two-dimensional because the undo manager allows batching transactions michael@0: * together for the purpose of undo or redo (batched transactions can never be michael@0: * undone or redone partially). michael@0: * michael@0: * The undoPosition property is set to the index of the top entry. If there is michael@0: * no entry at that index, there is nothing to undo. michael@0: * Entries prior to undoPosition, if any, are redo entries, the first one being michael@0: * the top redo entry. michael@0: * michael@0: * [ [2nd redo txn, 1st redo txn], <= 2nd redo entry michael@0: * [2nd redo txn, 1st redo txn], <= 1st redo entry michael@0: * [1st undo txn, 2nd undo txn], <= 1st undo entry michael@0: * [1st undo txn, 2nd undo txn] <= 2nd undo entry ] michael@0: * undoPostion: 2. michael@0: * michael@0: * Note that when a new entry is created, all redo entries are removed. michael@0: */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); 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: function updateCommandsOnActiveWindow() { michael@0: // Updating "undo" will cause a group update including "redo". michael@0: try { michael@0: let win = Services.focus.activeWindow; michael@0: if (win) michael@0: win.updateCommands("undo"); michael@0: } michael@0: catch(ex) { console.error(ex, "Couldn't update undo commands"); } michael@0: } michael@0: michael@0: // The internal object for managing the transactions history. michael@0: // The public API is included in PlacesTransactions. michael@0: // TODO bug 982099: extending the array "properly" makes it painful to implement michael@0: // getters. If/when ES6 gets proper array subclassing we can revise this. michael@0: let TransactionsHistory = []; michael@0: TransactionsHistory.__proto__ = { michael@0: __proto__: Array.prototype, michael@0: michael@0: // The index of the first undo entry (if any) - See the documentation michael@0: // at the top of this file. michael@0: _undoPosition: 0, michael@0: get undoPosition() this._undoPosition, michael@0: michael@0: // Handy shortcuts michael@0: get topUndoEntry() this.undoPosition < this.length ? michael@0: this[this.undoPosition] : null, michael@0: get topRedoEntry() this.undoPosition > 0 ? michael@0: this[this.undoPosition - 1] : null, michael@0: michael@0: // Outside of this module, the API of transactions is inaccessible, and so michael@0: // are any internal properties. To achieve that, transactions are proxified michael@0: // in their constructors. This maps the proxies to their respective raw michael@0: // objects. michael@0: proxifiedToRaw: new WeakMap(), michael@0: michael@0: /** michael@0: * Proxify a transaction object for consumers. michael@0: * @param aRawTransaction michael@0: * the raw transaction object. michael@0: * @return the proxified transaction object. michael@0: * @see getRawTransaction for retrieving the raw transaction. michael@0: */ michael@0: proxifyTransaction: function (aRawTransaction) { michael@0: let proxy = Object.freeze({}); michael@0: this.proxifiedToRaw.set(proxy, aRawTransaction); michael@0: return proxy; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the given object is a the proxy object for some transaction. michael@0: * @param aValue michael@0: * any JS value. michael@0: * @return true if aValue is the proxy object for some transaction, false michael@0: * otherwise. michael@0: */ michael@0: isProxifiedTransactionObject: michael@0: function (aValue) this.proxifiedToRaw.has(aValue), michael@0: michael@0: /** michael@0: * Get the raw transaction for the given proxy. michael@0: * @param aProxy michael@0: * the proxy object michael@0: * @return the transaction proxified by aProxy; |undefined| is returned if michael@0: * aProxy is not a proxified transaction. michael@0: */ michael@0: getRawTransaction: function (aProxy) this.proxifiedToRaw.get(aProxy), michael@0: michael@0: /** michael@0: * Undo the top undo entry, if any, and update the undo position accordingly. michael@0: */ michael@0: undo: function* () { michael@0: let entry = this.topUndoEntry; michael@0: if (!entry) michael@0: return; michael@0: michael@0: for (let transaction of entry) { michael@0: try { michael@0: yield TransactionsHistory.getRawTransaction(transaction).undo(); michael@0: } michael@0: catch(ex) { michael@0: // If one transaction is broken, it's not safe to work with any other michael@0: // undo entry. Report the error and clear the undo history. michael@0: console.error(ex, michael@0: "Couldn't undo a transaction, clearing all undo entries."); michael@0: this.clearUndoEntries(); michael@0: return; michael@0: } michael@0: } michael@0: this._undoPosition++; michael@0: updateCommandsOnActiveWindow(); michael@0: }, michael@0: michael@0: /** michael@0: * Redo the top redo entry, if any, and update the undo position accordingly. michael@0: */ michael@0: redo: function* () { michael@0: let entry = this.topRedoEntry; michael@0: if (!entry) michael@0: return; michael@0: michael@0: for (let i = entry.length - 1; i >= 0; i--) { michael@0: let transaction = TransactionsHistory.getRawTransaction(entry[i]); michael@0: try { michael@0: if (transaction.redo) michael@0: yield transaction.redo(); michael@0: else michael@0: yield transaction.execute(); michael@0: } michael@0: catch(ex) { michael@0: // If one transaction is broken, it's not safe to work with any other michael@0: // redo entry. Report the error and clear the undo history. michael@0: console.error(ex, michael@0: "Couldn't redo a transaction, clearing all redo entries."); michael@0: this.clearRedoEntries(); michael@0: return; michael@0: } michael@0: } michael@0: this._undoPosition--; michael@0: updateCommandsOnActiveWindow(); michael@0: }, michael@0: michael@0: /** michael@0: * Add a transaction either as a new entry, if forced or if there are no undo michael@0: * entries, or to the top undo entry. michael@0: * michael@0: * @param aProxifiedTransaction michael@0: * the proxified transaction object to be added to the transaction michael@0: * history. michael@0: * @param [optional] aForceNewEntry michael@0: * Force a new entry for the transaction. Default: false. michael@0: * If false, an entry will we created only if there's no undo entry michael@0: * to extend. michael@0: */ michael@0: add: function (aProxifiedTransaction, aForceNewEntry = false) { michael@0: if (!this.isProxifiedTransactionObject(aProxifiedTransaction)) michael@0: throw new Error("aProxifiedTransaction is not a proxified transaction"); michael@0: michael@0: if (this.length == 0 || aForceNewEntry) { michael@0: this.clearRedoEntries(); michael@0: this.unshift([aProxifiedTransaction]); michael@0: } michael@0: else { michael@0: this[this.undoPosition].unshift(aProxifiedTransaction); michael@0: } michael@0: updateCommandsOnActiveWindow(); michael@0: }, michael@0: michael@0: /** michael@0: * Clear all undo entries. michael@0: */ michael@0: clearUndoEntries: function () { michael@0: if (this.undoPosition < this.length) michael@0: this.splice(this.undoPosition); michael@0: }, michael@0: michael@0: /** michael@0: * Clear all redo entries. michael@0: */ michael@0: clearRedoEntries: function () { michael@0: if (this.undoPosition > 0) { michael@0: this.splice(0, this.undoPosition); michael@0: this._undoPosition = 0; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Clear all entries. michael@0: */ michael@0: clearAllEntries: function () { michael@0: if (this.length > 0) { michael@0: this.splice(0); michael@0: this._undoPosition = 0; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: // Our transaction manager is asynchronous in the sense that all of its methods michael@0: // don't execute synchronously. However, all actions must be serialized. michael@0: let currentTask = Promise.resolve(); michael@0: function Serialize(aTask) { michael@0: // Ignore failures. michael@0: return currentTask = currentTask.then( () => Task.spawn(aTask) ) michael@0: .then(null, Components.utils.reportError); michael@0: } michael@0: michael@0: // Transactions object should never be recycled (that is, |execute| should michael@0: // only be called once, or not at all, after they're constructed. michael@0: // This keeps track of all transactions which were executed. michael@0: let executedTransactions = new WeakMap(); // TODO: use WeakSet (bug 792439) michael@0: executedTransactions.add = k => executedTransactions.set(k, null); michael@0: michael@0: let PlacesTransactions = { michael@0: /** michael@0: * Asynchronously transact either a single transaction, or a sequence of michael@0: * transactions that would be treated as a single entry in the transactions michael@0: * history. michael@0: * michael@0: * @param aToTransact michael@0: * Either a transaction object or a generator function (ES6-style only) michael@0: * that yields transaction objects. michael@0: * michael@0: * Generator mode how-to: when a transaction is yielded, it's executed. michael@0: * Then, if it was executed successfully, the resolution of |execute| michael@0: * is sent to the generator. If |execute| threw or rejected, the michael@0: * exception is propagated to the generator. michael@0: * Any other value yielded by a generator function is handled the michael@0: * same way as in a Task (see Task.jsm). michael@0: * michael@0: * @return {Promise} michael@0: * @resolves either to the resolution of |execute|, in single-transaction mode, michael@0: * or to the return value of the generator, in generator-mode. michael@0: * @rejects either if |execute| threw, in single-transaction mode, or if michael@0: * the generator function threw (or didn't handle) an exception, in generator michael@0: * mode. michael@0: * @throws if aTransactionOrGeneratorFunction is neither a transaction object michael@0: * created by this module or a generator function. michael@0: * @note If no transaction was executed successfully, the transactions history michael@0: * is not affected. michael@0: * michael@0: * @note All PlacesTransactions operations are serialized. This means that the michael@0: * transactions history state may change by the time the transaction/generator michael@0: * is processed. It's guaranteed, however, that a generator function "blocks" michael@0: * the queue: that is, it is assured that no other operations are performed michael@0: * by or on PlacesTransactions until the generator returns. Keep in mind you michael@0: * are not protected from consumers who use the raw places APIs directly. michael@0: */ michael@0: transact: function (aToTransact) { michael@0: let isGeneratorObj = michael@0: o => Object.prototype.toString.call(o) == "[object Generator]"; michael@0: michael@0: let generator = null; michael@0: if (typeof(aToTransact) == "function") { michael@0: generator = aToTransact(); michael@0: if (!isGeneratorObj(generator)) michael@0: throw new Error("aToTransact is not a generator function"); michael@0: } michael@0: else if (!TransactionsHistory.isProxifiedTransactionObject(aToTransact)) { michael@0: throw new Error("aToTransact is not a valid transaction object"); michael@0: } michael@0: else if (executedTransactions.has(aToTransact)) { michael@0: throw new Error("Transactions objects may not be recycled."); michael@0: } michael@0: michael@0: return Serialize(function* () { michael@0: // The entry in the transactions history is created once the first michael@0: // transaction is committed. This means that if |transact| is called michael@0: // in its "generator mode" and no transactions are committed by the michael@0: // generator, the transactions history is left unchanged. michael@0: // Bug 982115: Depending on how this API is actually used we may revise michael@0: // this decision and make it so |transact| always forces a new entry. michael@0: let forceNewEntry = true; michael@0: function* transactOneTransaction(aTransaction) { michael@0: let retval = michael@0: yield TransactionsHistory.getRawTransaction(aTransaction).execute(); michael@0: executedTransactions.add(aTransaction); michael@0: TransactionsHistory.add(aTransaction, forceNewEntry); michael@0: forceNewEntry = false; michael@0: return retval; michael@0: } michael@0: michael@0: function* transactBatch(aGenerator) { michael@0: let error = false; michael@0: let sendValue = undefined; michael@0: while (true) { michael@0: let next = error ? michael@0: aGenerator.throw(sendValue) : aGenerator.next(sendValue); michael@0: sendValue = next.value; michael@0: if (isGeneratorObj(sendValue)) { michael@0: sendValue = yield transactBatch(sendValue); michael@0: } michael@0: else if (typeof(sendValue) == "object" && sendValue) { michael@0: if (TransactionsHistory.isProxifiedTransactionObject(sendValue)) { michael@0: if (executedTransactions.has(sendValue)) { michael@0: sendValue = new Error("Transactions may not be recycled."); michael@0: error = true; michael@0: } michael@0: else { michael@0: sendValue = yield transactOneTransaction(sendValue); michael@0: } michael@0: } michael@0: else if ("then" in sendValue) { michael@0: sendValue = yield sendValue; michael@0: } michael@0: } michael@0: if (next.done) michael@0: break; michael@0: } michael@0: return sendValue; michael@0: } michael@0: michael@0: if (generator) michael@0: return yield transactBatch(generator); michael@0: else michael@0: return yield transactOneTransaction(aToTransact); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously undo the transaction immediately after the current undo michael@0: * position in the transactions history in the reverse order, if any, and michael@0: * adjusts the undo position. michael@0: * michael@0: * @return {Promises). The promise always resolves. michael@0: * @note All undo manager operations are queued. This means that transactions michael@0: * history may change by the time your request is fulfilled. michael@0: */ michael@0: undo: function () Serialize(() => TransactionsHistory.undo()), michael@0: michael@0: /** michael@0: * Asynchronously redo the transaction immediately before the current undo michael@0: * position in the transactions history, if any, and adjusts the undo michael@0: * position. michael@0: * michael@0: * @return {Promises). The promise always resolves. michael@0: * @note All undo manager operations are queued. This means that transactions michael@0: * history may change by the time your request is fulfilled. michael@0: */ michael@0: redo: function () Serialize(() => TransactionsHistory.redo()), michael@0: michael@0: /** michael@0: * Asynchronously clear the undo, redo, or all entries from the transactions michael@0: * history. michael@0: * michael@0: * @param [optional] aUndoEntries michael@0: * Whether or not to clear undo entries. Default: true. michael@0: * @param [optional] aRedoEntries michael@0: * Whether or not to clear undo entries. Default: true. michael@0: * michael@0: * @return {Promises). The promise always resolves. michael@0: * @throws if both aUndoEntries and aRedoEntries are false. michael@0: * @note All undo manager operations are queued. This means that transactions michael@0: * history may change by the time your request is fulfilled. michael@0: */ michael@0: clearTransactionsHistory: michael@0: function (aUndoEntries = true, aRedoEntries = true) { michael@0: return Serialize(function* () { michael@0: if (aUndoEntries && aRedoEntries) michael@0: TransactionsHistory.clearAllEntries(); michael@0: else if (aUndoEntries) michael@0: TransactionsHistory.clearUndoEntries(); michael@0: else if (aRedoEntries) michael@0: TransactionsHistory.clearRedoEntries(); michael@0: else michael@0: throw new Error("either aUndoEntries or aRedoEntries should be true"); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The numbers of entries in the transactions history. michael@0: */ michael@0: get length() TransactionsHistory.length, michael@0: michael@0: /** michael@0: * Get the transaction history entry at a given index. Each entry consists michael@0: * of one or more transaction objects. michael@0: * michael@0: * @param aIndex michael@0: * the index of the entry to retrieve. michael@0: * @return an array of transaction objects in their undo order (that is, michael@0: * reversely to the order they were executed). michael@0: * @throw if aIndex is invalid (< 0 or >= length). michael@0: * @note the returned array is a clone of the history entry and is not michael@0: * kept in sync with the original entry if it changes. michael@0: */ michael@0: entry: function (aIndex) { michael@0: if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length) michael@0: throw new Error("Invalid index"); michael@0: michael@0: return TransactionsHistory[aIndex]; michael@0: }, michael@0: michael@0: /** michael@0: * The index of the top undo entry in the transactions history. michael@0: * If there are no undo entries, it equals to |length|. michael@0: * Entries past this point michael@0: * Entries at and past this point are redo entries. michael@0: */ michael@0: get undoPosition() TransactionsHistory.undoPosition, michael@0: michael@0: /** michael@0: * Shortcut for accessing the top undo entry in the transaction history. michael@0: */ michael@0: get topUndoEntry() TransactionsHistory.topUndoEntry, michael@0: michael@0: /** michael@0: * Shortcut for accessing the top redo entry in the transaction history. michael@0: */ michael@0: get topRedoEntry() TransactionsHistory.topRedoEntry michael@0: }; michael@0: michael@0: /** michael@0: * Internal helper for defining the standard transactions and their input. michael@0: * It takes the required and optional properties, and generates the public michael@0: * constructor (which takes the input in the form of a plain object) which, michael@0: * when called, creates the argument-less "public" |execute| method by binding michael@0: * the input properties to the function arguments (required properties first, michael@0: * then the optional properties). michael@0: * michael@0: * If this seems confusing, look at the consumers. michael@0: * michael@0: * This magic serves two purposes: michael@0: * (1) It completely hides the transactions' internals from the module michael@0: * consumers. michael@0: * (2) It keeps each transaction implementation to what is about, bypassing michael@0: * all this bureaucracy while still validating input appropriately. michael@0: */ michael@0: function DefineTransaction(aRequiredProps = [], aOptionalProps = []) { michael@0: for (let prop of [...aRequiredProps, ...aOptionalProps]) { michael@0: if (!DefineTransaction.inputProps.has(prop)) michael@0: throw new Error("Property '" + prop + "' is not defined"); michael@0: } michael@0: michael@0: let ctor = function (aInput) { michael@0: // We want to support both syntaxes: michael@0: // let t = new PlacesTransactions.NewBookmark(), michael@0: // let t = PlacesTransactions.NewBookmark() michael@0: if (this == PlacesTransactions) michael@0: return new ctor(aInput); michael@0: michael@0: if (aRequiredProps.length > 0 || aOptionalProps.length > 0) { michael@0: // Bind the input properties to the arguments of execute. michael@0: let input = DefineTransaction.verifyInput(aInput, aRequiredProps, michael@0: aOptionalProps); michael@0: let executeArgs = [this, michael@0: ...[input[prop] for (prop of aRequiredProps)], michael@0: ...[input[prop] for (prop of aOptionalProps)]]; michael@0: this.execute = Function.bind.apply(this.execute, executeArgs); michael@0: } michael@0: return TransactionsHistory.proxifyTransaction(this); michael@0: }; michael@0: return ctor; michael@0: } michael@0: michael@0: DefineTransaction.isStr = v => typeof(v) == "string"; michael@0: DefineTransaction.isURI = v => v instanceof Components.interfaces.nsIURI; michael@0: DefineTransaction.isIndex = v => Number.isInteger(v) && michael@0: v >= PlacesUtils.bookmarks.DEFAULT_INDEX; michael@0: DefineTransaction.isGUID = v => /^[a-zA-Z0-9\-_]{12}$/.test(v); michael@0: DefineTransaction.isPrimitive = v => v === null || (typeof(v) != "object" && michael@0: typeof(v) != "function"); michael@0: DefineTransaction.isAnnotationObject = function (obj) { michael@0: let checkProperty = (aPropName, aRequired, aCheckFunc) => { michael@0: if (aPropName in obj) michael@0: return aCheckFunc(obj[aPropName]); michael@0: michael@0: return !aRequired; michael@0: }; michael@0: michael@0: if (obj && michael@0: checkProperty("name", true, DefineTransaction.isStr) && michael@0: checkProperty("expires", false, Number.isInteger) && michael@0: checkProperty("flags", false, Number.isInteger) && michael@0: checkProperty("value", false, DefineTransaction.isPrimitive) ) { michael@0: // Nothing else should be set michael@0: let validKeys = ["name", "value", "flags", "expires"]; michael@0: if (Object.keys(obj).every( (k) => validKeys.indexOf(k) != -1 )) michael@0: return true; michael@0: } michael@0: return false; michael@0: }; michael@0: michael@0: DefineTransaction.inputProps = new Map(); michael@0: DefineTransaction.defineInputProps = michael@0: function (aNames, aValidationFunction, aDefaultValue) { michael@0: for (let name of aNames) { michael@0: this.inputProps.set(name, { michael@0: validate: aValidationFunction, michael@0: defaultValue: aDefaultValue, michael@0: isGUIDProp: false michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: DefineTransaction.defineArrayInputProp = michael@0: function (aName, aValidationFunction, aDefaultValue) { michael@0: this.inputProps.set(aName, { michael@0: validate: (v) => Array.isArray(v) && v.every(aValidationFunction), michael@0: defaultValue: aDefaultValue, michael@0: isGUIDProp: false michael@0: }); michael@0: }; michael@0: michael@0: DefineTransaction.verifyPropertyValue = michael@0: function (aProp, aValue, aRequired) { michael@0: if (aValue === undefined) { michael@0: if (aRequired) michael@0: throw new Error("Required property is missing: " + aProp); michael@0: return this.inputProps.get(aProp).defaultValue; michael@0: } michael@0: michael@0: if (!this.inputProps.get(aProp).validate(aValue)) michael@0: throw new Error("Invalid value for property: " + aProp); michael@0: michael@0: if (Array.isArray(aValue)) { michael@0: // The original array cannot be referenced by this module because it would michael@0: // then implicitly reference its global as well. michael@0: return Components.utils.cloneInto(aValue, {}); michael@0: } michael@0: michael@0: return aValue; michael@0: }; michael@0: michael@0: DefineTransaction.verifyInput = michael@0: function (aInput, aRequired = [], aOptional = []) { michael@0: if (aRequired.length == 0 && aOptional.length == 0) michael@0: return {}; michael@0: michael@0: // If there's just a single required/optional property, we allow passing it michael@0: // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGUID) michael@0: // rather than PlacesTransactions.RemoveItem({ GUID: myGUID}). michael@0: // This shortcut isn't supported for "complex" properties - e.g. one cannot michael@0: // pass an annotation object this way (note there is no use case for this at michael@0: // the moment anyway). michael@0: let isSinglePropertyInput = michael@0: this.isPrimitive(aInput) || michael@0: (aInput instanceof Components.interfaces.nsISupports); michael@0: let fixedInput = { }; michael@0: if (aRequired.length > 0) { michael@0: if (isSinglePropertyInput) { michael@0: if (aRequired.length == 1) { michael@0: let prop = aRequired[0], value = aInput; michael@0: value = this.verifyPropertyValue(prop, value, true); michael@0: fixedInput[prop] = value; michael@0: } michael@0: else { michael@0: throw new Error("Transaction input isn't an object"); michael@0: } michael@0: } michael@0: else { michael@0: for (let prop of aRequired) { michael@0: let value = this.verifyPropertyValue(prop, aInput[prop], true); michael@0: fixedInput[prop] = value; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (aOptional.length > 0) { michael@0: if (isSinglePropertyInput && !aRequired.length > 0) { michael@0: if (aOptional.length == 1) { michael@0: let prop = aOptional[0], value = aInput; michael@0: value = this.verifyPropertyValue(prop, value, true); michael@0: fixedInput[prop] = value; michael@0: } michael@0: else if (aInput !== null && aInput !== undefined) { michael@0: throw new Error("Transaction input isn't an object"); michael@0: } michael@0: } michael@0: else { michael@0: for (let prop of aOptional) { michael@0: let value = this.verifyPropertyValue(prop, aInput[prop], false); michael@0: if (value !== undefined) michael@0: fixedInput[prop] = value; michael@0: else michael@0: fixedInput[prop] = this.defaultValues[prop]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return fixedInput; michael@0: }; michael@0: michael@0: // Update the documentation at the top of this module if you add or michael@0: // remove properties. michael@0: DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"], michael@0: DefineTransaction.isURI, null); michael@0: DefineTransaction.defineInputProps(["GUID", "parentGUID", "newParentGUID"], michael@0: DefineTransaction.isGUID); michael@0: DefineTransaction.defineInputProps(["title", "keyword", "postData"], michael@0: DefineTransaction.isStr, ""); michael@0: DefineTransaction.defineInputProps(["index", "newIndex"], michael@0: DefineTransaction.isIndex, michael@0: PlacesUtils.bookmarks.DEFAULT_INDEX); michael@0: DefineTransaction.defineInputProps(["annotationObject"], michael@0: DefineTransaction.isAnnotationObject); michael@0: DefineTransaction.defineArrayInputProp("tags", michael@0: DefineTransaction.isStr, null); michael@0: DefineTransaction.defineArrayInputProp("annotations", michael@0: DefineTransaction.isAnnotationObject, michael@0: null); michael@0: michael@0: /** michael@0: * Internal helper for implementing the execute method of NewBookmark, NewFolder michael@0: * and NewSeparator. michael@0: * michael@0: * @param aTransaction michael@0: * The transaction object michael@0: * @param aParentGUID michael@0: * The guid of the parent folder michael@0: * @param aCreateItemFunction(aParentId, aGUIDToRestore) michael@0: * The function to be called for creating the item on execute and redo. michael@0: * It should return the itemId for the new item michael@0: * - aGUIDToRestore - the GUID to set for the item (used for redo). michael@0: * @param [optional] aOnUndo michael@0: * an additional function to call after undo michael@0: * @param [optional] aOnRedo michael@0: * an additional function to call after redo michael@0: */ michael@0: function* ExecuteCreateItem(aTransaction, aParentGUID, aCreateItemFunction, michael@0: aOnUndo = null, aOnRedo = null) { michael@0: let parentId = yield PlacesUtils.promiseItemId(aParentGUID), michael@0: itemId = yield aCreateItemFunction(parentId, ""), michael@0: guid = yield PlacesUtils.promiseItemGUID(itemId); michael@0: michael@0: // On redo, we'll restore the date-added and last-modified properties. michael@0: let dateAdded = 0, lastModified = 0; michael@0: aTransaction.undo = function* () { michael@0: if (dateAdded == 0) { michael@0: dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId); michael@0: lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId); michael@0: } michael@0: PlacesUtils.bookmarks.removeItem(itemId); michael@0: if (aOnUndo) { michael@0: yield aOnUndo(); michael@0: } michael@0: }; michael@0: aTransaction.redo = function* () { michael@0: parentId = yield PlacesUtils.promiseItemId(aParentGUID); michael@0: itemId = yield aCreateItemFunction(parentId, guid); michael@0: if (aOnRedo) michael@0: yield aOnRedo(); michael@0: michael@0: // aOnRedo is called first to make sure it doesn't override michael@0: // lastModified. michael@0: PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded); michael@0: PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified); michael@0: }; michael@0: return guid; michael@0: } michael@0: michael@0: /***************************************************************************** michael@0: * The Standard Places Transactions. michael@0: * michael@0: * See the documentation at the top of this file. The valid values for input michael@0: * are also documented there. michael@0: *****************************************************************************/ michael@0: michael@0: let PT = PlacesTransactions; michael@0: michael@0: /** michael@0: * Transaction for creating a bookmark. michael@0: * michael@0: * Required Input Properties: uri, parentGUID. michael@0: * Optional Input Properties: index, title, keyword, annotations, tags. michael@0: * michael@0: * When this transaction is executed, it's resolved to the new bookmark's GUID. michael@0: */ michael@0: PT.NewBookmark = DefineTransaction(["parentGUID", "uri"], michael@0: ["index", "title", "keyword", "postData", michael@0: "annotations", "tags"]); michael@0: PT.NewBookmark.prototype = Object.seal({ michael@0: execute: function (aParentGUID, aURI, aIndex, aTitle, michael@0: aKeyword, aPostData, aAnnos, aTags) { michael@0: return ExecuteCreateItem(this, aParentGUID, michael@0: function (parentId, guidToRestore = "") { michael@0: let itemId = PlacesUtils.bookmarks.insertBookmark( michael@0: parentId, aURI, aIndex, aTitle, guidToRestore); michael@0: if (aKeyword) michael@0: PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword); michael@0: if (aPostData) michael@0: PlacesUtils.setPostDataForBookmark(itemId, aPostData); michael@0: if (aAnnos) michael@0: PlacesUtils.setAnnotationsForItem(itemId, aAnnos); michael@0: if (aTags && aTags.length > 0) { michael@0: let currentTags = PlacesUtils.tagging.getTagsForURI(aURI); michael@0: aTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)]; michael@0: PlacesUtils.tagging.tagURI(aURI, aTags); michael@0: } michael@0: michael@0: return itemId; michael@0: }, michael@0: function _additionalOnUndo() { michael@0: if (aTags && aTags.length > 0) michael@0: PlacesUtils.tagging.untagURI(aURI, aTags); michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for creating a folder. michael@0: * michael@0: * Required Input Properties: title, parentGUID. michael@0: * Optional Input Properties: index, annotations. michael@0: * michael@0: * When this transaction is executed, it's resolved to the new folder's GUID. michael@0: */ michael@0: PT.NewFolder = DefineTransaction(["parentGUID", "title"], michael@0: ["index", "annotations"]); michael@0: PT.NewFolder.prototype = Object.seal({ michael@0: execute: function (aParentGUID, aTitle, aIndex, aAnnos) { michael@0: return ExecuteCreateItem(this, aParentGUID, michael@0: function(parentId, guidToRestore = "") { michael@0: let itemId = PlacesUtils.bookmarks.createFolder( michael@0: parentId, aTitle, aIndex, guidToRestore); michael@0: if (aAnnos) michael@0: PlacesUtils.setAnnotationsForItem(itemId, aAnnos); michael@0: return itemId; michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for creating a separator. michael@0: * michael@0: * Required Input Properties: parentGUID. michael@0: * Optional Input Properties: index. michael@0: * michael@0: * When this transaction is executed, it's resolved to the new separator's michael@0: * GUID. michael@0: */ michael@0: PT.NewSeparator = DefineTransaction(["parentGUID"], ["index"]); michael@0: PT.NewSeparator.prototype = Object.seal({ michael@0: execute: function (aParentGUID, aIndex) { michael@0: return ExecuteCreateItem(this, aParentGUID, michael@0: function (parentId, guidToRestore = "") { michael@0: let itemId = PlacesUtils.bookmarks.insertSeparator( michael@0: parentId, aIndex, guidToRestore); michael@0: return itemId; michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the michael@0: * semantics). michael@0: * michael@0: * Required Input Properties: feedURI, title, parentGUID. michael@0: * Optional Input Properties: siteURI, index, annotations. michael@0: * michael@0: * When this transaction is executed, it's resolved to the new separators's michael@0: * GUID. michael@0: */ michael@0: PT.NewLivemark = DefineTransaction(["feedURI", "title", "parentGUID"], michael@0: ["siteURI", "index", "annotations"]); michael@0: PT.NewLivemark.prototype = Object.seal({ michael@0: execute: function* (aFeedURI, aTitle, aParentGUID, aSiteURI, aIndex, aAnnos) { michael@0: let createItem = function* (aGUID = "") { michael@0: let parentId = yield PlacesUtils.promiseItemId(aParentGUID); michael@0: let livemarkInfo = { michael@0: title: aTitle michael@0: , feedURI: aFeedURI michael@0: , parentId: parentId michael@0: , index: aIndex michael@0: , siteURI: aSiteURI }; michael@0: if (aGUID) michael@0: livemarkInfo.guid = aGUID; michael@0: michael@0: let livemark = yield PlacesUtils.livemarks.addLivemark(livemarkInfo); michael@0: if (aAnnos) michael@0: PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos); michael@0: michael@0: return livemark; michael@0: }; michael@0: michael@0: let guid = (yield createItem()).guid; michael@0: this.undo = function* () { michael@0: yield PlacesUtils.livemarks.removeLivemark({ guid: guid }); michael@0: }; michael@0: this.redo = function* () { michael@0: yield createItem(guid); michael@0: }; michael@0: return guid; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for moving an item. michael@0: * michael@0: * Required Input Properties: GUID, newParentGUID. michael@0: * Optional Input Properties newIndex. michael@0: */ michael@0: PT.MoveItem = DefineTransaction(["GUID", "newParentGUID"], ["newIndex"]); michael@0: PT.MoveItem.prototype = Object.seal({ michael@0: execute: function* (aGUID, aNewParentGUID, aNewIndex) { michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID), michael@0: oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId), michael@0: oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId), michael@0: newParentId = yield PlacesUtils.promiseItemId(aNewParentGUID); michael@0: michael@0: PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex); michael@0: michael@0: let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId); michael@0: this.undo = () => { 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 (newParentId == oldParentId && oldIndex > undoIndex) michael@0: PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1); michael@0: else michael@0: PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex); michael@0: }; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for setting the title for an item. michael@0: * michael@0: * Required Input Properties: GUID, title. michael@0: */ michael@0: PT.EditTitle = DefineTransaction(["GUID", "title"]); michael@0: PT.EditTitle.prototype = Object.seal({ michael@0: execute: function* (aGUID, aTitle) { michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID), michael@0: oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId); michael@0: PlacesUtils.bookmarks.setItemTitle(itemId, aTitle); michael@0: this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); }; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for setting the URI for an item. michael@0: * michael@0: * Required Input Properties: GUID, uri. michael@0: */ michael@0: PT.EditURI = DefineTransaction(["GUID", "uri"]); michael@0: PT.EditURI.prototype = Object.seal({ michael@0: execute: function* (aGUID, aURI) { michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID), michael@0: oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId), michael@0: oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI), michael@0: newURIAdditionalTags = null; michael@0: PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI); michael@0: michael@0: // Move tags from old URI to new URI. michael@0: if (oldURITags.length > 0) { michael@0: // Only untag the old URI if this is the only bookmark. michael@0: if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0) michael@0: PlacesUtils.tagging.untagURI(oldURI, oldURITags); michael@0: michael@0: let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI); michael@0: newURIAdditionalTags = [t for (t of oldURITags) michael@0: if (currentNewURITags.indexOf(t) == -1)]; michael@0: if (newURIAdditionalTags) michael@0: PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags); michael@0: } michael@0: michael@0: this.undo = () => { michael@0: PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI); michael@0: // Move tags from new URI to old URI. michael@0: if (oldURITags.length > 0) { michael@0: // Only untag the new URI if this is the only bookmark. michael@0: if (newURIAdditionalTags && newURIAdditionalTags.length > 0 && michael@0: PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) { michael@0: PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags); michael@0: } michael@0: michael@0: PlacesUtils.tagging.tagURI(oldURI, oldURITags); michael@0: } michael@0: }; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for setting an annotation for an item. michael@0: * michael@0: * Required Input Properties: GUID, annotationObject michael@0: */ michael@0: PT.SetItemAnnotation = DefineTransaction(["GUID", "annotationObject"]); michael@0: PT.SetItemAnnotation.prototype = { michael@0: execute: function* (aGUID, aAnno) { michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID), oldAnno; michael@0: if (PlacesUtils.annotations.itemHasAnnotation(itemId, aAnno.name)) { michael@0: // Fill the old anno if it is set. michael@0: let flags = {}, expires = {}; michael@0: PlacesUtils.annotations.getItemAnnotationInfo(itemId, aAnno.name, flags, michael@0: expires, { }); michael@0: let value = PlacesUtils.annotations.getItemAnnotation(itemId, aAnno.name); michael@0: oldAnno = { name: aAnno.name, flags: flags.value, michael@0: value: value, expires: expires.value }; michael@0: } michael@0: else { michael@0: // An unset value removes the annoation. michael@0: oldAnno = { name: aAnno.name }; michael@0: } michael@0: michael@0: PlacesUtils.setAnnotationsForItem(itemId, [aAnno]); michael@0: this.undo = () => { PlacesUtils.setAnnotationsForItem(itemId, [oldAnno]); }; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Transaction for setting the keyword for a bookmark. michael@0: * michael@0: * Required Input Properties: GUID, keyword. michael@0: */ michael@0: PT.EditKeyword = DefineTransaction(["GUID", "keyword"]); michael@0: PT.EditKeyword.prototype = Object.seal({ michael@0: execute: function* (aGUID, aKeyword) { michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID), michael@0: oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); michael@0: PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword); michael@0: this.undo = () => { michael@0: PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword); michael@0: }; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Transaction for sorting a folder by name. michael@0: * michael@0: * Required Input Properties: GUID. michael@0: */ michael@0: PT.SortByName = DefineTransaction(["GUID"]); michael@0: PT.SortByName.prototype = { michael@0: execute: function* (aGUID) { michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID), michael@0: oldOrder = [], // [itemId] = old index michael@0: contents = PlacesUtils.getFolderContents(itemId, false, false).root, michael@0: count = contents.childCount; michael@0: michael@0: // Sort between separators. michael@0: let newOrder = [], // nodes, in the new order. michael@0: preSep = []; // Temporary array for sorting each group of nodes. michael@0: let sortingMethod = (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 node = contents.getChild(i); michael@0: oldOrder[node.itemId] = i; michael@0: if (PlacesUtils.nodeIsSeparator(node)) { 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(node); michael@0: } michael@0: else michael@0: preSep.push(node); 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: this.undo = () => { michael@0: let callback = { michael@0: runBatched: function() { michael@0: for (let item in oldOrder) { michael@0: PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]); michael@0: } 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 removing an item (any type). michael@0: * michael@0: * Required Input Properties: GUID. michael@0: */ michael@0: PT.RemoveItem = DefineTransaction(["GUID"]); michael@0: PT.RemoveItem.prototype = { michael@0: execute: function* (aGUID) { michael@0: const bms = PlacesUtils.bookmarks; michael@0: michael@0: let itemsToRestoreOnUndo = []; michael@0: function* saveItemRestoreData(aItem, aNode = null) { michael@0: if (!aItem || !aItem.GUID) michael@0: throw new Error("invalid item object"); michael@0: michael@0: let itemId = aNode ? michael@0: aNode.itemId : yield PlacesUtils.promiseItemId(aItem.GUID); michael@0: if (itemId == -1) michael@0: throw new Error("Unexpected non-bookmarks node"); michael@0: michael@0: aItem.itemType = function() { michael@0: if (aNode) { michael@0: switch (aNode.type) { michael@0: case aNode.RESULT_TYPE_SEPARATOR: michael@0: return bms.TYPE_SEPARATOR; michael@0: case aNode.RESULT_TYPE_URI: // regular bookmarks michael@0: case aNode.RESULT_TYPE_FOLDER_SHORTCUT: // place:folder= bookmarks michael@0: case aNode.RESULT_TYPE_QUERY: // smart bookmarks michael@0: return bms.TYPE_BOOKMARK; michael@0: case aNode.RESULT_TYPE_FOLDER: michael@0: return bms.TYPE_FOLDER; michael@0: default: michael@0: throw new Error("Unexpected node type"); michael@0: } michael@0: } michael@0: return bms.getItemType(itemId); michael@0: }(); michael@0: michael@0: let node = aNode; michael@0: if (!node && aItem.itemType == bms.TYPE_FOLDER) michael@0: node = PlacesUtils.getFolderContents(itemId).root; michael@0: michael@0: // dateAdded, lastModified and annotations apply to all types. michael@0: aItem.dateAdded = node ? node.dateAdded : bms.getItemDateAdded(itemId); michael@0: aItem.lastModified = node ? michael@0: node.lastModified : bms.getItemLastModified(itemId); michael@0: aItem.annotations = PlacesUtils.getAnnotationsForItem(itemId); michael@0: michael@0: // For the first-level item, we don't have the parent. michael@0: if (!aItem.parentGUID) { michael@0: let parentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId); michael@0: aItem.parentGUID = yield PlacesUtils.promiseItemGUID(parentId); michael@0: // For the first-level item, we also need the index. michael@0: // Note: node.bookmarkIndex doesn't work for root nodes. michael@0: aItem.index = bms.getItemIndex(itemId); michael@0: } michael@0: michael@0: // Separators don't have titles. michael@0: if (aItem.itemType != bms.TYPE_SEPARATOR) { michael@0: aItem.title = node ? node.title : bms.getItemTitle(itemId); michael@0: michael@0: if (aItem.itemType == bms.TYPE_BOOKMARK) { michael@0: aItem.uri = michael@0: node ? NetUtil.newURI(node.uri) : bms.getBookmarkURI(itemId); michael@0: aItem.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); michael@0: michael@0: // This may be the last bookmark (excluding the tag-items themselves) michael@0: // for the URI, so we need to preserve the tags. michael@0: let tags = PlacesUtils.tagging.getTagsForURI(aItem.uri);; michael@0: if (tags.length > 0) michael@0: aItem.tags = tags; michael@0: } michael@0: else { // folder michael@0: // We always have the node for folders michael@0: aItem.readOnly = node.childrenReadOnly; michael@0: for (let i = 0; i < node.childCount; i++) { michael@0: let childNode = node.getChild(i); michael@0: let childItem = michael@0: { GUID: yield PlacesUtils.promiseItemGUID(childNode.itemId) michael@0: , parentGUID: aItem.GUID }; michael@0: itemsToRestoreOnUndo.push(childItem); michael@0: yield saveItemRestoreData(childItem, childNode); michael@0: } michael@0: node.containerOpen = false; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let item = { GUID: aGUID, parentGUID: null }; michael@0: itemsToRestoreOnUndo.push(item); michael@0: yield saveItemRestoreData(item); michael@0: michael@0: let itemId = yield PlacesUtils.promiseItemId(aGUID); michael@0: PlacesUtils.bookmarks.removeItem(itemId); michael@0: this.undo = function() { michael@0: for (let item of itemsToRestoreOnUndo) { michael@0: let parentId = yield PlacesUtils.promiseItemId(item.parentGUID); michael@0: let index = "index" in item ? michael@0: index : PlacesUtils.bookmarks.DEFAULT_INDEX; michael@0: let itemId; michael@0: if (item.itemType == bms.TYPE_SEPARATOR) { michael@0: itemId = bms.insertSeparator(parentId, index, item.GUID); michael@0: } michael@0: else if (item.itemType == bms.TYPE_BOOKMARK) { michael@0: itemId = bms.insertBookmark(parentId, item.uri, index, item.title, michael@0: item.GUID); michael@0: } michael@0: else { // folder michael@0: itemId = bms.createFolder(parentId, item.title, index, item.GUID); michael@0: } michael@0: michael@0: if (item.itemType == bms.TYPE_BOOKMARK) { michael@0: if (item.keyword) michael@0: bms.setKeywordForBookmark(itemId, item.keyword); michael@0: if ("tags" in item) michael@0: PlacesUtils.tagging.tagURI(item.uri, item.tags); michael@0: } michael@0: else if (item.readOnly === true) { michael@0: bms.setFolderReadonly(itemId, true); michael@0: } michael@0: michael@0: PlacesUtils.setAnnotationsForItem(itemId, item.annotations); michael@0: PlacesUtils.bookmarks.setItemDateAdded(itemId, item.dateAdded); michael@0: PlacesUtils.bookmarks.setItemLastModified(itemId, item.lastModified); michael@0: } michael@0: }; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Transaction for tagging a URI. michael@0: * michael@0: * Required Input Properties: uri, tags. michael@0: */ michael@0: PT.TagURI = DefineTransaction(["uri", "tags"]); michael@0: PT.TagURI.prototype = { michael@0: execute: function* (aURI, aTags) { michael@0: if (PlacesUtils.getMostRecentBookmarkForURI(aURI) == -1) { michael@0: // Tagging is only allowed for bookmarked URIs. michael@0: let unfileGUID = michael@0: yield PlacesUtils.promiseItemGUID(PlacesUtils.unfiledBookmarksFolderId); michael@0: let createTxn = TransactionsHistory.getRawTransaction( michael@0: PT.NewBookmark({ uri: aURI, tags: aTags, parentGUID: unfileGUID })); michael@0: yield createTxn.execute(); michael@0: this.undo = createTxn.undo.bind(createTxn); michael@0: this.redo = createTxn.redo.bind(createTxn); michael@0: } michael@0: else { michael@0: let currentTags = PlacesUtils.tagging.getTagsForURI(aURI); michael@0: let newTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)]; michael@0: PlacesUtils.tagging.tagURI(aURI, newTags); michael@0: this.undo = () => { PlacesUtils.tagging.untagURI(aURI, newTags); }; michael@0: this.redo = () => { PlacesUtils.tagging.tagURI(aURI, newTags); }; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Transaction for removing tags from a URI. michael@0: * michael@0: * Required Input Properties: uri. michael@0: * Optional Input Properties: tags. michael@0: * michael@0: * If |tags| is not set, all tags set for |uri| are removed. michael@0: */ michael@0: PT.UntagURI = DefineTransaction(["uri"], ["tags"]); michael@0: PT.UntagURI.prototype = { michael@0: execute: function* (aURI, aTags) { michael@0: let tagsSet = PlacesUtils.tagging.getTagsForURI(aURI); michael@0: michael@0: if (aTags && aTags.length > 0) michael@0: aTags = [t for (t of aTags) if (tagsSet.indexOf(t) != -1)]; michael@0: else michael@0: aTags = tagsSet; michael@0: michael@0: PlacesUtils.tagging.untagURI(aURI, aTags); michael@0: this.undo = () => { PlacesUtils.tagging.tagURI(aURI, aTags); }; michael@0: this.redo = () => { PlacesUtils.tagging.untagURI(aURI, aTags); }; michael@0: } michael@0: };