toolkit/components/places/PlacesTransactions.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
michael@0 8
michael@0 9 /**
michael@0 10 * Overview
michael@0 11 * --------
michael@0 12 * This modules serves as the transactions manager for Places, and implements
michael@0 13 * all the standard transactions for its UI commands (creating items, editing
michael@0 14 * various properties, etc.). It shares most of its semantics with common
michael@0 15 * command pattern implementations, the HTML5 Undo Manager in particular.
michael@0 16 * However, the asynchronous design of [future] Places APIs, combined with the
michael@0 17 * commitment to serialize all UI operations, makes things a little bit
michael@0 18 * different. For example, when |undo| is called in order to undo the top undo
michael@0 19 * entry, the caller cannot tell for sure what entry would it be because the
michael@0 20 * execution of some transaction is either in process, or queued.
michael@0 21 *
michael@0 22 * GUIDs and item-ids
michael@0 23 * -------------------
michael@0 24 * The Bookmarks API still relies heavily on item-ids, but since those do not
michael@0 25 * play nicely with the concept of undo and redo (especially not in an
michael@0 26 * asynchronous environment), this API only accepts bookmark GUIDs, both for
michael@0 27 * input (e.g. for specifying the parent folder for a new bookmark) and for
michael@0 28 * output (when the GUID for such a bookmark is propagated).
michael@0 29 *
michael@0 30 * GUIDs are readily available when dealing with the "output" of this API and
michael@0 31 * when result nodes are used (see nsINavHistoryResultNode::bookmarkGUID).
michael@0 32 * If you only have item-ids in hand, use PlacesUtils.promiseItemGUID for
michael@0 33 * converting them. Should you need to convert them back into itemIds, use
michael@0 34 * PlacesUtils.promiseItemId.
michael@0 35 *
michael@0 36 * The Standard Transactions
michael@0 37 * -------------------------
michael@0 38 * At the bottom of this module you will find implementations for all Places UI
michael@0 39 * commands (One should almost never fallback to raw Places APIs. Please file
michael@0 40 * a bug if you find anything uncovered). The transactions' constructors are
michael@0 41 * set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder).
michael@0 42 * The input for this constructors is taken in the form of a single argument
michael@0 43 * plain object. Input properties may be either required (e.g. the |keyword|
michael@0 44 * property for the EditKeyword transaction) or optional (e.g. the |keyword|
michael@0 45 * property for NewBookmark). Once a transaction is created, you may pass it
michael@0 46 * to |transact| or use it in the for batching (see next section).
michael@0 47 *
michael@0 48 * The constructors throw right away when any required input is missing or when
michael@0 49 * some input is invalid "on the surface" (e.g. GUID values are validated to be
michael@0 50 * 12-characters strings, but are not validated to point to existing item. Such
michael@0 51 * an error will reveal when the transaction is executed).
michael@0 52 *
michael@0 53 * To make things simple, a given input property has the same basic meaning and
michael@0 54 * valid values across all transactions which accept it in the input object.
michael@0 55 * Here is a list of all supported input properties along with their expected
michael@0 56 * values:
michael@0 57 * - uri: an nsIURI object.
michael@0 58 * - feedURI: an nsIURI object, holding the url for a live bookmark.
michael@0 59 * - siteURI: an nsIURI object, holding the url for the site with which
michael@0 60 * a live bookmark is associated.
michael@0 61 * - GUID, parentGUID, newParentGUID: a valid places GUID string.
michael@0 62 * - title: a string
michael@0 63 * - index, newIndex: the position of an item in its containing folder,
michael@0 64 * starting from 0.
michael@0 65 * integer and PlacesUtils.bookmarks.DEFAULT_INDEX
michael@0 66 * - annotationObject: see PlacesUtils.setAnnotationsForItem
michael@0 67 * - annotations: an array of annotation objects as above.
michael@0 68 * - tags: an array of strings.
michael@0 69 *
michael@0 70 * Batching transactions
michael@0 71 * ---------------------
michael@0 72 * Sometimes it is useful to "batch" or "merge" transactions. For example,
michael@0 73 * "Bookmark All Tabs" may be implemented as one NewFolder transaction followed
michael@0 74 * by numerous NewBookmark transactions - all to be undone or redone in a single
michael@0 75 * command. The |transact| method makes this possible using a generator
michael@0 76 * function as an input. These generators have the same semantics as in
michael@0 77 * Task.jsm except that when you yield a transaction, it's executed, and the
michael@0 78 * resolution (e.g. the new bookmark GUID) is sent to the generator so you can
michael@0 79 * use it as the input for another transaction. See |transact| for the details.
michael@0 80 *
michael@0 81 * "Custom" transactions
michael@0 82 * ---------------------
michael@0 83 * In the legacy transactions API it was possible to pass-in transactions
michael@0 84 * implemented "externally". For various reason this isn't allowed anymore:
michael@0 85 * transact throws right away if one attempts to pass a transaction that was not
michael@0 86 * created by this module. However, it's almost always possible to achieve the
michael@0 87 * same functionality with the batching technique described above.
michael@0 88 *
michael@0 89 * The transactions-history structure
michael@0 90 * ----------------------------------
michael@0 91 * The transactions-history is a two-dimensional stack of transactions: the
michael@0 92 * transactions are ordered in reverse to the order they were committed.
michael@0 93 * It's two-dimensional because the undo manager allows batching transactions
michael@0 94 * together for the purpose of undo or redo (batched transactions can never be
michael@0 95 * undone or redone partially).
michael@0 96 *
michael@0 97 * The undoPosition property is set to the index of the top entry. If there is
michael@0 98 * no entry at that index, there is nothing to undo.
michael@0 99 * Entries prior to undoPosition, if any, are redo entries, the first one being
michael@0 100 * the top redo entry.
michael@0 101 *
michael@0 102 * [ [2nd redo txn, 1st redo txn], <= 2nd redo entry
michael@0 103 * [2nd redo txn, 1st redo txn], <= 1st redo entry
michael@0 104 * [1st undo txn, 2nd undo txn], <= 1st undo entry
michael@0 105 * [1st undo txn, 2nd undo txn] <= 2nd undo entry ]
michael@0 106 * undoPostion: 2.
michael@0 107 *
michael@0 108 * Note that when a new entry is created, all redo entries are removed.
michael@0 109 */
michael@0 110
michael@0 111 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 112 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 113 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 114 "resource://gre/modules/Promise.jsm");
michael@0 115 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 116 "resource://gre/modules/Task.jsm");
michael@0 117 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 118 "resource://gre/modules/NetUtil.jsm");
michael@0 119 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
michael@0 120 "resource://gre/modules/PlacesUtils.jsm");
michael@0 121 XPCOMUtils.defineLazyModuleGetter(this, "console",
michael@0 122 "resource://gre/modules/devtools/Console.jsm");
michael@0 123
michael@0 124 // Updates commands in the undo group of the active window commands.
michael@0 125 // Inactive windows commands will be updated on focus.
michael@0 126 function updateCommandsOnActiveWindow() {
michael@0 127 // Updating "undo" will cause a group update including "redo".
michael@0 128 try {
michael@0 129 let win = Services.focus.activeWindow;
michael@0 130 if (win)
michael@0 131 win.updateCommands("undo");
michael@0 132 }
michael@0 133 catch(ex) { console.error(ex, "Couldn't update undo commands"); }
michael@0 134 }
michael@0 135
michael@0 136 // The internal object for managing the transactions history.
michael@0 137 // The public API is included in PlacesTransactions.
michael@0 138 // TODO bug 982099: extending the array "properly" makes it painful to implement
michael@0 139 // getters. If/when ES6 gets proper array subclassing we can revise this.
michael@0 140 let TransactionsHistory = [];
michael@0 141 TransactionsHistory.__proto__ = {
michael@0 142 __proto__: Array.prototype,
michael@0 143
michael@0 144 // The index of the first undo entry (if any) - See the documentation
michael@0 145 // at the top of this file.
michael@0 146 _undoPosition: 0,
michael@0 147 get undoPosition() this._undoPosition,
michael@0 148
michael@0 149 // Handy shortcuts
michael@0 150 get topUndoEntry() this.undoPosition < this.length ?
michael@0 151 this[this.undoPosition] : null,
michael@0 152 get topRedoEntry() this.undoPosition > 0 ?
michael@0 153 this[this.undoPosition - 1] : null,
michael@0 154
michael@0 155 // Outside of this module, the API of transactions is inaccessible, and so
michael@0 156 // are any internal properties. To achieve that, transactions are proxified
michael@0 157 // in their constructors. This maps the proxies to their respective raw
michael@0 158 // objects.
michael@0 159 proxifiedToRaw: new WeakMap(),
michael@0 160
michael@0 161 /**
michael@0 162 * Proxify a transaction object for consumers.
michael@0 163 * @param aRawTransaction
michael@0 164 * the raw transaction object.
michael@0 165 * @return the proxified transaction object.
michael@0 166 * @see getRawTransaction for retrieving the raw transaction.
michael@0 167 */
michael@0 168 proxifyTransaction: function (aRawTransaction) {
michael@0 169 let proxy = Object.freeze({});
michael@0 170 this.proxifiedToRaw.set(proxy, aRawTransaction);
michael@0 171 return proxy;
michael@0 172 },
michael@0 173
michael@0 174 /**
michael@0 175 * Check if the given object is a the proxy object for some transaction.
michael@0 176 * @param aValue
michael@0 177 * any JS value.
michael@0 178 * @return true if aValue is the proxy object for some transaction, false
michael@0 179 * otherwise.
michael@0 180 */
michael@0 181 isProxifiedTransactionObject:
michael@0 182 function (aValue) this.proxifiedToRaw.has(aValue),
michael@0 183
michael@0 184 /**
michael@0 185 * Get the raw transaction for the given proxy.
michael@0 186 * @param aProxy
michael@0 187 * the proxy object
michael@0 188 * @return the transaction proxified by aProxy; |undefined| is returned if
michael@0 189 * aProxy is not a proxified transaction.
michael@0 190 */
michael@0 191 getRawTransaction: function (aProxy) this.proxifiedToRaw.get(aProxy),
michael@0 192
michael@0 193 /**
michael@0 194 * Undo the top undo entry, if any, and update the undo position accordingly.
michael@0 195 */
michael@0 196 undo: function* () {
michael@0 197 let entry = this.topUndoEntry;
michael@0 198 if (!entry)
michael@0 199 return;
michael@0 200
michael@0 201 for (let transaction of entry) {
michael@0 202 try {
michael@0 203 yield TransactionsHistory.getRawTransaction(transaction).undo();
michael@0 204 }
michael@0 205 catch(ex) {
michael@0 206 // If one transaction is broken, it's not safe to work with any other
michael@0 207 // undo entry. Report the error and clear the undo history.
michael@0 208 console.error(ex,
michael@0 209 "Couldn't undo a transaction, clearing all undo entries.");
michael@0 210 this.clearUndoEntries();
michael@0 211 return;
michael@0 212 }
michael@0 213 }
michael@0 214 this._undoPosition++;
michael@0 215 updateCommandsOnActiveWindow();
michael@0 216 },
michael@0 217
michael@0 218 /**
michael@0 219 * Redo the top redo entry, if any, and update the undo position accordingly.
michael@0 220 */
michael@0 221 redo: function* () {
michael@0 222 let entry = this.topRedoEntry;
michael@0 223 if (!entry)
michael@0 224 return;
michael@0 225
michael@0 226 for (let i = entry.length - 1; i >= 0; i--) {
michael@0 227 let transaction = TransactionsHistory.getRawTransaction(entry[i]);
michael@0 228 try {
michael@0 229 if (transaction.redo)
michael@0 230 yield transaction.redo();
michael@0 231 else
michael@0 232 yield transaction.execute();
michael@0 233 }
michael@0 234 catch(ex) {
michael@0 235 // If one transaction is broken, it's not safe to work with any other
michael@0 236 // redo entry. Report the error and clear the undo history.
michael@0 237 console.error(ex,
michael@0 238 "Couldn't redo a transaction, clearing all redo entries.");
michael@0 239 this.clearRedoEntries();
michael@0 240 return;
michael@0 241 }
michael@0 242 }
michael@0 243 this._undoPosition--;
michael@0 244 updateCommandsOnActiveWindow();
michael@0 245 },
michael@0 246
michael@0 247 /**
michael@0 248 * Add a transaction either as a new entry, if forced or if there are no undo
michael@0 249 * entries, or to the top undo entry.
michael@0 250 *
michael@0 251 * @param aProxifiedTransaction
michael@0 252 * the proxified transaction object to be added to the transaction
michael@0 253 * history.
michael@0 254 * @param [optional] aForceNewEntry
michael@0 255 * Force a new entry for the transaction. Default: false.
michael@0 256 * If false, an entry will we created only if there's no undo entry
michael@0 257 * to extend.
michael@0 258 */
michael@0 259 add: function (aProxifiedTransaction, aForceNewEntry = false) {
michael@0 260 if (!this.isProxifiedTransactionObject(aProxifiedTransaction))
michael@0 261 throw new Error("aProxifiedTransaction is not a proxified transaction");
michael@0 262
michael@0 263 if (this.length == 0 || aForceNewEntry) {
michael@0 264 this.clearRedoEntries();
michael@0 265 this.unshift([aProxifiedTransaction]);
michael@0 266 }
michael@0 267 else {
michael@0 268 this[this.undoPosition].unshift(aProxifiedTransaction);
michael@0 269 }
michael@0 270 updateCommandsOnActiveWindow();
michael@0 271 },
michael@0 272
michael@0 273 /**
michael@0 274 * Clear all undo entries.
michael@0 275 */
michael@0 276 clearUndoEntries: function () {
michael@0 277 if (this.undoPosition < this.length)
michael@0 278 this.splice(this.undoPosition);
michael@0 279 },
michael@0 280
michael@0 281 /**
michael@0 282 * Clear all redo entries.
michael@0 283 */
michael@0 284 clearRedoEntries: function () {
michael@0 285 if (this.undoPosition > 0) {
michael@0 286 this.splice(0, this.undoPosition);
michael@0 287 this._undoPosition = 0;
michael@0 288 }
michael@0 289 },
michael@0 290
michael@0 291 /**
michael@0 292 * Clear all entries.
michael@0 293 */
michael@0 294 clearAllEntries: function () {
michael@0 295 if (this.length > 0) {
michael@0 296 this.splice(0);
michael@0 297 this._undoPosition = 0;
michael@0 298 }
michael@0 299 }
michael@0 300 };
michael@0 301
michael@0 302
michael@0 303 // Our transaction manager is asynchronous in the sense that all of its methods
michael@0 304 // don't execute synchronously. However, all actions must be serialized.
michael@0 305 let currentTask = Promise.resolve();
michael@0 306 function Serialize(aTask) {
michael@0 307 // Ignore failures.
michael@0 308 return currentTask = currentTask.then( () => Task.spawn(aTask) )
michael@0 309 .then(null, Components.utils.reportError);
michael@0 310 }
michael@0 311
michael@0 312 // Transactions object should never be recycled (that is, |execute| should
michael@0 313 // only be called once, or not at all, after they're constructed.
michael@0 314 // This keeps track of all transactions which were executed.
michael@0 315 let executedTransactions = new WeakMap(); // TODO: use WeakSet (bug 792439)
michael@0 316 executedTransactions.add = k => executedTransactions.set(k, null);
michael@0 317
michael@0 318 let PlacesTransactions = {
michael@0 319 /**
michael@0 320 * Asynchronously transact either a single transaction, or a sequence of
michael@0 321 * transactions that would be treated as a single entry in the transactions
michael@0 322 * history.
michael@0 323 *
michael@0 324 * @param aToTransact
michael@0 325 * Either a transaction object or a generator function (ES6-style only)
michael@0 326 * that yields transaction objects.
michael@0 327 *
michael@0 328 * Generator mode how-to: when a transaction is yielded, it's executed.
michael@0 329 * Then, if it was executed successfully, the resolution of |execute|
michael@0 330 * is sent to the generator. If |execute| threw or rejected, the
michael@0 331 * exception is propagated to the generator.
michael@0 332 * Any other value yielded by a generator function is handled the
michael@0 333 * same way as in a Task (see Task.jsm).
michael@0 334 *
michael@0 335 * @return {Promise}
michael@0 336 * @resolves either to the resolution of |execute|, in single-transaction mode,
michael@0 337 * or to the return value of the generator, in generator-mode.
michael@0 338 * @rejects either if |execute| threw, in single-transaction mode, or if
michael@0 339 * the generator function threw (or didn't handle) an exception, in generator
michael@0 340 * mode.
michael@0 341 * @throws if aTransactionOrGeneratorFunction is neither a transaction object
michael@0 342 * created by this module or a generator function.
michael@0 343 * @note If no transaction was executed successfully, the transactions history
michael@0 344 * is not affected.
michael@0 345 *
michael@0 346 * @note All PlacesTransactions operations are serialized. This means that the
michael@0 347 * transactions history state may change by the time the transaction/generator
michael@0 348 * is processed. It's guaranteed, however, that a generator function "blocks"
michael@0 349 * the queue: that is, it is assured that no other operations are performed
michael@0 350 * by or on PlacesTransactions until the generator returns. Keep in mind you
michael@0 351 * are not protected from consumers who use the raw places APIs directly.
michael@0 352 */
michael@0 353 transact: function (aToTransact) {
michael@0 354 let isGeneratorObj =
michael@0 355 o => Object.prototype.toString.call(o) == "[object Generator]";
michael@0 356
michael@0 357 let generator = null;
michael@0 358 if (typeof(aToTransact) == "function") {
michael@0 359 generator = aToTransact();
michael@0 360 if (!isGeneratorObj(generator))
michael@0 361 throw new Error("aToTransact is not a generator function");
michael@0 362 }
michael@0 363 else if (!TransactionsHistory.isProxifiedTransactionObject(aToTransact)) {
michael@0 364 throw new Error("aToTransact is not a valid transaction object");
michael@0 365 }
michael@0 366 else if (executedTransactions.has(aToTransact)) {
michael@0 367 throw new Error("Transactions objects may not be recycled.");
michael@0 368 }
michael@0 369
michael@0 370 return Serialize(function* () {
michael@0 371 // The entry in the transactions history is created once the first
michael@0 372 // transaction is committed. This means that if |transact| is called
michael@0 373 // in its "generator mode" and no transactions are committed by the
michael@0 374 // generator, the transactions history is left unchanged.
michael@0 375 // Bug 982115: Depending on how this API is actually used we may revise
michael@0 376 // this decision and make it so |transact| always forces a new entry.
michael@0 377 let forceNewEntry = true;
michael@0 378 function* transactOneTransaction(aTransaction) {
michael@0 379 let retval =
michael@0 380 yield TransactionsHistory.getRawTransaction(aTransaction).execute();
michael@0 381 executedTransactions.add(aTransaction);
michael@0 382 TransactionsHistory.add(aTransaction, forceNewEntry);
michael@0 383 forceNewEntry = false;
michael@0 384 return retval;
michael@0 385 }
michael@0 386
michael@0 387 function* transactBatch(aGenerator) {
michael@0 388 let error = false;
michael@0 389 let sendValue = undefined;
michael@0 390 while (true) {
michael@0 391 let next = error ?
michael@0 392 aGenerator.throw(sendValue) : aGenerator.next(sendValue);
michael@0 393 sendValue = next.value;
michael@0 394 if (isGeneratorObj(sendValue)) {
michael@0 395 sendValue = yield transactBatch(sendValue);
michael@0 396 }
michael@0 397 else if (typeof(sendValue) == "object" && sendValue) {
michael@0 398 if (TransactionsHistory.isProxifiedTransactionObject(sendValue)) {
michael@0 399 if (executedTransactions.has(sendValue)) {
michael@0 400 sendValue = new Error("Transactions may not be recycled.");
michael@0 401 error = true;
michael@0 402 }
michael@0 403 else {
michael@0 404 sendValue = yield transactOneTransaction(sendValue);
michael@0 405 }
michael@0 406 }
michael@0 407 else if ("then" in sendValue) {
michael@0 408 sendValue = yield sendValue;
michael@0 409 }
michael@0 410 }
michael@0 411 if (next.done)
michael@0 412 break;
michael@0 413 }
michael@0 414 return sendValue;
michael@0 415 }
michael@0 416
michael@0 417 if (generator)
michael@0 418 return yield transactBatch(generator);
michael@0 419 else
michael@0 420 return yield transactOneTransaction(aToTransact);
michael@0 421 }.bind(this));
michael@0 422 },
michael@0 423
michael@0 424 /**
michael@0 425 * Asynchronously undo the transaction immediately after the current undo
michael@0 426 * position in the transactions history in the reverse order, if any, and
michael@0 427 * adjusts the undo position.
michael@0 428 *
michael@0 429 * @return {Promises). The promise always resolves.
michael@0 430 * @note All undo manager operations are queued. This means that transactions
michael@0 431 * history may change by the time your request is fulfilled.
michael@0 432 */
michael@0 433 undo: function () Serialize(() => TransactionsHistory.undo()),
michael@0 434
michael@0 435 /**
michael@0 436 * Asynchronously redo the transaction immediately before the current undo
michael@0 437 * position in the transactions history, if any, and adjusts the undo
michael@0 438 * position.
michael@0 439 *
michael@0 440 * @return {Promises). The promise always resolves.
michael@0 441 * @note All undo manager operations are queued. This means that transactions
michael@0 442 * history may change by the time your request is fulfilled.
michael@0 443 */
michael@0 444 redo: function () Serialize(() => TransactionsHistory.redo()),
michael@0 445
michael@0 446 /**
michael@0 447 * Asynchronously clear the undo, redo, or all entries from the transactions
michael@0 448 * history.
michael@0 449 *
michael@0 450 * @param [optional] aUndoEntries
michael@0 451 * Whether or not to clear undo entries. Default: true.
michael@0 452 * @param [optional] aRedoEntries
michael@0 453 * Whether or not to clear undo entries. Default: true.
michael@0 454 *
michael@0 455 * @return {Promises). The promise always resolves.
michael@0 456 * @throws if both aUndoEntries and aRedoEntries are false.
michael@0 457 * @note All undo manager operations are queued. This means that transactions
michael@0 458 * history may change by the time your request is fulfilled.
michael@0 459 */
michael@0 460 clearTransactionsHistory:
michael@0 461 function (aUndoEntries = true, aRedoEntries = true) {
michael@0 462 return Serialize(function* () {
michael@0 463 if (aUndoEntries && aRedoEntries)
michael@0 464 TransactionsHistory.clearAllEntries();
michael@0 465 else if (aUndoEntries)
michael@0 466 TransactionsHistory.clearUndoEntries();
michael@0 467 else if (aRedoEntries)
michael@0 468 TransactionsHistory.clearRedoEntries();
michael@0 469 else
michael@0 470 throw new Error("either aUndoEntries or aRedoEntries should be true");
michael@0 471 });
michael@0 472 },
michael@0 473
michael@0 474 /**
michael@0 475 * The numbers of entries in the transactions history.
michael@0 476 */
michael@0 477 get length() TransactionsHistory.length,
michael@0 478
michael@0 479 /**
michael@0 480 * Get the transaction history entry at a given index. Each entry consists
michael@0 481 * of one or more transaction objects.
michael@0 482 *
michael@0 483 * @param aIndex
michael@0 484 * the index of the entry to retrieve.
michael@0 485 * @return an array of transaction objects in their undo order (that is,
michael@0 486 * reversely to the order they were executed).
michael@0 487 * @throw if aIndex is invalid (< 0 or >= length).
michael@0 488 * @note the returned array is a clone of the history entry and is not
michael@0 489 * kept in sync with the original entry if it changes.
michael@0 490 */
michael@0 491 entry: function (aIndex) {
michael@0 492 if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
michael@0 493 throw new Error("Invalid index");
michael@0 494
michael@0 495 return TransactionsHistory[aIndex];
michael@0 496 },
michael@0 497
michael@0 498 /**
michael@0 499 * The index of the top undo entry in the transactions history.
michael@0 500 * If there are no undo entries, it equals to |length|.
michael@0 501 * Entries past this point
michael@0 502 * Entries at and past this point are redo entries.
michael@0 503 */
michael@0 504 get undoPosition() TransactionsHistory.undoPosition,
michael@0 505
michael@0 506 /**
michael@0 507 * Shortcut for accessing the top undo entry in the transaction history.
michael@0 508 */
michael@0 509 get topUndoEntry() TransactionsHistory.topUndoEntry,
michael@0 510
michael@0 511 /**
michael@0 512 * Shortcut for accessing the top redo entry in the transaction history.
michael@0 513 */
michael@0 514 get topRedoEntry() TransactionsHistory.topRedoEntry
michael@0 515 };
michael@0 516
michael@0 517 /**
michael@0 518 * Internal helper for defining the standard transactions and their input.
michael@0 519 * It takes the required and optional properties, and generates the public
michael@0 520 * constructor (which takes the input in the form of a plain object) which,
michael@0 521 * when called, creates the argument-less "public" |execute| method by binding
michael@0 522 * the input properties to the function arguments (required properties first,
michael@0 523 * then the optional properties).
michael@0 524 *
michael@0 525 * If this seems confusing, look at the consumers.
michael@0 526 *
michael@0 527 * This magic serves two purposes:
michael@0 528 * (1) It completely hides the transactions' internals from the module
michael@0 529 * consumers.
michael@0 530 * (2) It keeps each transaction implementation to what is about, bypassing
michael@0 531 * all this bureaucracy while still validating input appropriately.
michael@0 532 */
michael@0 533 function DefineTransaction(aRequiredProps = [], aOptionalProps = []) {
michael@0 534 for (let prop of [...aRequiredProps, ...aOptionalProps]) {
michael@0 535 if (!DefineTransaction.inputProps.has(prop))
michael@0 536 throw new Error("Property '" + prop + "' is not defined");
michael@0 537 }
michael@0 538
michael@0 539 let ctor = function (aInput) {
michael@0 540 // We want to support both syntaxes:
michael@0 541 // let t = new PlacesTransactions.NewBookmark(),
michael@0 542 // let t = PlacesTransactions.NewBookmark()
michael@0 543 if (this == PlacesTransactions)
michael@0 544 return new ctor(aInput);
michael@0 545
michael@0 546 if (aRequiredProps.length > 0 || aOptionalProps.length > 0) {
michael@0 547 // Bind the input properties to the arguments of execute.
michael@0 548 let input = DefineTransaction.verifyInput(aInput, aRequiredProps,
michael@0 549 aOptionalProps);
michael@0 550 let executeArgs = [this,
michael@0 551 ...[input[prop] for (prop of aRequiredProps)],
michael@0 552 ...[input[prop] for (prop of aOptionalProps)]];
michael@0 553 this.execute = Function.bind.apply(this.execute, executeArgs);
michael@0 554 }
michael@0 555 return TransactionsHistory.proxifyTransaction(this);
michael@0 556 };
michael@0 557 return ctor;
michael@0 558 }
michael@0 559
michael@0 560 DefineTransaction.isStr = v => typeof(v) == "string";
michael@0 561 DefineTransaction.isURI = v => v instanceof Components.interfaces.nsIURI;
michael@0 562 DefineTransaction.isIndex = v => Number.isInteger(v) &&
michael@0 563 v >= PlacesUtils.bookmarks.DEFAULT_INDEX;
michael@0 564 DefineTransaction.isGUID = v => /^[a-zA-Z0-9\-_]{12}$/.test(v);
michael@0 565 DefineTransaction.isPrimitive = v => v === null || (typeof(v) != "object" &&
michael@0 566 typeof(v) != "function");
michael@0 567 DefineTransaction.isAnnotationObject = function (obj) {
michael@0 568 let checkProperty = (aPropName, aRequired, aCheckFunc) => {
michael@0 569 if (aPropName in obj)
michael@0 570 return aCheckFunc(obj[aPropName]);
michael@0 571
michael@0 572 return !aRequired;
michael@0 573 };
michael@0 574
michael@0 575 if (obj &&
michael@0 576 checkProperty("name", true, DefineTransaction.isStr) &&
michael@0 577 checkProperty("expires", false, Number.isInteger) &&
michael@0 578 checkProperty("flags", false, Number.isInteger) &&
michael@0 579 checkProperty("value", false, DefineTransaction.isPrimitive) ) {
michael@0 580 // Nothing else should be set
michael@0 581 let validKeys = ["name", "value", "flags", "expires"];
michael@0 582 if (Object.keys(obj).every( (k) => validKeys.indexOf(k) != -1 ))
michael@0 583 return true;
michael@0 584 }
michael@0 585 return false;
michael@0 586 };
michael@0 587
michael@0 588 DefineTransaction.inputProps = new Map();
michael@0 589 DefineTransaction.defineInputProps =
michael@0 590 function (aNames, aValidationFunction, aDefaultValue) {
michael@0 591 for (let name of aNames) {
michael@0 592 this.inputProps.set(name, {
michael@0 593 validate: aValidationFunction,
michael@0 594 defaultValue: aDefaultValue,
michael@0 595 isGUIDProp: false
michael@0 596 });
michael@0 597 }
michael@0 598 };
michael@0 599
michael@0 600 DefineTransaction.defineArrayInputProp =
michael@0 601 function (aName, aValidationFunction, aDefaultValue) {
michael@0 602 this.inputProps.set(aName, {
michael@0 603 validate: (v) => Array.isArray(v) && v.every(aValidationFunction),
michael@0 604 defaultValue: aDefaultValue,
michael@0 605 isGUIDProp: false
michael@0 606 });
michael@0 607 };
michael@0 608
michael@0 609 DefineTransaction.verifyPropertyValue =
michael@0 610 function (aProp, aValue, aRequired) {
michael@0 611 if (aValue === undefined) {
michael@0 612 if (aRequired)
michael@0 613 throw new Error("Required property is missing: " + aProp);
michael@0 614 return this.inputProps.get(aProp).defaultValue;
michael@0 615 }
michael@0 616
michael@0 617 if (!this.inputProps.get(aProp).validate(aValue))
michael@0 618 throw new Error("Invalid value for property: " + aProp);
michael@0 619
michael@0 620 if (Array.isArray(aValue)) {
michael@0 621 // The original array cannot be referenced by this module because it would
michael@0 622 // then implicitly reference its global as well.
michael@0 623 return Components.utils.cloneInto(aValue, {});
michael@0 624 }
michael@0 625
michael@0 626 return aValue;
michael@0 627 };
michael@0 628
michael@0 629 DefineTransaction.verifyInput =
michael@0 630 function (aInput, aRequired = [], aOptional = []) {
michael@0 631 if (aRequired.length == 0 && aOptional.length == 0)
michael@0 632 return {};
michael@0 633
michael@0 634 // If there's just a single required/optional property, we allow passing it
michael@0 635 // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGUID)
michael@0 636 // rather than PlacesTransactions.RemoveItem({ GUID: myGUID}).
michael@0 637 // This shortcut isn't supported for "complex" properties - e.g. one cannot
michael@0 638 // pass an annotation object this way (note there is no use case for this at
michael@0 639 // the moment anyway).
michael@0 640 let isSinglePropertyInput =
michael@0 641 this.isPrimitive(aInput) ||
michael@0 642 (aInput instanceof Components.interfaces.nsISupports);
michael@0 643 let fixedInput = { };
michael@0 644 if (aRequired.length > 0) {
michael@0 645 if (isSinglePropertyInput) {
michael@0 646 if (aRequired.length == 1) {
michael@0 647 let prop = aRequired[0], value = aInput;
michael@0 648 value = this.verifyPropertyValue(prop, value, true);
michael@0 649 fixedInput[prop] = value;
michael@0 650 }
michael@0 651 else {
michael@0 652 throw new Error("Transaction input isn't an object");
michael@0 653 }
michael@0 654 }
michael@0 655 else {
michael@0 656 for (let prop of aRequired) {
michael@0 657 let value = this.verifyPropertyValue(prop, aInput[prop], true);
michael@0 658 fixedInput[prop] = value;
michael@0 659 }
michael@0 660 }
michael@0 661 }
michael@0 662
michael@0 663 if (aOptional.length > 0) {
michael@0 664 if (isSinglePropertyInput && !aRequired.length > 0) {
michael@0 665 if (aOptional.length == 1) {
michael@0 666 let prop = aOptional[0], value = aInput;
michael@0 667 value = this.verifyPropertyValue(prop, value, true);
michael@0 668 fixedInput[prop] = value;
michael@0 669 }
michael@0 670 else if (aInput !== null && aInput !== undefined) {
michael@0 671 throw new Error("Transaction input isn't an object");
michael@0 672 }
michael@0 673 }
michael@0 674 else {
michael@0 675 for (let prop of aOptional) {
michael@0 676 let value = this.verifyPropertyValue(prop, aInput[prop], false);
michael@0 677 if (value !== undefined)
michael@0 678 fixedInput[prop] = value;
michael@0 679 else
michael@0 680 fixedInput[prop] = this.defaultValues[prop];
michael@0 681 }
michael@0 682 }
michael@0 683 }
michael@0 684
michael@0 685 return fixedInput;
michael@0 686 };
michael@0 687
michael@0 688 // Update the documentation at the top of this module if you add or
michael@0 689 // remove properties.
michael@0 690 DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"],
michael@0 691 DefineTransaction.isURI, null);
michael@0 692 DefineTransaction.defineInputProps(["GUID", "parentGUID", "newParentGUID"],
michael@0 693 DefineTransaction.isGUID);
michael@0 694 DefineTransaction.defineInputProps(["title", "keyword", "postData"],
michael@0 695 DefineTransaction.isStr, "");
michael@0 696 DefineTransaction.defineInputProps(["index", "newIndex"],
michael@0 697 DefineTransaction.isIndex,
michael@0 698 PlacesUtils.bookmarks.DEFAULT_INDEX);
michael@0 699 DefineTransaction.defineInputProps(["annotationObject"],
michael@0 700 DefineTransaction.isAnnotationObject);
michael@0 701 DefineTransaction.defineArrayInputProp("tags",
michael@0 702 DefineTransaction.isStr, null);
michael@0 703 DefineTransaction.defineArrayInputProp("annotations",
michael@0 704 DefineTransaction.isAnnotationObject,
michael@0 705 null);
michael@0 706
michael@0 707 /**
michael@0 708 * Internal helper for implementing the execute method of NewBookmark, NewFolder
michael@0 709 * and NewSeparator.
michael@0 710 *
michael@0 711 * @param aTransaction
michael@0 712 * The transaction object
michael@0 713 * @param aParentGUID
michael@0 714 * The guid of the parent folder
michael@0 715 * @param aCreateItemFunction(aParentId, aGUIDToRestore)
michael@0 716 * The function to be called for creating the item on execute and redo.
michael@0 717 * It should return the itemId for the new item
michael@0 718 * - aGUIDToRestore - the GUID to set for the item (used for redo).
michael@0 719 * @param [optional] aOnUndo
michael@0 720 * an additional function to call after undo
michael@0 721 * @param [optional] aOnRedo
michael@0 722 * an additional function to call after redo
michael@0 723 */
michael@0 724 function* ExecuteCreateItem(aTransaction, aParentGUID, aCreateItemFunction,
michael@0 725 aOnUndo = null, aOnRedo = null) {
michael@0 726 let parentId = yield PlacesUtils.promiseItemId(aParentGUID),
michael@0 727 itemId = yield aCreateItemFunction(parentId, ""),
michael@0 728 guid = yield PlacesUtils.promiseItemGUID(itemId);
michael@0 729
michael@0 730 // On redo, we'll restore the date-added and last-modified properties.
michael@0 731 let dateAdded = 0, lastModified = 0;
michael@0 732 aTransaction.undo = function* () {
michael@0 733 if (dateAdded == 0) {
michael@0 734 dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId);
michael@0 735 lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId);
michael@0 736 }
michael@0 737 PlacesUtils.bookmarks.removeItem(itemId);
michael@0 738 if (aOnUndo) {
michael@0 739 yield aOnUndo();
michael@0 740 }
michael@0 741 };
michael@0 742 aTransaction.redo = function* () {
michael@0 743 parentId = yield PlacesUtils.promiseItemId(aParentGUID);
michael@0 744 itemId = yield aCreateItemFunction(parentId, guid);
michael@0 745 if (aOnRedo)
michael@0 746 yield aOnRedo();
michael@0 747
michael@0 748 // aOnRedo is called first to make sure it doesn't override
michael@0 749 // lastModified.
michael@0 750 PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded);
michael@0 751 PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
michael@0 752 };
michael@0 753 return guid;
michael@0 754 }
michael@0 755
michael@0 756 /*****************************************************************************
michael@0 757 * The Standard Places Transactions.
michael@0 758 *
michael@0 759 * See the documentation at the top of this file. The valid values for input
michael@0 760 * are also documented there.
michael@0 761 *****************************************************************************/
michael@0 762
michael@0 763 let PT = PlacesTransactions;
michael@0 764
michael@0 765 /**
michael@0 766 * Transaction for creating a bookmark.
michael@0 767 *
michael@0 768 * Required Input Properties: uri, parentGUID.
michael@0 769 * Optional Input Properties: index, title, keyword, annotations, tags.
michael@0 770 *
michael@0 771 * When this transaction is executed, it's resolved to the new bookmark's GUID.
michael@0 772 */
michael@0 773 PT.NewBookmark = DefineTransaction(["parentGUID", "uri"],
michael@0 774 ["index", "title", "keyword", "postData",
michael@0 775 "annotations", "tags"]);
michael@0 776 PT.NewBookmark.prototype = Object.seal({
michael@0 777 execute: function (aParentGUID, aURI, aIndex, aTitle,
michael@0 778 aKeyword, aPostData, aAnnos, aTags) {
michael@0 779 return ExecuteCreateItem(this, aParentGUID,
michael@0 780 function (parentId, guidToRestore = "") {
michael@0 781 let itemId = PlacesUtils.bookmarks.insertBookmark(
michael@0 782 parentId, aURI, aIndex, aTitle, guidToRestore);
michael@0 783 if (aKeyword)
michael@0 784 PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
michael@0 785 if (aPostData)
michael@0 786 PlacesUtils.setPostDataForBookmark(itemId, aPostData);
michael@0 787 if (aAnnos)
michael@0 788 PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
michael@0 789 if (aTags && aTags.length > 0) {
michael@0 790 let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
michael@0 791 aTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)];
michael@0 792 PlacesUtils.tagging.tagURI(aURI, aTags);
michael@0 793 }
michael@0 794
michael@0 795 return itemId;
michael@0 796 },
michael@0 797 function _additionalOnUndo() {
michael@0 798 if (aTags && aTags.length > 0)
michael@0 799 PlacesUtils.tagging.untagURI(aURI, aTags);
michael@0 800 });
michael@0 801 }
michael@0 802 });
michael@0 803
michael@0 804 /**
michael@0 805 * Transaction for creating a folder.
michael@0 806 *
michael@0 807 * Required Input Properties: title, parentGUID.
michael@0 808 * Optional Input Properties: index, annotations.
michael@0 809 *
michael@0 810 * When this transaction is executed, it's resolved to the new folder's GUID.
michael@0 811 */
michael@0 812 PT.NewFolder = DefineTransaction(["parentGUID", "title"],
michael@0 813 ["index", "annotations"]);
michael@0 814 PT.NewFolder.prototype = Object.seal({
michael@0 815 execute: function (aParentGUID, aTitle, aIndex, aAnnos) {
michael@0 816 return ExecuteCreateItem(this, aParentGUID,
michael@0 817 function(parentId, guidToRestore = "") {
michael@0 818 let itemId = PlacesUtils.bookmarks.createFolder(
michael@0 819 parentId, aTitle, aIndex, guidToRestore);
michael@0 820 if (aAnnos)
michael@0 821 PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
michael@0 822 return itemId;
michael@0 823 });
michael@0 824 }
michael@0 825 });
michael@0 826
michael@0 827 /**
michael@0 828 * Transaction for creating a separator.
michael@0 829 *
michael@0 830 * Required Input Properties: parentGUID.
michael@0 831 * Optional Input Properties: index.
michael@0 832 *
michael@0 833 * When this transaction is executed, it's resolved to the new separator's
michael@0 834 * GUID.
michael@0 835 */
michael@0 836 PT.NewSeparator = DefineTransaction(["parentGUID"], ["index"]);
michael@0 837 PT.NewSeparator.prototype = Object.seal({
michael@0 838 execute: function (aParentGUID, aIndex) {
michael@0 839 return ExecuteCreateItem(this, aParentGUID,
michael@0 840 function (parentId, guidToRestore = "") {
michael@0 841 let itemId = PlacesUtils.bookmarks.insertSeparator(
michael@0 842 parentId, aIndex, guidToRestore);
michael@0 843 return itemId;
michael@0 844 });
michael@0 845 }
michael@0 846 });
michael@0 847
michael@0 848 /**
michael@0 849 * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
michael@0 850 * semantics).
michael@0 851 *
michael@0 852 * Required Input Properties: feedURI, title, parentGUID.
michael@0 853 * Optional Input Properties: siteURI, index, annotations.
michael@0 854 *
michael@0 855 * When this transaction is executed, it's resolved to the new separators's
michael@0 856 * GUID.
michael@0 857 */
michael@0 858 PT.NewLivemark = DefineTransaction(["feedURI", "title", "parentGUID"],
michael@0 859 ["siteURI", "index", "annotations"]);
michael@0 860 PT.NewLivemark.prototype = Object.seal({
michael@0 861 execute: function* (aFeedURI, aTitle, aParentGUID, aSiteURI, aIndex, aAnnos) {
michael@0 862 let createItem = function* (aGUID = "") {
michael@0 863 let parentId = yield PlacesUtils.promiseItemId(aParentGUID);
michael@0 864 let livemarkInfo = {
michael@0 865 title: aTitle
michael@0 866 , feedURI: aFeedURI
michael@0 867 , parentId: parentId
michael@0 868 , index: aIndex
michael@0 869 , siteURI: aSiteURI };
michael@0 870 if (aGUID)
michael@0 871 livemarkInfo.guid = aGUID;
michael@0 872
michael@0 873 let livemark = yield PlacesUtils.livemarks.addLivemark(livemarkInfo);
michael@0 874 if (aAnnos)
michael@0 875 PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos);
michael@0 876
michael@0 877 return livemark;
michael@0 878 };
michael@0 879
michael@0 880 let guid = (yield createItem()).guid;
michael@0 881 this.undo = function* () {
michael@0 882 yield PlacesUtils.livemarks.removeLivemark({ guid: guid });
michael@0 883 };
michael@0 884 this.redo = function* () {
michael@0 885 yield createItem(guid);
michael@0 886 };
michael@0 887 return guid;
michael@0 888 }
michael@0 889 });
michael@0 890
michael@0 891 /**
michael@0 892 * Transaction for moving an item.
michael@0 893 *
michael@0 894 * Required Input Properties: GUID, newParentGUID.
michael@0 895 * Optional Input Properties newIndex.
michael@0 896 */
michael@0 897 PT.MoveItem = DefineTransaction(["GUID", "newParentGUID"], ["newIndex"]);
michael@0 898 PT.MoveItem.prototype = Object.seal({
michael@0 899 execute: function* (aGUID, aNewParentGUID, aNewIndex) {
michael@0 900 let itemId = yield PlacesUtils.promiseItemId(aGUID),
michael@0 901 oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId),
michael@0 902 oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId),
michael@0 903 newParentId = yield PlacesUtils.promiseItemId(aNewParentGUID);
michael@0 904
michael@0 905 PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex);
michael@0 906
michael@0 907 let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId);
michael@0 908 this.undo = () => {
michael@0 909 // Moving down in the same parent takes in count removal of the item
michael@0 910 // so to revert positions we must move to oldIndex + 1
michael@0 911 if (newParentId == oldParentId && oldIndex > undoIndex)
michael@0 912 PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1);
michael@0 913 else
michael@0 914 PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex);
michael@0 915 };
michael@0 916 }
michael@0 917 });
michael@0 918
michael@0 919 /**
michael@0 920 * Transaction for setting the title for an item.
michael@0 921 *
michael@0 922 * Required Input Properties: GUID, title.
michael@0 923 */
michael@0 924 PT.EditTitle = DefineTransaction(["GUID", "title"]);
michael@0 925 PT.EditTitle.prototype = Object.seal({
michael@0 926 execute: function* (aGUID, aTitle) {
michael@0 927 let itemId = yield PlacesUtils.promiseItemId(aGUID),
michael@0 928 oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
michael@0 929 PlacesUtils.bookmarks.setItemTitle(itemId, aTitle);
michael@0 930 this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); };
michael@0 931 }
michael@0 932 });
michael@0 933
michael@0 934 /**
michael@0 935 * Transaction for setting the URI for an item.
michael@0 936 *
michael@0 937 * Required Input Properties: GUID, uri.
michael@0 938 */
michael@0 939 PT.EditURI = DefineTransaction(["GUID", "uri"]);
michael@0 940 PT.EditURI.prototype = Object.seal({
michael@0 941 execute: function* (aGUID, aURI) {
michael@0 942 let itemId = yield PlacesUtils.promiseItemId(aGUID),
michael@0 943 oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
michael@0 944 oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI),
michael@0 945 newURIAdditionalTags = null;
michael@0 946 PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI);
michael@0 947
michael@0 948 // Move tags from old URI to new URI.
michael@0 949 if (oldURITags.length > 0) {
michael@0 950 // Only untag the old URI if this is the only bookmark.
michael@0 951 if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0)
michael@0 952 PlacesUtils.tagging.untagURI(oldURI, oldURITags);
michael@0 953
michael@0 954 let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI);
michael@0 955 newURIAdditionalTags = [t for (t of oldURITags)
michael@0 956 if (currentNewURITags.indexOf(t) == -1)];
michael@0 957 if (newURIAdditionalTags)
michael@0 958 PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags);
michael@0 959 }
michael@0 960
michael@0 961 this.undo = () => {
michael@0 962 PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI);
michael@0 963 // Move tags from new URI to old URI.
michael@0 964 if (oldURITags.length > 0) {
michael@0 965 // Only untag the new URI if this is the only bookmark.
michael@0 966 if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
michael@0 967 PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) {
michael@0 968 PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags);
michael@0 969 }
michael@0 970
michael@0 971 PlacesUtils.tagging.tagURI(oldURI, oldURITags);
michael@0 972 }
michael@0 973 };
michael@0 974 }
michael@0 975 });
michael@0 976
michael@0 977 /**
michael@0 978 * Transaction for setting an annotation for an item.
michael@0 979 *
michael@0 980 * Required Input Properties: GUID, annotationObject
michael@0 981 */
michael@0 982 PT.SetItemAnnotation = DefineTransaction(["GUID", "annotationObject"]);
michael@0 983 PT.SetItemAnnotation.prototype = {
michael@0 984 execute: function* (aGUID, aAnno) {
michael@0 985 let itemId = yield PlacesUtils.promiseItemId(aGUID), oldAnno;
michael@0 986 if (PlacesUtils.annotations.itemHasAnnotation(itemId, aAnno.name)) {
michael@0 987 // Fill the old anno if it is set.
michael@0 988 let flags = {}, expires = {};
michael@0 989 PlacesUtils.annotations.getItemAnnotationInfo(itemId, aAnno.name, flags,
michael@0 990 expires, { });
michael@0 991 let value = PlacesUtils.annotations.getItemAnnotation(itemId, aAnno.name);
michael@0 992 oldAnno = { name: aAnno.name, flags: flags.value,
michael@0 993 value: value, expires: expires.value };
michael@0 994 }
michael@0 995 else {
michael@0 996 // An unset value removes the annoation.
michael@0 997 oldAnno = { name: aAnno.name };
michael@0 998 }
michael@0 999
michael@0 1000 PlacesUtils.setAnnotationsForItem(itemId, [aAnno]);
michael@0 1001 this.undo = () => { PlacesUtils.setAnnotationsForItem(itemId, [oldAnno]); };
michael@0 1002 }
michael@0 1003 };
michael@0 1004
michael@0 1005 /**
michael@0 1006 * Transaction for setting the keyword for a bookmark.
michael@0 1007 *
michael@0 1008 * Required Input Properties: GUID, keyword.
michael@0 1009 */
michael@0 1010 PT.EditKeyword = DefineTransaction(["GUID", "keyword"]);
michael@0 1011 PT.EditKeyword.prototype = Object.seal({
michael@0 1012 execute: function* (aGUID, aKeyword) {
michael@0 1013 let itemId = yield PlacesUtils.promiseItemId(aGUID),
michael@0 1014 oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
michael@0 1015 PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
michael@0 1016 this.undo = () => {
michael@0 1017 PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword);
michael@0 1018 };
michael@0 1019 }
michael@0 1020 });
michael@0 1021
michael@0 1022 /**
michael@0 1023 * Transaction for sorting a folder by name.
michael@0 1024 *
michael@0 1025 * Required Input Properties: GUID.
michael@0 1026 */
michael@0 1027 PT.SortByName = DefineTransaction(["GUID"]);
michael@0 1028 PT.SortByName.prototype = {
michael@0 1029 execute: function* (aGUID) {
michael@0 1030 let itemId = yield PlacesUtils.promiseItemId(aGUID),
michael@0 1031 oldOrder = [], // [itemId] = old index
michael@0 1032 contents = PlacesUtils.getFolderContents(itemId, false, false).root,
michael@0 1033 count = contents.childCount;
michael@0 1034
michael@0 1035 // Sort between separators.
michael@0 1036 let newOrder = [], // nodes, in the new order.
michael@0 1037 preSep = []; // Temporary array for sorting each group of nodes.
michael@0 1038 let sortingMethod = (a, b) => {
michael@0 1039 if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
michael@0 1040 return -1;
michael@0 1041 if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
michael@0 1042 return 1;
michael@0 1043 return a.title.localeCompare(b.title);
michael@0 1044 };
michael@0 1045
michael@0 1046 for (let i = 0; i < count; ++i) {
michael@0 1047 let node = contents.getChild(i);
michael@0 1048 oldOrder[node.itemId] = i;
michael@0 1049 if (PlacesUtils.nodeIsSeparator(node)) {
michael@0 1050 if (preSep.length > 0) {
michael@0 1051 preSep.sort(sortingMethod);
michael@0 1052 newOrder = newOrder.concat(preSep);
michael@0 1053 preSep.splice(0, preSep.length);
michael@0 1054 }
michael@0 1055 newOrder.push(node);
michael@0 1056 }
michael@0 1057 else
michael@0 1058 preSep.push(node);
michael@0 1059 }
michael@0 1060 contents.containerOpen = false;
michael@0 1061
michael@0 1062 if (preSep.length > 0) {
michael@0 1063 preSep.sort(sortingMethod);
michael@0 1064 newOrder = newOrder.concat(preSep);
michael@0 1065 }
michael@0 1066
michael@0 1067 // Set the nex indexes.
michael@0 1068 let callback = {
michael@0 1069 runBatched: function() {
michael@0 1070 for (let i = 0; i < newOrder.length; ++i) {
michael@0 1071 PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
michael@0 1072 }
michael@0 1073 }
michael@0 1074 };
michael@0 1075 PlacesUtils.bookmarks.runInBatchMode(callback, null);
michael@0 1076
michael@0 1077 this.undo = () => {
michael@0 1078 let callback = {
michael@0 1079 runBatched: function() {
michael@0 1080 for (let item in oldOrder) {
michael@0 1081 PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]);
michael@0 1082 }
michael@0 1083 }
michael@0 1084 };
michael@0 1085 PlacesUtils.bookmarks.runInBatchMode(callback, null);
michael@0 1086 };
michael@0 1087 }
michael@0 1088 };
michael@0 1089
michael@0 1090 /**
michael@0 1091 * Transaction for removing an item (any type).
michael@0 1092 *
michael@0 1093 * Required Input Properties: GUID.
michael@0 1094 */
michael@0 1095 PT.RemoveItem = DefineTransaction(["GUID"]);
michael@0 1096 PT.RemoveItem.prototype = {
michael@0 1097 execute: function* (aGUID) {
michael@0 1098 const bms = PlacesUtils.bookmarks;
michael@0 1099
michael@0 1100 let itemsToRestoreOnUndo = [];
michael@0 1101 function* saveItemRestoreData(aItem, aNode = null) {
michael@0 1102 if (!aItem || !aItem.GUID)
michael@0 1103 throw new Error("invalid item object");
michael@0 1104
michael@0 1105 let itemId = aNode ?
michael@0 1106 aNode.itemId : yield PlacesUtils.promiseItemId(aItem.GUID);
michael@0 1107 if (itemId == -1)
michael@0 1108 throw new Error("Unexpected non-bookmarks node");
michael@0 1109
michael@0 1110 aItem.itemType = function() {
michael@0 1111 if (aNode) {
michael@0 1112 switch (aNode.type) {
michael@0 1113 case aNode.RESULT_TYPE_SEPARATOR:
michael@0 1114 return bms.TYPE_SEPARATOR;
michael@0 1115 case aNode.RESULT_TYPE_URI: // regular bookmarks
michael@0 1116 case aNode.RESULT_TYPE_FOLDER_SHORTCUT: // place:folder= bookmarks
michael@0 1117 case aNode.RESULT_TYPE_QUERY: // smart bookmarks
michael@0 1118 return bms.TYPE_BOOKMARK;
michael@0 1119 case aNode.RESULT_TYPE_FOLDER:
michael@0 1120 return bms.TYPE_FOLDER;
michael@0 1121 default:
michael@0 1122 throw new Error("Unexpected node type");
michael@0 1123 }
michael@0 1124 }
michael@0 1125 return bms.getItemType(itemId);
michael@0 1126 }();
michael@0 1127
michael@0 1128 let node = aNode;
michael@0 1129 if (!node && aItem.itemType == bms.TYPE_FOLDER)
michael@0 1130 node = PlacesUtils.getFolderContents(itemId).root;
michael@0 1131
michael@0 1132 // dateAdded, lastModified and annotations apply to all types.
michael@0 1133 aItem.dateAdded = node ? node.dateAdded : bms.getItemDateAdded(itemId);
michael@0 1134 aItem.lastModified = node ?
michael@0 1135 node.lastModified : bms.getItemLastModified(itemId);
michael@0 1136 aItem.annotations = PlacesUtils.getAnnotationsForItem(itemId);
michael@0 1137
michael@0 1138 // For the first-level item, we don't have the parent.
michael@0 1139 if (!aItem.parentGUID) {
michael@0 1140 let parentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
michael@0 1141 aItem.parentGUID = yield PlacesUtils.promiseItemGUID(parentId);
michael@0 1142 // For the first-level item, we also need the index.
michael@0 1143 // Note: node.bookmarkIndex doesn't work for root nodes.
michael@0 1144 aItem.index = bms.getItemIndex(itemId);
michael@0 1145 }
michael@0 1146
michael@0 1147 // Separators don't have titles.
michael@0 1148 if (aItem.itemType != bms.TYPE_SEPARATOR) {
michael@0 1149 aItem.title = node ? node.title : bms.getItemTitle(itemId);
michael@0 1150
michael@0 1151 if (aItem.itemType == bms.TYPE_BOOKMARK) {
michael@0 1152 aItem.uri =
michael@0 1153 node ? NetUtil.newURI(node.uri) : bms.getBookmarkURI(itemId);
michael@0 1154 aItem.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
michael@0 1155
michael@0 1156 // This may be the last bookmark (excluding the tag-items themselves)
michael@0 1157 // for the URI, so we need to preserve the tags.
michael@0 1158 let tags = PlacesUtils.tagging.getTagsForURI(aItem.uri);;
michael@0 1159 if (tags.length > 0)
michael@0 1160 aItem.tags = tags;
michael@0 1161 }
michael@0 1162 else { // folder
michael@0 1163 // We always have the node for folders
michael@0 1164 aItem.readOnly = node.childrenReadOnly;
michael@0 1165 for (let i = 0; i < node.childCount; i++) {
michael@0 1166 let childNode = node.getChild(i);
michael@0 1167 let childItem =
michael@0 1168 { GUID: yield PlacesUtils.promiseItemGUID(childNode.itemId)
michael@0 1169 , parentGUID: aItem.GUID };
michael@0 1170 itemsToRestoreOnUndo.push(childItem);
michael@0 1171 yield saveItemRestoreData(childItem, childNode);
michael@0 1172 }
michael@0 1173 node.containerOpen = false;
michael@0 1174 }
michael@0 1175 }
michael@0 1176 }
michael@0 1177
michael@0 1178 let item = { GUID: aGUID, parentGUID: null };
michael@0 1179 itemsToRestoreOnUndo.push(item);
michael@0 1180 yield saveItemRestoreData(item);
michael@0 1181
michael@0 1182 let itemId = yield PlacesUtils.promiseItemId(aGUID);
michael@0 1183 PlacesUtils.bookmarks.removeItem(itemId);
michael@0 1184 this.undo = function() {
michael@0 1185 for (let item of itemsToRestoreOnUndo) {
michael@0 1186 let parentId = yield PlacesUtils.promiseItemId(item.parentGUID);
michael@0 1187 let index = "index" in item ?
michael@0 1188 index : PlacesUtils.bookmarks.DEFAULT_INDEX;
michael@0 1189 let itemId;
michael@0 1190 if (item.itemType == bms.TYPE_SEPARATOR) {
michael@0 1191 itemId = bms.insertSeparator(parentId, index, item.GUID);
michael@0 1192 }
michael@0 1193 else if (item.itemType == bms.TYPE_BOOKMARK) {
michael@0 1194 itemId = bms.insertBookmark(parentId, item.uri, index, item.title,
michael@0 1195 item.GUID);
michael@0 1196 }
michael@0 1197 else { // folder
michael@0 1198 itemId = bms.createFolder(parentId, item.title, index, item.GUID);
michael@0 1199 }
michael@0 1200
michael@0 1201 if (item.itemType == bms.TYPE_BOOKMARK) {
michael@0 1202 if (item.keyword)
michael@0 1203 bms.setKeywordForBookmark(itemId, item.keyword);
michael@0 1204 if ("tags" in item)
michael@0 1205 PlacesUtils.tagging.tagURI(item.uri, item.tags);
michael@0 1206 }
michael@0 1207 else if (item.readOnly === true) {
michael@0 1208 bms.setFolderReadonly(itemId, true);
michael@0 1209 }
michael@0 1210
michael@0 1211 PlacesUtils.setAnnotationsForItem(itemId, item.annotations);
michael@0 1212 PlacesUtils.bookmarks.setItemDateAdded(itemId, item.dateAdded);
michael@0 1213 PlacesUtils.bookmarks.setItemLastModified(itemId, item.lastModified);
michael@0 1214 }
michael@0 1215 };
michael@0 1216 }
michael@0 1217 };
michael@0 1218
michael@0 1219 /**
michael@0 1220 * Transaction for tagging a URI.
michael@0 1221 *
michael@0 1222 * Required Input Properties: uri, tags.
michael@0 1223 */
michael@0 1224 PT.TagURI = DefineTransaction(["uri", "tags"]);
michael@0 1225 PT.TagURI.prototype = {
michael@0 1226 execute: function* (aURI, aTags) {
michael@0 1227 if (PlacesUtils.getMostRecentBookmarkForURI(aURI) == -1) {
michael@0 1228 // Tagging is only allowed for bookmarked URIs.
michael@0 1229 let unfileGUID =
michael@0 1230 yield PlacesUtils.promiseItemGUID(PlacesUtils.unfiledBookmarksFolderId);
michael@0 1231 let createTxn = TransactionsHistory.getRawTransaction(
michael@0 1232 PT.NewBookmark({ uri: aURI, tags: aTags, parentGUID: unfileGUID }));
michael@0 1233 yield createTxn.execute();
michael@0 1234 this.undo = createTxn.undo.bind(createTxn);
michael@0 1235 this.redo = createTxn.redo.bind(createTxn);
michael@0 1236 }
michael@0 1237 else {
michael@0 1238 let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
michael@0 1239 let newTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)];
michael@0 1240 PlacesUtils.tagging.tagURI(aURI, newTags);
michael@0 1241 this.undo = () => { PlacesUtils.tagging.untagURI(aURI, newTags); };
michael@0 1242 this.redo = () => { PlacesUtils.tagging.tagURI(aURI, newTags); };
michael@0 1243 }
michael@0 1244 }
michael@0 1245 };
michael@0 1246
michael@0 1247 /**
michael@0 1248 * Transaction for removing tags from a URI.
michael@0 1249 *
michael@0 1250 * Required Input Properties: uri.
michael@0 1251 * Optional Input Properties: tags.
michael@0 1252 *
michael@0 1253 * If |tags| is not set, all tags set for |uri| are removed.
michael@0 1254 */
michael@0 1255 PT.UntagURI = DefineTransaction(["uri"], ["tags"]);
michael@0 1256 PT.UntagURI.prototype = {
michael@0 1257 execute: function* (aURI, aTags) {
michael@0 1258 let tagsSet = PlacesUtils.tagging.getTagsForURI(aURI);
michael@0 1259
michael@0 1260 if (aTags && aTags.length > 0)
michael@0 1261 aTags = [t for (t of aTags) if (tagsSet.indexOf(t) != -1)];
michael@0 1262 else
michael@0 1263 aTags = tagsSet;
michael@0 1264
michael@0 1265 PlacesUtils.tagging.untagURI(aURI, aTags);
michael@0 1266 this.undo = () => { PlacesUtils.tagging.tagURI(aURI, aTags); };
michael@0 1267 this.redo = () => { PlacesUtils.tagging.untagURI(aURI, aTags); };
michael@0 1268 }
michael@0 1269 };

mercurial