toolkit/components/places/PlacesTransactions.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial