1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/PlacesTransactions.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1269 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["PlacesTransactions"]; 1.11 + 1.12 +/** 1.13 + * Overview 1.14 + * -------- 1.15 + * This modules serves as the transactions manager for Places, and implements 1.16 + * all the standard transactions for its UI commands (creating items, editing 1.17 + * various properties, etc.). It shares most of its semantics with common 1.18 + * command pattern implementations, the HTML5 Undo Manager in particular. 1.19 + * However, the asynchronous design of [future] Places APIs, combined with the 1.20 + * commitment to serialize all UI operations, makes things a little bit 1.21 + * different. For example, when |undo| is called in order to undo the top undo 1.22 + * entry, the caller cannot tell for sure what entry would it be because the 1.23 + * execution of some transaction is either in process, or queued. 1.24 + * 1.25 + * GUIDs and item-ids 1.26 + * ------------------- 1.27 + * The Bookmarks API still relies heavily on item-ids, but since those do not 1.28 + * play nicely with the concept of undo and redo (especially not in an 1.29 + * asynchronous environment), this API only accepts bookmark GUIDs, both for 1.30 + * input (e.g. for specifying the parent folder for a new bookmark) and for 1.31 + * output (when the GUID for such a bookmark is propagated). 1.32 + * 1.33 + * GUIDs are readily available when dealing with the "output" of this API and 1.34 + * when result nodes are used (see nsINavHistoryResultNode::bookmarkGUID). 1.35 + * If you only have item-ids in hand, use PlacesUtils.promiseItemGUID for 1.36 + * converting them. Should you need to convert them back into itemIds, use 1.37 + * PlacesUtils.promiseItemId. 1.38 + * 1.39 + * The Standard Transactions 1.40 + * ------------------------- 1.41 + * At the bottom of this module you will find implementations for all Places UI 1.42 + * commands (One should almost never fallback to raw Places APIs. Please file 1.43 + * a bug if you find anything uncovered). The transactions' constructors are 1.44 + * set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder). 1.45 + * The input for this constructors is taken in the form of a single argument 1.46 + * plain object. Input properties may be either required (e.g. the |keyword| 1.47 + * property for the EditKeyword transaction) or optional (e.g. the |keyword| 1.48 + * property for NewBookmark). Once a transaction is created, you may pass it 1.49 + * to |transact| or use it in the for batching (see next section). 1.50 + * 1.51 + * The constructors throw right away when any required input is missing or when 1.52 + * some input is invalid "on the surface" (e.g. GUID values are validated to be 1.53 + * 12-characters strings, but are not validated to point to existing item. Such 1.54 + * an error will reveal when the transaction is executed). 1.55 + * 1.56 + * To make things simple, a given input property has the same basic meaning and 1.57 + * valid values across all transactions which accept it in the input object. 1.58 + * Here is a list of all supported input properties along with their expected 1.59 + * values: 1.60 + * - uri: an nsIURI object. 1.61 + * - feedURI: an nsIURI object, holding the url for a live bookmark. 1.62 + * - siteURI: an nsIURI object, holding the url for the site with which 1.63 + * a live bookmark is associated. 1.64 + * - GUID, parentGUID, newParentGUID: a valid places GUID string. 1.65 + * - title: a string 1.66 + * - index, newIndex: the position of an item in its containing folder, 1.67 + * starting from 0. 1.68 + * integer and PlacesUtils.bookmarks.DEFAULT_INDEX 1.69 + * - annotationObject: see PlacesUtils.setAnnotationsForItem 1.70 + * - annotations: an array of annotation objects as above. 1.71 + * - tags: an array of strings. 1.72 + * 1.73 + * Batching transactions 1.74 + * --------------------- 1.75 + * Sometimes it is useful to "batch" or "merge" transactions. For example, 1.76 + * "Bookmark All Tabs" may be implemented as one NewFolder transaction followed 1.77 + * by numerous NewBookmark transactions - all to be undone or redone in a single 1.78 + * command. The |transact| method makes this possible using a generator 1.79 + * function as an input. These generators have the same semantics as in 1.80 + * Task.jsm except that when you yield a transaction, it's executed, and the 1.81 + * resolution (e.g. the new bookmark GUID) is sent to the generator so you can 1.82 + * use it as the input for another transaction. See |transact| for the details. 1.83 + * 1.84 + * "Custom" transactions 1.85 + * --------------------- 1.86 + * In the legacy transactions API it was possible to pass-in transactions 1.87 + * implemented "externally". For various reason this isn't allowed anymore: 1.88 + * transact throws right away if one attempts to pass a transaction that was not 1.89 + * created by this module. However, it's almost always possible to achieve the 1.90 + * same functionality with the batching technique described above. 1.91 + * 1.92 + * The transactions-history structure 1.93 + * ---------------------------------- 1.94 + * The transactions-history is a two-dimensional stack of transactions: the 1.95 + * transactions are ordered in reverse to the order they were committed. 1.96 + * It's two-dimensional because the undo manager allows batching transactions 1.97 + * together for the purpose of undo or redo (batched transactions can never be 1.98 + * undone or redone partially). 1.99 + * 1.100 + * The undoPosition property is set to the index of the top entry. If there is 1.101 + * no entry at that index, there is nothing to undo. 1.102 + * Entries prior to undoPosition, if any, are redo entries, the first one being 1.103 + * the top redo entry. 1.104 + * 1.105 + * [ [2nd redo txn, 1st redo txn], <= 2nd redo entry 1.106 + * [2nd redo txn, 1st redo txn], <= 1st redo entry 1.107 + * [1st undo txn, 2nd undo txn], <= 1st undo entry 1.108 + * [1st undo txn, 2nd undo txn] <= 2nd undo entry ] 1.109 + * undoPostion: 2. 1.110 + * 1.111 + * Note that when a new entry is created, all redo entries are removed. 1.112 + */ 1.113 + 1.114 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.115 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.116 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.117 + "resource://gre/modules/Promise.jsm"); 1.118 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.119 + "resource://gre/modules/Task.jsm"); 1.120 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.121 + "resource://gre/modules/NetUtil.jsm"); 1.122 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", 1.123 + "resource://gre/modules/PlacesUtils.jsm"); 1.124 +XPCOMUtils.defineLazyModuleGetter(this, "console", 1.125 + "resource://gre/modules/devtools/Console.jsm"); 1.126 + 1.127 +// Updates commands in the undo group of the active window commands. 1.128 +// Inactive windows commands will be updated on focus. 1.129 +function updateCommandsOnActiveWindow() { 1.130 + // Updating "undo" will cause a group update including "redo". 1.131 + try { 1.132 + let win = Services.focus.activeWindow; 1.133 + if (win) 1.134 + win.updateCommands("undo"); 1.135 + } 1.136 + catch(ex) { console.error(ex, "Couldn't update undo commands"); } 1.137 +} 1.138 + 1.139 +// The internal object for managing the transactions history. 1.140 +// The public API is included in PlacesTransactions. 1.141 +// TODO bug 982099: extending the array "properly" makes it painful to implement 1.142 +// getters. If/when ES6 gets proper array subclassing we can revise this. 1.143 +let TransactionsHistory = []; 1.144 +TransactionsHistory.__proto__ = { 1.145 + __proto__: Array.prototype, 1.146 + 1.147 + // The index of the first undo entry (if any) - See the documentation 1.148 + // at the top of this file. 1.149 + _undoPosition: 0, 1.150 + get undoPosition() this._undoPosition, 1.151 + 1.152 + // Handy shortcuts 1.153 + get topUndoEntry() this.undoPosition < this.length ? 1.154 + this[this.undoPosition] : null, 1.155 + get topRedoEntry() this.undoPosition > 0 ? 1.156 + this[this.undoPosition - 1] : null, 1.157 + 1.158 + // Outside of this module, the API of transactions is inaccessible, and so 1.159 + // are any internal properties. To achieve that, transactions are proxified 1.160 + // in their constructors. This maps the proxies to their respective raw 1.161 + // objects. 1.162 + proxifiedToRaw: new WeakMap(), 1.163 + 1.164 + /** 1.165 + * Proxify a transaction object for consumers. 1.166 + * @param aRawTransaction 1.167 + * the raw transaction object. 1.168 + * @return the proxified transaction object. 1.169 + * @see getRawTransaction for retrieving the raw transaction. 1.170 + */ 1.171 + proxifyTransaction: function (aRawTransaction) { 1.172 + let proxy = Object.freeze({}); 1.173 + this.proxifiedToRaw.set(proxy, aRawTransaction); 1.174 + return proxy; 1.175 + }, 1.176 + 1.177 + /** 1.178 + * Check if the given object is a the proxy object for some transaction. 1.179 + * @param aValue 1.180 + * any JS value. 1.181 + * @return true if aValue is the proxy object for some transaction, false 1.182 + * otherwise. 1.183 + */ 1.184 + isProxifiedTransactionObject: 1.185 + function (aValue) this.proxifiedToRaw.has(aValue), 1.186 + 1.187 + /** 1.188 + * Get the raw transaction for the given proxy. 1.189 + * @param aProxy 1.190 + * the proxy object 1.191 + * @return the transaction proxified by aProxy; |undefined| is returned if 1.192 + * aProxy is not a proxified transaction. 1.193 + */ 1.194 + getRawTransaction: function (aProxy) this.proxifiedToRaw.get(aProxy), 1.195 + 1.196 + /** 1.197 + * Undo the top undo entry, if any, and update the undo position accordingly. 1.198 + */ 1.199 + undo: function* () { 1.200 + let entry = this.topUndoEntry; 1.201 + if (!entry) 1.202 + return; 1.203 + 1.204 + for (let transaction of entry) { 1.205 + try { 1.206 + yield TransactionsHistory.getRawTransaction(transaction).undo(); 1.207 + } 1.208 + catch(ex) { 1.209 + // If one transaction is broken, it's not safe to work with any other 1.210 + // undo entry. Report the error and clear the undo history. 1.211 + console.error(ex, 1.212 + "Couldn't undo a transaction, clearing all undo entries."); 1.213 + this.clearUndoEntries(); 1.214 + return; 1.215 + } 1.216 + } 1.217 + this._undoPosition++; 1.218 + updateCommandsOnActiveWindow(); 1.219 + }, 1.220 + 1.221 + /** 1.222 + * Redo the top redo entry, if any, and update the undo position accordingly. 1.223 + */ 1.224 + redo: function* () { 1.225 + let entry = this.topRedoEntry; 1.226 + if (!entry) 1.227 + return; 1.228 + 1.229 + for (let i = entry.length - 1; i >= 0; i--) { 1.230 + let transaction = TransactionsHistory.getRawTransaction(entry[i]); 1.231 + try { 1.232 + if (transaction.redo) 1.233 + yield transaction.redo(); 1.234 + else 1.235 + yield transaction.execute(); 1.236 + } 1.237 + catch(ex) { 1.238 + // If one transaction is broken, it's not safe to work with any other 1.239 + // redo entry. Report the error and clear the undo history. 1.240 + console.error(ex, 1.241 + "Couldn't redo a transaction, clearing all redo entries."); 1.242 + this.clearRedoEntries(); 1.243 + return; 1.244 + } 1.245 + } 1.246 + this._undoPosition--; 1.247 + updateCommandsOnActiveWindow(); 1.248 + }, 1.249 + 1.250 + /** 1.251 + * Add a transaction either as a new entry, if forced or if there are no undo 1.252 + * entries, or to the top undo entry. 1.253 + * 1.254 + * @param aProxifiedTransaction 1.255 + * the proxified transaction object to be added to the transaction 1.256 + * history. 1.257 + * @param [optional] aForceNewEntry 1.258 + * Force a new entry for the transaction. Default: false. 1.259 + * If false, an entry will we created only if there's no undo entry 1.260 + * to extend. 1.261 + */ 1.262 + add: function (aProxifiedTransaction, aForceNewEntry = false) { 1.263 + if (!this.isProxifiedTransactionObject(aProxifiedTransaction)) 1.264 + throw new Error("aProxifiedTransaction is not a proxified transaction"); 1.265 + 1.266 + if (this.length == 0 || aForceNewEntry) { 1.267 + this.clearRedoEntries(); 1.268 + this.unshift([aProxifiedTransaction]); 1.269 + } 1.270 + else { 1.271 + this[this.undoPosition].unshift(aProxifiedTransaction); 1.272 + } 1.273 + updateCommandsOnActiveWindow(); 1.274 + }, 1.275 + 1.276 + /** 1.277 + * Clear all undo entries. 1.278 + */ 1.279 + clearUndoEntries: function () { 1.280 + if (this.undoPosition < this.length) 1.281 + this.splice(this.undoPosition); 1.282 + }, 1.283 + 1.284 + /** 1.285 + * Clear all redo entries. 1.286 + */ 1.287 + clearRedoEntries: function () { 1.288 + if (this.undoPosition > 0) { 1.289 + this.splice(0, this.undoPosition); 1.290 + this._undoPosition = 0; 1.291 + } 1.292 + }, 1.293 + 1.294 + /** 1.295 + * Clear all entries. 1.296 + */ 1.297 + clearAllEntries: function () { 1.298 + if (this.length > 0) { 1.299 + this.splice(0); 1.300 + this._undoPosition = 0; 1.301 + } 1.302 + } 1.303 +}; 1.304 + 1.305 + 1.306 +// Our transaction manager is asynchronous in the sense that all of its methods 1.307 +// don't execute synchronously. However, all actions must be serialized. 1.308 +let currentTask = Promise.resolve(); 1.309 +function Serialize(aTask) { 1.310 + // Ignore failures. 1.311 + return currentTask = currentTask.then( () => Task.spawn(aTask) ) 1.312 + .then(null, Components.utils.reportError); 1.313 +} 1.314 + 1.315 +// Transactions object should never be recycled (that is, |execute| should 1.316 +// only be called once, or not at all, after they're constructed. 1.317 +// This keeps track of all transactions which were executed. 1.318 +let executedTransactions = new WeakMap(); // TODO: use WeakSet (bug 792439) 1.319 +executedTransactions.add = k => executedTransactions.set(k, null); 1.320 + 1.321 +let PlacesTransactions = { 1.322 + /** 1.323 + * Asynchronously transact either a single transaction, or a sequence of 1.324 + * transactions that would be treated as a single entry in the transactions 1.325 + * history. 1.326 + * 1.327 + * @param aToTransact 1.328 + * Either a transaction object or a generator function (ES6-style only) 1.329 + * that yields transaction objects. 1.330 + * 1.331 + * Generator mode how-to: when a transaction is yielded, it's executed. 1.332 + * Then, if it was executed successfully, the resolution of |execute| 1.333 + * is sent to the generator. If |execute| threw or rejected, the 1.334 + * exception is propagated to the generator. 1.335 + * Any other value yielded by a generator function is handled the 1.336 + * same way as in a Task (see Task.jsm). 1.337 + * 1.338 + * @return {Promise} 1.339 + * @resolves either to the resolution of |execute|, in single-transaction mode, 1.340 + * or to the return value of the generator, in generator-mode. 1.341 + * @rejects either if |execute| threw, in single-transaction mode, or if 1.342 + * the generator function threw (or didn't handle) an exception, in generator 1.343 + * mode. 1.344 + * @throws if aTransactionOrGeneratorFunction is neither a transaction object 1.345 + * created by this module or a generator function. 1.346 + * @note If no transaction was executed successfully, the transactions history 1.347 + * is not affected. 1.348 + * 1.349 + * @note All PlacesTransactions operations are serialized. This means that the 1.350 + * transactions history state may change by the time the transaction/generator 1.351 + * is processed. It's guaranteed, however, that a generator function "blocks" 1.352 + * the queue: that is, it is assured that no other operations are performed 1.353 + * by or on PlacesTransactions until the generator returns. Keep in mind you 1.354 + * are not protected from consumers who use the raw places APIs directly. 1.355 + */ 1.356 + transact: function (aToTransact) { 1.357 + let isGeneratorObj = 1.358 + o => Object.prototype.toString.call(o) == "[object Generator]"; 1.359 + 1.360 + let generator = null; 1.361 + if (typeof(aToTransact) == "function") { 1.362 + generator = aToTransact(); 1.363 + if (!isGeneratorObj(generator)) 1.364 + throw new Error("aToTransact is not a generator function"); 1.365 + } 1.366 + else if (!TransactionsHistory.isProxifiedTransactionObject(aToTransact)) { 1.367 + throw new Error("aToTransact is not a valid transaction object"); 1.368 + } 1.369 + else if (executedTransactions.has(aToTransact)) { 1.370 + throw new Error("Transactions objects may not be recycled."); 1.371 + } 1.372 + 1.373 + return Serialize(function* () { 1.374 + // The entry in the transactions history is created once the first 1.375 + // transaction is committed. This means that if |transact| is called 1.376 + // in its "generator mode" and no transactions are committed by the 1.377 + // generator, the transactions history is left unchanged. 1.378 + // Bug 982115: Depending on how this API is actually used we may revise 1.379 + // this decision and make it so |transact| always forces a new entry. 1.380 + let forceNewEntry = true; 1.381 + function* transactOneTransaction(aTransaction) { 1.382 + let retval = 1.383 + yield TransactionsHistory.getRawTransaction(aTransaction).execute(); 1.384 + executedTransactions.add(aTransaction); 1.385 + TransactionsHistory.add(aTransaction, forceNewEntry); 1.386 + forceNewEntry = false; 1.387 + return retval; 1.388 + } 1.389 + 1.390 + function* transactBatch(aGenerator) { 1.391 + let error = false; 1.392 + let sendValue = undefined; 1.393 + while (true) { 1.394 + let next = error ? 1.395 + aGenerator.throw(sendValue) : aGenerator.next(sendValue); 1.396 + sendValue = next.value; 1.397 + if (isGeneratorObj(sendValue)) { 1.398 + sendValue = yield transactBatch(sendValue); 1.399 + } 1.400 + else if (typeof(sendValue) == "object" && sendValue) { 1.401 + if (TransactionsHistory.isProxifiedTransactionObject(sendValue)) { 1.402 + if (executedTransactions.has(sendValue)) { 1.403 + sendValue = new Error("Transactions may not be recycled."); 1.404 + error = true; 1.405 + } 1.406 + else { 1.407 + sendValue = yield transactOneTransaction(sendValue); 1.408 + } 1.409 + } 1.410 + else if ("then" in sendValue) { 1.411 + sendValue = yield sendValue; 1.412 + } 1.413 + } 1.414 + if (next.done) 1.415 + break; 1.416 + } 1.417 + return sendValue; 1.418 + } 1.419 + 1.420 + if (generator) 1.421 + return yield transactBatch(generator); 1.422 + else 1.423 + return yield transactOneTransaction(aToTransact); 1.424 + }.bind(this)); 1.425 + }, 1.426 + 1.427 + /** 1.428 + * Asynchronously undo the transaction immediately after the current undo 1.429 + * position in the transactions history in the reverse order, if any, and 1.430 + * adjusts the undo position. 1.431 + * 1.432 + * @return {Promises). The promise always resolves. 1.433 + * @note All undo manager operations are queued. This means that transactions 1.434 + * history may change by the time your request is fulfilled. 1.435 + */ 1.436 + undo: function () Serialize(() => TransactionsHistory.undo()), 1.437 + 1.438 + /** 1.439 + * Asynchronously redo the transaction immediately before the current undo 1.440 + * position in the transactions history, if any, and adjusts the undo 1.441 + * position. 1.442 + * 1.443 + * @return {Promises). The promise always resolves. 1.444 + * @note All undo manager operations are queued. This means that transactions 1.445 + * history may change by the time your request is fulfilled. 1.446 + */ 1.447 + redo: function () Serialize(() => TransactionsHistory.redo()), 1.448 + 1.449 + /** 1.450 + * Asynchronously clear the undo, redo, or all entries from the transactions 1.451 + * history. 1.452 + * 1.453 + * @param [optional] aUndoEntries 1.454 + * Whether or not to clear undo entries. Default: true. 1.455 + * @param [optional] aRedoEntries 1.456 + * Whether or not to clear undo entries. Default: true. 1.457 + * 1.458 + * @return {Promises). The promise always resolves. 1.459 + * @throws if both aUndoEntries and aRedoEntries are false. 1.460 + * @note All undo manager operations are queued. This means that transactions 1.461 + * history may change by the time your request is fulfilled. 1.462 + */ 1.463 + clearTransactionsHistory: 1.464 + function (aUndoEntries = true, aRedoEntries = true) { 1.465 + return Serialize(function* () { 1.466 + if (aUndoEntries && aRedoEntries) 1.467 + TransactionsHistory.clearAllEntries(); 1.468 + else if (aUndoEntries) 1.469 + TransactionsHistory.clearUndoEntries(); 1.470 + else if (aRedoEntries) 1.471 + TransactionsHistory.clearRedoEntries(); 1.472 + else 1.473 + throw new Error("either aUndoEntries or aRedoEntries should be true"); 1.474 + }); 1.475 + }, 1.476 + 1.477 + /** 1.478 + * The numbers of entries in the transactions history. 1.479 + */ 1.480 + get length() TransactionsHistory.length, 1.481 + 1.482 + /** 1.483 + * Get the transaction history entry at a given index. Each entry consists 1.484 + * of one or more transaction objects. 1.485 + * 1.486 + * @param aIndex 1.487 + * the index of the entry to retrieve. 1.488 + * @return an array of transaction objects in their undo order (that is, 1.489 + * reversely to the order they were executed). 1.490 + * @throw if aIndex is invalid (< 0 or >= length). 1.491 + * @note the returned array is a clone of the history entry and is not 1.492 + * kept in sync with the original entry if it changes. 1.493 + */ 1.494 + entry: function (aIndex) { 1.495 + if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length) 1.496 + throw new Error("Invalid index"); 1.497 + 1.498 + return TransactionsHistory[aIndex]; 1.499 + }, 1.500 + 1.501 + /** 1.502 + * The index of the top undo entry in the transactions history. 1.503 + * If there are no undo entries, it equals to |length|. 1.504 + * Entries past this point 1.505 + * Entries at and past this point are redo entries. 1.506 + */ 1.507 + get undoPosition() TransactionsHistory.undoPosition, 1.508 + 1.509 + /** 1.510 + * Shortcut for accessing the top undo entry in the transaction history. 1.511 + */ 1.512 + get topUndoEntry() TransactionsHistory.topUndoEntry, 1.513 + 1.514 + /** 1.515 + * Shortcut for accessing the top redo entry in the transaction history. 1.516 + */ 1.517 + get topRedoEntry() TransactionsHistory.topRedoEntry 1.518 +}; 1.519 + 1.520 +/** 1.521 + * Internal helper for defining the standard transactions and their input. 1.522 + * It takes the required and optional properties, and generates the public 1.523 + * constructor (which takes the input in the form of a plain object) which, 1.524 + * when called, creates the argument-less "public" |execute| method by binding 1.525 + * the input properties to the function arguments (required properties first, 1.526 + * then the optional properties). 1.527 + * 1.528 + * If this seems confusing, look at the consumers. 1.529 + * 1.530 + * This magic serves two purposes: 1.531 + * (1) It completely hides the transactions' internals from the module 1.532 + * consumers. 1.533 + * (2) It keeps each transaction implementation to what is about, bypassing 1.534 + * all this bureaucracy while still validating input appropriately. 1.535 + */ 1.536 +function DefineTransaction(aRequiredProps = [], aOptionalProps = []) { 1.537 + for (let prop of [...aRequiredProps, ...aOptionalProps]) { 1.538 + if (!DefineTransaction.inputProps.has(prop)) 1.539 + throw new Error("Property '" + prop + "' is not defined"); 1.540 + } 1.541 + 1.542 + let ctor = function (aInput) { 1.543 + // We want to support both syntaxes: 1.544 + // let t = new PlacesTransactions.NewBookmark(), 1.545 + // let t = PlacesTransactions.NewBookmark() 1.546 + if (this == PlacesTransactions) 1.547 + return new ctor(aInput); 1.548 + 1.549 + if (aRequiredProps.length > 0 || aOptionalProps.length > 0) { 1.550 + // Bind the input properties to the arguments of execute. 1.551 + let input = DefineTransaction.verifyInput(aInput, aRequiredProps, 1.552 + aOptionalProps); 1.553 + let executeArgs = [this, 1.554 + ...[input[prop] for (prop of aRequiredProps)], 1.555 + ...[input[prop] for (prop of aOptionalProps)]]; 1.556 + this.execute = Function.bind.apply(this.execute, executeArgs); 1.557 + } 1.558 + return TransactionsHistory.proxifyTransaction(this); 1.559 + }; 1.560 + return ctor; 1.561 +} 1.562 + 1.563 +DefineTransaction.isStr = v => typeof(v) == "string"; 1.564 +DefineTransaction.isURI = v => v instanceof Components.interfaces.nsIURI; 1.565 +DefineTransaction.isIndex = v => Number.isInteger(v) && 1.566 + v >= PlacesUtils.bookmarks.DEFAULT_INDEX; 1.567 +DefineTransaction.isGUID = v => /^[a-zA-Z0-9\-_]{12}$/.test(v); 1.568 +DefineTransaction.isPrimitive = v => v === null || (typeof(v) != "object" && 1.569 + typeof(v) != "function"); 1.570 +DefineTransaction.isAnnotationObject = function (obj) { 1.571 + let checkProperty = (aPropName, aRequired, aCheckFunc) => { 1.572 + if (aPropName in obj) 1.573 + return aCheckFunc(obj[aPropName]); 1.574 + 1.575 + return !aRequired; 1.576 + }; 1.577 + 1.578 + if (obj && 1.579 + checkProperty("name", true, DefineTransaction.isStr) && 1.580 + checkProperty("expires", false, Number.isInteger) && 1.581 + checkProperty("flags", false, Number.isInteger) && 1.582 + checkProperty("value", false, DefineTransaction.isPrimitive) ) { 1.583 + // Nothing else should be set 1.584 + let validKeys = ["name", "value", "flags", "expires"]; 1.585 + if (Object.keys(obj).every( (k) => validKeys.indexOf(k) != -1 )) 1.586 + return true; 1.587 + } 1.588 + return false; 1.589 +}; 1.590 + 1.591 +DefineTransaction.inputProps = new Map(); 1.592 +DefineTransaction.defineInputProps = 1.593 +function (aNames, aValidationFunction, aDefaultValue) { 1.594 + for (let name of aNames) { 1.595 + this.inputProps.set(name, { 1.596 + validate: aValidationFunction, 1.597 + defaultValue: aDefaultValue, 1.598 + isGUIDProp: false 1.599 + }); 1.600 + } 1.601 +}; 1.602 + 1.603 +DefineTransaction.defineArrayInputProp = 1.604 +function (aName, aValidationFunction, aDefaultValue) { 1.605 + this.inputProps.set(aName, { 1.606 + validate: (v) => Array.isArray(v) && v.every(aValidationFunction), 1.607 + defaultValue: aDefaultValue, 1.608 + isGUIDProp: false 1.609 + }); 1.610 +}; 1.611 + 1.612 +DefineTransaction.verifyPropertyValue = 1.613 +function (aProp, aValue, aRequired) { 1.614 + if (aValue === undefined) { 1.615 + if (aRequired) 1.616 + throw new Error("Required property is missing: " + aProp); 1.617 + return this.inputProps.get(aProp).defaultValue; 1.618 + } 1.619 + 1.620 + if (!this.inputProps.get(aProp).validate(aValue)) 1.621 + throw new Error("Invalid value for property: " + aProp); 1.622 + 1.623 + if (Array.isArray(aValue)) { 1.624 + // The original array cannot be referenced by this module because it would 1.625 + // then implicitly reference its global as well. 1.626 + return Components.utils.cloneInto(aValue, {}); 1.627 + } 1.628 + 1.629 + return aValue; 1.630 +}; 1.631 + 1.632 +DefineTransaction.verifyInput = 1.633 +function (aInput, aRequired = [], aOptional = []) { 1.634 + if (aRequired.length == 0 && aOptional.length == 0) 1.635 + return {}; 1.636 + 1.637 + // If there's just a single required/optional property, we allow passing it 1.638 + // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGUID) 1.639 + // rather than PlacesTransactions.RemoveItem({ GUID: myGUID}). 1.640 + // This shortcut isn't supported for "complex" properties - e.g. one cannot 1.641 + // pass an annotation object this way (note there is no use case for this at 1.642 + // the moment anyway). 1.643 + let isSinglePropertyInput = 1.644 + this.isPrimitive(aInput) || 1.645 + (aInput instanceof Components.interfaces.nsISupports); 1.646 + let fixedInput = { }; 1.647 + if (aRequired.length > 0) { 1.648 + if (isSinglePropertyInput) { 1.649 + if (aRequired.length == 1) { 1.650 + let prop = aRequired[0], value = aInput; 1.651 + value = this.verifyPropertyValue(prop, value, true); 1.652 + fixedInput[prop] = value; 1.653 + } 1.654 + else { 1.655 + throw new Error("Transaction input isn't an object"); 1.656 + } 1.657 + } 1.658 + else { 1.659 + for (let prop of aRequired) { 1.660 + let value = this.verifyPropertyValue(prop, aInput[prop], true); 1.661 + fixedInput[prop] = value; 1.662 + } 1.663 + } 1.664 + } 1.665 + 1.666 + if (aOptional.length > 0) { 1.667 + if (isSinglePropertyInput && !aRequired.length > 0) { 1.668 + if (aOptional.length == 1) { 1.669 + let prop = aOptional[0], value = aInput; 1.670 + value = this.verifyPropertyValue(prop, value, true); 1.671 + fixedInput[prop] = value; 1.672 + } 1.673 + else if (aInput !== null && aInput !== undefined) { 1.674 + throw new Error("Transaction input isn't an object"); 1.675 + } 1.676 + } 1.677 + else { 1.678 + for (let prop of aOptional) { 1.679 + let value = this.verifyPropertyValue(prop, aInput[prop], false); 1.680 + if (value !== undefined) 1.681 + fixedInput[prop] = value; 1.682 + else 1.683 + fixedInput[prop] = this.defaultValues[prop]; 1.684 + } 1.685 + } 1.686 + } 1.687 + 1.688 + return fixedInput; 1.689 +}; 1.690 + 1.691 +// Update the documentation at the top of this module if you add or 1.692 +// remove properties. 1.693 +DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"], 1.694 + DefineTransaction.isURI, null); 1.695 +DefineTransaction.defineInputProps(["GUID", "parentGUID", "newParentGUID"], 1.696 + DefineTransaction.isGUID); 1.697 +DefineTransaction.defineInputProps(["title", "keyword", "postData"], 1.698 + DefineTransaction.isStr, ""); 1.699 +DefineTransaction.defineInputProps(["index", "newIndex"], 1.700 + DefineTransaction.isIndex, 1.701 + PlacesUtils.bookmarks.DEFAULT_INDEX); 1.702 +DefineTransaction.defineInputProps(["annotationObject"], 1.703 + DefineTransaction.isAnnotationObject); 1.704 +DefineTransaction.defineArrayInputProp("tags", 1.705 + DefineTransaction.isStr, null); 1.706 +DefineTransaction.defineArrayInputProp("annotations", 1.707 + DefineTransaction.isAnnotationObject, 1.708 + null); 1.709 + 1.710 +/** 1.711 + * Internal helper for implementing the execute method of NewBookmark, NewFolder 1.712 + * and NewSeparator. 1.713 + * 1.714 + * @param aTransaction 1.715 + * The transaction object 1.716 + * @param aParentGUID 1.717 + * The guid of the parent folder 1.718 + * @param aCreateItemFunction(aParentId, aGUIDToRestore) 1.719 + * The function to be called for creating the item on execute and redo. 1.720 + * It should return the itemId for the new item 1.721 + * - aGUIDToRestore - the GUID to set for the item (used for redo). 1.722 + * @param [optional] aOnUndo 1.723 + * an additional function to call after undo 1.724 + * @param [optional] aOnRedo 1.725 + * an additional function to call after redo 1.726 + */ 1.727 +function* ExecuteCreateItem(aTransaction, aParentGUID, aCreateItemFunction, 1.728 + aOnUndo = null, aOnRedo = null) { 1.729 + let parentId = yield PlacesUtils.promiseItemId(aParentGUID), 1.730 + itemId = yield aCreateItemFunction(parentId, ""), 1.731 + guid = yield PlacesUtils.promiseItemGUID(itemId); 1.732 + 1.733 + // On redo, we'll restore the date-added and last-modified properties. 1.734 + let dateAdded = 0, lastModified = 0; 1.735 + aTransaction.undo = function* () { 1.736 + if (dateAdded == 0) { 1.737 + dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId); 1.738 + lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId); 1.739 + } 1.740 + PlacesUtils.bookmarks.removeItem(itemId); 1.741 + if (aOnUndo) { 1.742 + yield aOnUndo(); 1.743 + } 1.744 + }; 1.745 + aTransaction.redo = function* () { 1.746 + parentId = yield PlacesUtils.promiseItemId(aParentGUID); 1.747 + itemId = yield aCreateItemFunction(parentId, guid); 1.748 + if (aOnRedo) 1.749 + yield aOnRedo(); 1.750 + 1.751 + // aOnRedo is called first to make sure it doesn't override 1.752 + // lastModified. 1.753 + PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded); 1.754 + PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified); 1.755 + }; 1.756 + return guid; 1.757 +} 1.758 + 1.759 +/***************************************************************************** 1.760 + * The Standard Places Transactions. 1.761 + * 1.762 + * See the documentation at the top of this file. The valid values for input 1.763 + * are also documented there. 1.764 + *****************************************************************************/ 1.765 + 1.766 +let PT = PlacesTransactions; 1.767 + 1.768 +/** 1.769 + * Transaction for creating a bookmark. 1.770 + * 1.771 + * Required Input Properties: uri, parentGUID. 1.772 + * Optional Input Properties: index, title, keyword, annotations, tags. 1.773 + * 1.774 + * When this transaction is executed, it's resolved to the new bookmark's GUID. 1.775 + */ 1.776 +PT.NewBookmark = DefineTransaction(["parentGUID", "uri"], 1.777 + ["index", "title", "keyword", "postData", 1.778 + "annotations", "tags"]); 1.779 +PT.NewBookmark.prototype = Object.seal({ 1.780 + execute: function (aParentGUID, aURI, aIndex, aTitle, 1.781 + aKeyword, aPostData, aAnnos, aTags) { 1.782 + return ExecuteCreateItem(this, aParentGUID, 1.783 + function (parentId, guidToRestore = "") { 1.784 + let itemId = PlacesUtils.bookmarks.insertBookmark( 1.785 + parentId, aURI, aIndex, aTitle, guidToRestore); 1.786 + if (aKeyword) 1.787 + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword); 1.788 + if (aPostData) 1.789 + PlacesUtils.setPostDataForBookmark(itemId, aPostData); 1.790 + if (aAnnos) 1.791 + PlacesUtils.setAnnotationsForItem(itemId, aAnnos); 1.792 + if (aTags && aTags.length > 0) { 1.793 + let currentTags = PlacesUtils.tagging.getTagsForURI(aURI); 1.794 + aTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)]; 1.795 + PlacesUtils.tagging.tagURI(aURI, aTags); 1.796 + } 1.797 + 1.798 + return itemId; 1.799 + }, 1.800 + function _additionalOnUndo() { 1.801 + if (aTags && aTags.length > 0) 1.802 + PlacesUtils.tagging.untagURI(aURI, aTags); 1.803 + }); 1.804 + } 1.805 +}); 1.806 + 1.807 +/** 1.808 + * Transaction for creating a folder. 1.809 + * 1.810 + * Required Input Properties: title, parentGUID. 1.811 + * Optional Input Properties: index, annotations. 1.812 + * 1.813 + * When this transaction is executed, it's resolved to the new folder's GUID. 1.814 + */ 1.815 +PT.NewFolder = DefineTransaction(["parentGUID", "title"], 1.816 + ["index", "annotations"]); 1.817 +PT.NewFolder.prototype = Object.seal({ 1.818 + execute: function (aParentGUID, aTitle, aIndex, aAnnos) { 1.819 + return ExecuteCreateItem(this, aParentGUID, 1.820 + function(parentId, guidToRestore = "") { 1.821 + let itemId = PlacesUtils.bookmarks.createFolder( 1.822 + parentId, aTitle, aIndex, guidToRestore); 1.823 + if (aAnnos) 1.824 + PlacesUtils.setAnnotationsForItem(itemId, aAnnos); 1.825 + return itemId; 1.826 + }); 1.827 + } 1.828 +}); 1.829 + 1.830 +/** 1.831 + * Transaction for creating a separator. 1.832 + * 1.833 + * Required Input Properties: parentGUID. 1.834 + * Optional Input Properties: index. 1.835 + * 1.836 + * When this transaction is executed, it's resolved to the new separator's 1.837 + * GUID. 1.838 + */ 1.839 +PT.NewSeparator = DefineTransaction(["parentGUID"], ["index"]); 1.840 +PT.NewSeparator.prototype = Object.seal({ 1.841 + execute: function (aParentGUID, aIndex) { 1.842 + return ExecuteCreateItem(this, aParentGUID, 1.843 + function (parentId, guidToRestore = "") { 1.844 + let itemId = PlacesUtils.bookmarks.insertSeparator( 1.845 + parentId, aIndex, guidToRestore); 1.846 + return itemId; 1.847 + }); 1.848 + } 1.849 +}); 1.850 + 1.851 +/** 1.852 + * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the 1.853 + * semantics). 1.854 + * 1.855 + * Required Input Properties: feedURI, title, parentGUID. 1.856 + * Optional Input Properties: siteURI, index, annotations. 1.857 + * 1.858 + * When this transaction is executed, it's resolved to the new separators's 1.859 + * GUID. 1.860 + */ 1.861 +PT.NewLivemark = DefineTransaction(["feedURI", "title", "parentGUID"], 1.862 + ["siteURI", "index", "annotations"]); 1.863 +PT.NewLivemark.prototype = Object.seal({ 1.864 + execute: function* (aFeedURI, aTitle, aParentGUID, aSiteURI, aIndex, aAnnos) { 1.865 + let createItem = function* (aGUID = "") { 1.866 + let parentId = yield PlacesUtils.promiseItemId(aParentGUID); 1.867 + let livemarkInfo = { 1.868 + title: aTitle 1.869 + , feedURI: aFeedURI 1.870 + , parentId: parentId 1.871 + , index: aIndex 1.872 + , siteURI: aSiteURI }; 1.873 + if (aGUID) 1.874 + livemarkInfo.guid = aGUID; 1.875 + 1.876 + let livemark = yield PlacesUtils.livemarks.addLivemark(livemarkInfo); 1.877 + if (aAnnos) 1.878 + PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos); 1.879 + 1.880 + return livemark; 1.881 + }; 1.882 + 1.883 + let guid = (yield createItem()).guid; 1.884 + this.undo = function* () { 1.885 + yield PlacesUtils.livemarks.removeLivemark({ guid: guid }); 1.886 + }; 1.887 + this.redo = function* () { 1.888 + yield createItem(guid); 1.889 + }; 1.890 + return guid; 1.891 + } 1.892 +}); 1.893 + 1.894 +/** 1.895 + * Transaction for moving an item. 1.896 + * 1.897 + * Required Input Properties: GUID, newParentGUID. 1.898 + * Optional Input Properties newIndex. 1.899 + */ 1.900 +PT.MoveItem = DefineTransaction(["GUID", "newParentGUID"], ["newIndex"]); 1.901 +PT.MoveItem.prototype = Object.seal({ 1.902 + execute: function* (aGUID, aNewParentGUID, aNewIndex) { 1.903 + let itemId = yield PlacesUtils.promiseItemId(aGUID), 1.904 + oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId), 1.905 + oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId), 1.906 + newParentId = yield PlacesUtils.promiseItemId(aNewParentGUID); 1.907 + 1.908 + PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex); 1.909 + 1.910 + let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId); 1.911 + this.undo = () => { 1.912 + // Moving down in the same parent takes in count removal of the item 1.913 + // so to revert positions we must move to oldIndex + 1 1.914 + if (newParentId == oldParentId && oldIndex > undoIndex) 1.915 + PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1); 1.916 + else 1.917 + PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex); 1.918 + }; 1.919 + } 1.920 +}); 1.921 + 1.922 +/** 1.923 + * Transaction for setting the title for an item. 1.924 + * 1.925 + * Required Input Properties: GUID, title. 1.926 + */ 1.927 +PT.EditTitle = DefineTransaction(["GUID", "title"]); 1.928 +PT.EditTitle.prototype = Object.seal({ 1.929 + execute: function* (aGUID, aTitle) { 1.930 + let itemId = yield PlacesUtils.promiseItemId(aGUID), 1.931 + oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId); 1.932 + PlacesUtils.bookmarks.setItemTitle(itemId, aTitle); 1.933 + this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); }; 1.934 + } 1.935 +}); 1.936 + 1.937 +/** 1.938 + * Transaction for setting the URI for an item. 1.939 + * 1.940 + * Required Input Properties: GUID, uri. 1.941 + */ 1.942 +PT.EditURI = DefineTransaction(["GUID", "uri"]); 1.943 +PT.EditURI.prototype = Object.seal({ 1.944 + execute: function* (aGUID, aURI) { 1.945 + let itemId = yield PlacesUtils.promiseItemId(aGUID), 1.946 + oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId), 1.947 + oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI), 1.948 + newURIAdditionalTags = null; 1.949 + PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI); 1.950 + 1.951 + // Move tags from old URI to new URI. 1.952 + if (oldURITags.length > 0) { 1.953 + // Only untag the old URI if this is the only bookmark. 1.954 + if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0) 1.955 + PlacesUtils.tagging.untagURI(oldURI, oldURITags); 1.956 + 1.957 + let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI); 1.958 + newURIAdditionalTags = [t for (t of oldURITags) 1.959 + if (currentNewURITags.indexOf(t) == -1)]; 1.960 + if (newURIAdditionalTags) 1.961 + PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags); 1.962 + } 1.963 + 1.964 + this.undo = () => { 1.965 + PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI); 1.966 + // Move tags from new URI to old URI. 1.967 + if (oldURITags.length > 0) { 1.968 + // Only untag the new URI if this is the only bookmark. 1.969 + if (newURIAdditionalTags && newURIAdditionalTags.length > 0 && 1.970 + PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) { 1.971 + PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags); 1.972 + } 1.973 + 1.974 + PlacesUtils.tagging.tagURI(oldURI, oldURITags); 1.975 + } 1.976 + }; 1.977 + } 1.978 +}); 1.979 + 1.980 +/** 1.981 + * Transaction for setting an annotation for an item. 1.982 + * 1.983 + * Required Input Properties: GUID, annotationObject 1.984 + */ 1.985 +PT.SetItemAnnotation = DefineTransaction(["GUID", "annotationObject"]); 1.986 +PT.SetItemAnnotation.prototype = { 1.987 + execute: function* (aGUID, aAnno) { 1.988 + let itemId = yield PlacesUtils.promiseItemId(aGUID), oldAnno; 1.989 + if (PlacesUtils.annotations.itemHasAnnotation(itemId, aAnno.name)) { 1.990 + // Fill the old anno if it is set. 1.991 + let flags = {}, expires = {}; 1.992 + PlacesUtils.annotations.getItemAnnotationInfo(itemId, aAnno.name, flags, 1.993 + expires, { }); 1.994 + let value = PlacesUtils.annotations.getItemAnnotation(itemId, aAnno.name); 1.995 + oldAnno = { name: aAnno.name, flags: flags.value, 1.996 + value: value, expires: expires.value }; 1.997 + } 1.998 + else { 1.999 + // An unset value removes the annoation. 1.1000 + oldAnno = { name: aAnno.name }; 1.1001 + } 1.1002 + 1.1003 + PlacesUtils.setAnnotationsForItem(itemId, [aAnno]); 1.1004 + this.undo = () => { PlacesUtils.setAnnotationsForItem(itemId, [oldAnno]); }; 1.1005 + } 1.1006 +}; 1.1007 + 1.1008 +/** 1.1009 + * Transaction for setting the keyword for a bookmark. 1.1010 + * 1.1011 + * Required Input Properties: GUID, keyword. 1.1012 + */ 1.1013 +PT.EditKeyword = DefineTransaction(["GUID", "keyword"]); 1.1014 +PT.EditKeyword.prototype = Object.seal({ 1.1015 + execute: function* (aGUID, aKeyword) { 1.1016 + let itemId = yield PlacesUtils.promiseItemId(aGUID), 1.1017 + oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); 1.1018 + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword); 1.1019 + this.undo = () => { 1.1020 + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword); 1.1021 + }; 1.1022 + } 1.1023 +}); 1.1024 + 1.1025 +/** 1.1026 + * Transaction for sorting a folder by name. 1.1027 + * 1.1028 + * Required Input Properties: GUID. 1.1029 + */ 1.1030 +PT.SortByName = DefineTransaction(["GUID"]); 1.1031 +PT.SortByName.prototype = { 1.1032 + execute: function* (aGUID) { 1.1033 + let itemId = yield PlacesUtils.promiseItemId(aGUID), 1.1034 + oldOrder = [], // [itemId] = old index 1.1035 + contents = PlacesUtils.getFolderContents(itemId, false, false).root, 1.1036 + count = contents.childCount; 1.1037 + 1.1038 + // Sort between separators. 1.1039 + let newOrder = [], // nodes, in the new order. 1.1040 + preSep = []; // Temporary array for sorting each group of nodes. 1.1041 + let sortingMethod = (a, b) => { 1.1042 + if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b)) 1.1043 + return -1; 1.1044 + if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b)) 1.1045 + return 1; 1.1046 + return a.title.localeCompare(b.title); 1.1047 + }; 1.1048 + 1.1049 + for (let i = 0; i < count; ++i) { 1.1050 + let node = contents.getChild(i); 1.1051 + oldOrder[node.itemId] = i; 1.1052 + if (PlacesUtils.nodeIsSeparator(node)) { 1.1053 + if (preSep.length > 0) { 1.1054 + preSep.sort(sortingMethod); 1.1055 + newOrder = newOrder.concat(preSep); 1.1056 + preSep.splice(0, preSep.length); 1.1057 + } 1.1058 + newOrder.push(node); 1.1059 + } 1.1060 + else 1.1061 + preSep.push(node); 1.1062 + } 1.1063 + contents.containerOpen = false; 1.1064 + 1.1065 + if (preSep.length > 0) { 1.1066 + preSep.sort(sortingMethod); 1.1067 + newOrder = newOrder.concat(preSep); 1.1068 + } 1.1069 + 1.1070 + // Set the nex indexes. 1.1071 + let callback = { 1.1072 + runBatched: function() { 1.1073 + for (let i = 0; i < newOrder.length; ++i) { 1.1074 + PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i); 1.1075 + } 1.1076 + } 1.1077 + }; 1.1078 + PlacesUtils.bookmarks.runInBatchMode(callback, null); 1.1079 + 1.1080 + this.undo = () => { 1.1081 + let callback = { 1.1082 + runBatched: function() { 1.1083 + for (let item in oldOrder) { 1.1084 + PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]); 1.1085 + } 1.1086 + } 1.1087 + }; 1.1088 + PlacesUtils.bookmarks.runInBatchMode(callback, null); 1.1089 + }; 1.1090 + } 1.1091 +}; 1.1092 + 1.1093 +/** 1.1094 + * Transaction for removing an item (any type). 1.1095 + * 1.1096 + * Required Input Properties: GUID. 1.1097 + */ 1.1098 +PT.RemoveItem = DefineTransaction(["GUID"]); 1.1099 +PT.RemoveItem.prototype = { 1.1100 + execute: function* (aGUID) { 1.1101 + const bms = PlacesUtils.bookmarks; 1.1102 + 1.1103 + let itemsToRestoreOnUndo = []; 1.1104 + function* saveItemRestoreData(aItem, aNode = null) { 1.1105 + if (!aItem || !aItem.GUID) 1.1106 + throw new Error("invalid item object"); 1.1107 + 1.1108 + let itemId = aNode ? 1.1109 + aNode.itemId : yield PlacesUtils.promiseItemId(aItem.GUID); 1.1110 + if (itemId == -1) 1.1111 + throw new Error("Unexpected non-bookmarks node"); 1.1112 + 1.1113 + aItem.itemType = function() { 1.1114 + if (aNode) { 1.1115 + switch (aNode.type) { 1.1116 + case aNode.RESULT_TYPE_SEPARATOR: 1.1117 + return bms.TYPE_SEPARATOR; 1.1118 + case aNode.RESULT_TYPE_URI: // regular bookmarks 1.1119 + case aNode.RESULT_TYPE_FOLDER_SHORTCUT: // place:folder= bookmarks 1.1120 + case aNode.RESULT_TYPE_QUERY: // smart bookmarks 1.1121 + return bms.TYPE_BOOKMARK; 1.1122 + case aNode.RESULT_TYPE_FOLDER: 1.1123 + return bms.TYPE_FOLDER; 1.1124 + default: 1.1125 + throw new Error("Unexpected node type"); 1.1126 + } 1.1127 + } 1.1128 + return bms.getItemType(itemId); 1.1129 + }(); 1.1130 + 1.1131 + let node = aNode; 1.1132 + if (!node && aItem.itemType == bms.TYPE_FOLDER) 1.1133 + node = PlacesUtils.getFolderContents(itemId).root; 1.1134 + 1.1135 + // dateAdded, lastModified and annotations apply to all types. 1.1136 + aItem.dateAdded = node ? node.dateAdded : bms.getItemDateAdded(itemId); 1.1137 + aItem.lastModified = node ? 1.1138 + node.lastModified : bms.getItemLastModified(itemId); 1.1139 + aItem.annotations = PlacesUtils.getAnnotationsForItem(itemId); 1.1140 + 1.1141 + // For the first-level item, we don't have the parent. 1.1142 + if (!aItem.parentGUID) { 1.1143 + let parentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId); 1.1144 + aItem.parentGUID = yield PlacesUtils.promiseItemGUID(parentId); 1.1145 + // For the first-level item, we also need the index. 1.1146 + // Note: node.bookmarkIndex doesn't work for root nodes. 1.1147 + aItem.index = bms.getItemIndex(itemId); 1.1148 + } 1.1149 + 1.1150 + // Separators don't have titles. 1.1151 + if (aItem.itemType != bms.TYPE_SEPARATOR) { 1.1152 + aItem.title = node ? node.title : bms.getItemTitle(itemId); 1.1153 + 1.1154 + if (aItem.itemType == bms.TYPE_BOOKMARK) { 1.1155 + aItem.uri = 1.1156 + node ? NetUtil.newURI(node.uri) : bms.getBookmarkURI(itemId); 1.1157 + aItem.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); 1.1158 + 1.1159 + // This may be the last bookmark (excluding the tag-items themselves) 1.1160 + // for the URI, so we need to preserve the tags. 1.1161 + let tags = PlacesUtils.tagging.getTagsForURI(aItem.uri);; 1.1162 + if (tags.length > 0) 1.1163 + aItem.tags = tags; 1.1164 + } 1.1165 + else { // folder 1.1166 + // We always have the node for folders 1.1167 + aItem.readOnly = node.childrenReadOnly; 1.1168 + for (let i = 0; i < node.childCount; i++) { 1.1169 + let childNode = node.getChild(i); 1.1170 + let childItem = 1.1171 + { GUID: yield PlacesUtils.promiseItemGUID(childNode.itemId) 1.1172 + , parentGUID: aItem.GUID }; 1.1173 + itemsToRestoreOnUndo.push(childItem); 1.1174 + yield saveItemRestoreData(childItem, childNode); 1.1175 + } 1.1176 + node.containerOpen = false; 1.1177 + } 1.1178 + } 1.1179 + } 1.1180 + 1.1181 + let item = { GUID: aGUID, parentGUID: null }; 1.1182 + itemsToRestoreOnUndo.push(item); 1.1183 + yield saveItemRestoreData(item); 1.1184 + 1.1185 + let itemId = yield PlacesUtils.promiseItemId(aGUID); 1.1186 + PlacesUtils.bookmarks.removeItem(itemId); 1.1187 + this.undo = function() { 1.1188 + for (let item of itemsToRestoreOnUndo) { 1.1189 + let parentId = yield PlacesUtils.promiseItemId(item.parentGUID); 1.1190 + let index = "index" in item ? 1.1191 + index : PlacesUtils.bookmarks.DEFAULT_INDEX; 1.1192 + let itemId; 1.1193 + if (item.itemType == bms.TYPE_SEPARATOR) { 1.1194 + itemId = bms.insertSeparator(parentId, index, item.GUID); 1.1195 + } 1.1196 + else if (item.itemType == bms.TYPE_BOOKMARK) { 1.1197 + itemId = bms.insertBookmark(parentId, item.uri, index, item.title, 1.1198 + item.GUID); 1.1199 + } 1.1200 + else { // folder 1.1201 + itemId = bms.createFolder(parentId, item.title, index, item.GUID); 1.1202 + } 1.1203 + 1.1204 + if (item.itemType == bms.TYPE_BOOKMARK) { 1.1205 + if (item.keyword) 1.1206 + bms.setKeywordForBookmark(itemId, item.keyword); 1.1207 + if ("tags" in item) 1.1208 + PlacesUtils.tagging.tagURI(item.uri, item.tags); 1.1209 + } 1.1210 + else if (item.readOnly === true) { 1.1211 + bms.setFolderReadonly(itemId, true); 1.1212 + } 1.1213 + 1.1214 + PlacesUtils.setAnnotationsForItem(itemId, item.annotations); 1.1215 + PlacesUtils.bookmarks.setItemDateAdded(itemId, item.dateAdded); 1.1216 + PlacesUtils.bookmarks.setItemLastModified(itemId, item.lastModified); 1.1217 + } 1.1218 + }; 1.1219 + } 1.1220 +}; 1.1221 + 1.1222 +/** 1.1223 + * Transaction for tagging a URI. 1.1224 + * 1.1225 + * Required Input Properties: uri, tags. 1.1226 + */ 1.1227 +PT.TagURI = DefineTransaction(["uri", "tags"]); 1.1228 +PT.TagURI.prototype = { 1.1229 + execute: function* (aURI, aTags) { 1.1230 + if (PlacesUtils.getMostRecentBookmarkForURI(aURI) == -1) { 1.1231 + // Tagging is only allowed for bookmarked URIs. 1.1232 + let unfileGUID = 1.1233 + yield PlacesUtils.promiseItemGUID(PlacesUtils.unfiledBookmarksFolderId); 1.1234 + let createTxn = TransactionsHistory.getRawTransaction( 1.1235 + PT.NewBookmark({ uri: aURI, tags: aTags, parentGUID: unfileGUID })); 1.1236 + yield createTxn.execute(); 1.1237 + this.undo = createTxn.undo.bind(createTxn); 1.1238 + this.redo = createTxn.redo.bind(createTxn); 1.1239 + } 1.1240 + else { 1.1241 + let currentTags = PlacesUtils.tagging.getTagsForURI(aURI); 1.1242 + let newTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)]; 1.1243 + PlacesUtils.tagging.tagURI(aURI, newTags); 1.1244 + this.undo = () => { PlacesUtils.tagging.untagURI(aURI, newTags); }; 1.1245 + this.redo = () => { PlacesUtils.tagging.tagURI(aURI, newTags); }; 1.1246 + } 1.1247 + } 1.1248 +}; 1.1249 + 1.1250 +/** 1.1251 + * Transaction for removing tags from a URI. 1.1252 + * 1.1253 + * Required Input Properties: uri. 1.1254 + * Optional Input Properties: tags. 1.1255 + * 1.1256 + * If |tags| is not set, all tags set for |uri| are removed. 1.1257 + */ 1.1258 +PT.UntagURI = DefineTransaction(["uri"], ["tags"]); 1.1259 +PT.UntagURI.prototype = { 1.1260 + execute: function* (aURI, aTags) { 1.1261 + let tagsSet = PlacesUtils.tagging.getTagsForURI(aURI); 1.1262 + 1.1263 + if (aTags && aTags.length > 0) 1.1264 + aTags = [t for (t of aTags) if (tagsSet.indexOf(t) != -1)]; 1.1265 + else 1.1266 + aTags = tagsSet; 1.1267 + 1.1268 + PlacesUtils.tagging.untagURI(aURI, aTags); 1.1269 + this.undo = () => { PlacesUtils.tagging.tagURI(aURI, aTags); }; 1.1270 + this.redo = () => { PlacesUtils.tagging.untagURI(aURI, aTags); }; 1.1271 + } 1.1272 +};