forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1798 lines
		
	
	
	
		
			57 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1798 lines
		
	
	
	
		
			57 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /**
 | |
|  * Overview
 | |
|  * --------
 | |
|  * This modules serves as the transactions manager for Places (hereinafter PTM).
 | |
|  * It implements all the elementary transactions for its UI commands: creating
 | |
|  * items, editing their various properties, and so forth.
 | |
|  *
 | |
|  * Note that since the effect of invoking a Places command is not limited to the
 | |
|  * window in which it was performed (e.g. a folder created in the Library may be
 | |
|  * the parent of a bookmark created in some browser window), PTM is a singleton.
 | |
|  * It's therefore unnecessary to initialize PTM in any way apart importing this
 | |
|  * module.
 | |
|  *
 | |
|  * PTM shares most of its semantics with common command pattern implementations.
 | |
|  * However, the asynchronous design of contemporary and future APIs, combined
 | |
|  * with the commitment to serialize all UI operations, does make things a little
 | |
|  * bit different.  For example, when |undo| is called in order to undo the top
 | |
|  * undo entry, the caller cannot tell for sure what entry would it be, because
 | |
|  * the execution of some transactions is either in process, or enqueued to be.
 | |
|  *
 | |
|  * Also note that unlike the nsITransactionManager, for example, this API is by
 | |
|  * no means generic.  That is, it cannot be used to execute anything but the
 | |
|  * elementary transactions implemented here (Please file a bug if you find
 | |
|  * anything uncovered).  More-complex transactions (e.g. creating a folder and
 | |
|  * moving a bookmark into it) may be implemented as a batch (see below).
 | |
|  *
 | |
|  * A note about GUIDs and item-ids
 | |
|  * -------------------------------
 | |
|  * There's an ongoing effort (see bug 1071511) to deprecate item-ids in Places
 | |
|  * in favor of GUIDs.  Both because new APIs (e.g. Bookmarks.sys.mjs) expose them
 | |
|  * to the minimum necessary, and because GUIDs play much better with
 | |
|  * implementing |redo|, this API doesn't support item-ids at all, and only
 | |
|  * accepts bookmark GUIDs, both for input (e.g. for setting the parent folder
 | |
|  * for a new bookmark) and for output (when the GUID for such a bookmark is
 | |
|  * propagated).
 | |
|  *
 | |
|  * Constructing transactions
 | |
|  * -------------------------
 | |
|  * At the bottom of this module you will find transactions for all Places UI
 | |
|  * commands.  They are exposed as constructors set on the PlacesTransactions
 | |
|  * object (e.g. PlacesTransactions.NewFolder).  The input for this constructors
 | |
|  * is taken in the form of a single argument, a plain object consisting of the
 | |
|  * properties for the transaction.  Input properties may be either required or
 | |
|  * optional (for example, |keyword| is required for the EditKeyword transaction,
 | |
|  * but optional for the NewBookmark transaction).
 | |
|  *
 | |
|  * To make things simple, a given input property has the same basic meaning and
 | |
|  * valid values across all transactions which accept it in the input object.
 | |
|  * Here is a list of all supported input properties along with their expected
 | |
|  * values:
 | |
|  *  - url: a URL object, an nsIURI object, or a href.
 | |
|  *  - urls: an array of urls, as above.
 | |
|  *  - tag - a string.
 | |
|  *  - tags: an array of strings.
 | |
|  *  - guid, parentGuid, newParentGuid: a valid Places GUID string.
 | |
|  *  - guids: an array of valid Places GUID strings.
 | |
|  *  - title: a string
 | |
|  *  - index, newIndex: the position of an item in its containing folder,
 | |
|  *    starting from 0.
 | |
|  *    integer and PlacesUtils.bookmarks.DEFAULT_INDEX
 | |
|  *
 | |
|  * If a required property is missing in the input object (e.g. not specifying
 | |
|  * parentGuid for NewBookmark), or if the value for any of the input properties
 | |
|  * is invalid "on the surface" (e.g. a numeric value for GUID, or a string that
 | |
|  * isn't 12-characters long), the transaction constructor throws right way.
 | |
|  * More complex errors (e.g. passing a non-existent GUID for parentGuid) only
 | |
|  * reveal once the transaction is executed.
 | |
|  *
 | |
|  * Executing Transactions (the |transact| method of transactions)
 | |
|  * --------------------------------------------------------------
 | |
|  * Once a transaction is created, you must call its |transact| method for it to
 | |
|  * be executed and take effect.  |transact| is an asynchronous method that takes
 | |
|  * no arguments, and returns a promise that resolves once the transaction is
 | |
|  * executed.  Executing one of the transactions for creating items (NewBookmark,
 | |
|  * NewFolder, NewSeparator) resolve to the new item's GUID.
 | |
|  * There's no resolution value for other transactions.
 | |
|  * If a transaction fails to execute, |transact| rejects and the transactions
 | |
|  * history is not affected.
 | |
|  *
 | |
|  * |transact| throws if it's called more than once (successfully or not) on the
 | |
|  * same transaction object.
 | |
|  *
 | |
|  * Batches
 | |
|  * -------
 | |
|  * Sometimes it is useful to "batch" or "merge" transactions.  For example,
 | |
|  * something like "Bookmark All Tabs" may be implemented as one NewFolder
 | |
|  * transaction followed by numerous NewBookmark transactions - all to be undone
 | |
|  * or redone in a single undo or redo command.  Use `PlacesTransactions.batch()`
 | |
|  * in such cases.
 | |
|  * It takes an array of transactions which will be executed in the given order
 | |
|  * and later be treated as a single entry in the transactions history.
 | |
|  * If a transaction depends on the results from a previous one, it can be
 | |
|  * replaced by a function that will be invoked with an array of results
 | |
|  * accumulated from the previous transactions, indexed in the same positions.
 | |
|  * The function should return the transaction to execute. For example:
 | |
|  *
 | |
|  *  let transactions = [
 | |
|  *    // Returns the GUID of the new bookmark.
 | |
|  *    PlacesTransactions.NewBookmark({
 | |
|  *      parentGuid: "someGUID",
 | |
|  *      title: "someTitle",
 | |
|  *      url: "https://www.mozilla.org/""
 | |
|  *    }),
 | |
|  *    previousResults => PlacesTransactions.EditKeyword({
 | |
|  *      // Get the GUID from the result of transactions[0].
 | |
|  *      guid: previousResults[0],
 | |
|  *      keyword: "someKeyword",
 | |
|  *    },
 | |
|  *  ];
 | |
|  *
 | |
|  * `PlacesTransactions.batch()` returns a promise resolved when the batch ends.
 | |
|  * The resolution value is an array with all the transaction return values
 | |
|  * indexed like the original transactions. So, for example, if a transaction
 | |
|  * returns an array of GUIDs, to get a list of all the created GUIDs for all the
 | |
|  * transactions one could use .flat() to flatten the array.
 | |
|  *
 | |
|  * If any transactions fails to execute, the batch continues (exceptions are
 | |
|  * logged) and the result of that transactions will be set to undefined.
 | |
|  * Only transactions that were executed successfully are added to the
 | |
|  * transactions history as part of the batch.
 | |
|  *
 | |
|  * Serialization
 | |
|  * -------------
 | |
|  * All |PlacesTransaction| operations are serialized.  That is, even though the
 | |
|  * implementation is asynchronous, the order in which PlacesTransactions methods
 | |
|  * is called does guarantee the order in which they are to be invoked.
 | |
|  *
 | |
|  * The only exception to this rule is |transact| calls done during a batch (see
 | |
|  * above).  |transact| calls are serialized with each other (and with undo, redo
 | |
|  * and clearTransactionsHistory), but they  are, of course, not serialized with
 | |
|  * batches.
 | |
|  *
 | |
|  * The transactions-history structure
 | |
|  * ----------------------------------
 | |
|  * The transactions-history is a two-dimensional stack of transactions: the
 | |
|  * transactions are ordered in reverse to the order they were committed.
 | |
|  * It's two-dimensional because PTM allows batching transactions together for
 | |
|  * the purpose of undo or redo (see Batches above).
 | |
|  *
 | |
|  * The undoPosition property is set to the index of the top entry. If there is
 | |
|  * no entry at that index, there is nothing to undo.
 | |
|  * Entries prior to undoPosition, if any, are redo entries, the first one being
 | |
|  * the top redo entry.
 | |
|  *
 | |
|  * [ [2nd redo txn, 1st redo txn],  <= 2nd redo entry
 | |
|  *   [2nd redo txn, 1st redo txn],  <= 1st redo entry
 | |
|  *   [1st undo txn, 2nd undo txn],  <= 1st undo entry
 | |
|  *   [1st undo txn, 2nd undo txn]   <= 2nd undo entry ]
 | |
|  * undoPostion: 2.
 | |
|  *
 | |
|  * Note that when a new entry is created, all redo entries are removed.
 | |
|  */
 | |
| 
 | |
| const TRANSACTIONS_QUEUE_TIMEOUT_MS = 240000; // 4 Mins.
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| // Use a single queue bookmarks transaction manager. This pref exists as an
 | |
| // emergency switch-off, it will go away in the future.
 | |
| const prefs = {};
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   prefs,
 | |
|   "USE_SINGLE_QUEUE",
 | |
|   "places.bookmarks.useSingleQueueTransactionManager",
 | |
|   true
 | |
| );
 | |
| 
 | |
| import { PlacesUtils } from "resource://gre/modules/PlacesUtils.sys.mjs";
 | |
| 
 | |
| function setTimeout(callback, ms) {
 | |
|   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|   timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
 | |
| }
 | |
| 
 | |
| const lazy = {};
 | |
| ChromeUtils.defineLazyGetter(lazy, "logger", function () {
 | |
|   return PlacesUtils.getLogger({ prefix: "Transactions" });
 | |
| });
 | |
| 
 | |
| class TransactionsHistoryArray extends Array {
 | |
|   constructor() {
 | |
|     super();
 | |
| 
 | |
|     // The index of the first undo entry (if any) - See the documentation
 | |
|     // at the top of this file.
 | |
|     this._undoPosition = 0;
 | |
|     // Outside of this module, the API of transactions is inaccessible, and so
 | |
|     // are any internal properties.  To achieve that, transactions are proxified
 | |
|     // in their constructors.  This maps the proxies to their respective raw
 | |
|     // objects.
 | |
|     this.proxifiedToRaw = new WeakMap();
 | |
|   }
 | |
| 
 | |
|   get undoPosition() {
 | |
|     return this._undoPosition;
 | |
|   }
 | |
| 
 | |
|   // Handy shortcuts
 | |
|   get topUndoEntry() {
 | |
|     return this.undoPosition < this.length ? this[this.undoPosition] : null;
 | |
|   }
 | |
|   get topRedoEntry() {
 | |
|     return this.undoPosition > 0 ? this[this.undoPosition - 1] : null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Proxify a transaction object for consumers.
 | |
|    * @param rawTransaction
 | |
|    *        the raw transaction object.
 | |
|    * @return the proxified transaction object.
 | |
|    * @see getRawTransaction for retrieving the raw transaction.
 | |
|    */
 | |
|   proxifyTransaction(rawTransaction) {
 | |
|     let proxy = Object.freeze({
 | |
|       transact(inBatch, batchIndex) {
 | |
|         return TransactionsManager.transact(this, inBatch, batchIndex);
 | |
|       },
 | |
|       toString() {
 | |
|         return rawTransaction.toString();
 | |
|       },
 | |
|     });
 | |
|     this.proxifiedToRaw.set(proxy, rawTransaction);
 | |
|     return proxy;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if the given object is a the proxy object for some transaction.
 | |
|    * @param aValue
 | |
|    *        any JS value.
 | |
|    * @return true if aValue is the proxy object for some transaction, false
 | |
|    * otherwise.
 | |
|    */
 | |
|   isProxifiedTransactionObject(value) {
 | |
|     return this.proxifiedToRaw.has(value);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the raw transaction for the given proxy.
 | |
|    * @param aProxy
 | |
|    *        the proxy object
 | |
|    * @return the transaction proxified by aProxy; |undefined| is returned if
 | |
|    * aProxy is not a proxified transaction.
 | |
|    */
 | |
|   getRawTransaction(proxy) {
 | |
|     return this.proxifiedToRaw.get(proxy);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add a transaction either as a new entry, if forced or if there are no undo
 | |
|    * entries, or to the top undo entry.
 | |
|    *
 | |
|    * @param aProxifiedTransaction
 | |
|    *        the proxified transaction object to be added to the transaction
 | |
|    *        history.
 | |
|    * @param [optional] aForceNewEntry
 | |
|    *        Force a new entry for the transaction. Default: false.
 | |
|    *        If false, an entry will we created only if there's no undo entry
 | |
|    *        to extend.
 | |
|    */
 | |
|   add(proxifiedTransaction, forceNewEntry = false) {
 | |
|     if (!this.isProxifiedTransactionObject(proxifiedTransaction)) {
 | |
|       throw new Error("aProxifiedTransaction is not a proxified transaction");
 | |
|     }
 | |
| 
 | |
|     if (!this.length || forceNewEntry) {
 | |
|       this.clearRedoEntries();
 | |
|       lazy.logger.debug(`Adding transaction: ${proxifiedTransaction}`);
 | |
|       this.unshift([proxifiedTransaction]);
 | |
|     } else {
 | |
|       lazy.logger.debug(`Adding transaction: ${proxifiedTransaction}`);
 | |
|       this[this.undoPosition].unshift(proxifiedTransaction);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clear all undo entries.
 | |
|    */
 | |
|   clearUndoEntries() {
 | |
|     lazy.logger.debug("Clearing undo entries");
 | |
|     if (this.undoPosition < this.length) {
 | |
|       this.splice(this.undoPosition);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clear all redo entries.
 | |
|    */
 | |
|   clearRedoEntries() {
 | |
|     lazy.logger.debug("Clearing redo entries");
 | |
|     if (this.undoPosition > 0) {
 | |
|       this.splice(0, this.undoPosition);
 | |
|       this._undoPosition = 0;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clear all entries.
 | |
|    */
 | |
|   clearAllEntries() {
 | |
|     lazy.logger.debug("Clearing all entries");
 | |
|     if (this.length) {
 | |
|       this.splice(0);
 | |
|       this._undoPosition = 0;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(
 | |
|   lazy,
 | |
|   "TransactionsHistory",
 | |
|   () => new TransactionsHistoryArray()
 | |
| );
 | |
| 
 | |
| export var PlacesTransactions = {
 | |
|   /**
 | |
|    * @see Batches in the module documentation.
 | |
|    */
 | |
|   batch(transactionsToBatch, batchName) {
 | |
|     if (!Array.isArray(transactionsToBatch) || !transactionsToBatch.length) {
 | |
|       throw new Error("Must pass a non-empty array");
 | |
|     }
 | |
|     if (
 | |
|       transactionsToBatch.some(
 | |
|         o =>
 | |
|           !lazy.TransactionsHistory.isProxifiedTransactionObject(o) &&
 | |
|           typeof o != "function"
 | |
|       )
 | |
|     ) {
 | |
|       throw new Error("Must pass only transactions or functions");
 | |
|     }
 | |
|     lazy.logger.debug(
 | |
|       `Batch ${batchName}: ${transactionsToBatch.length} transactions`
 | |
|     );
 | |
|     return TransactionsManager.batch(async function () {
 | |
|       lazy.logger.debug(`Batch ${batchName}: executing transactions`);
 | |
|       let accumulatedResults = [];
 | |
|       for (let txn of transactionsToBatch) {
 | |
|         try {
 | |
|           if (typeof txn == "function") {
 | |
|             txn = txn(accumulatedResults);
 | |
|           }
 | |
|           accumulatedResults.push(
 | |
|             await txn.transact(true, accumulatedResults.length)
 | |
|           );
 | |
|         } catch (ex) {
 | |
|           // TODO Bug 1865631: handle these errors better, currently we just
 | |
|           // continue, that works for non-dependent transactions, but will
 | |
|           // skip most of the work for functions depending on previous results.
 | |
|           // Moreover in both cases we should notify the user about the problem.
 | |
|           accumulatedResults.push(undefined);
 | |
|           // Using console.error() here sometimes fails, due to unknown XPC
 | |
|           // wrappers reasons, so just use our logger.
 | |
|           lazy.logger.error(`Failed to execute batched transaction: ${ex}`);
 | |
|         }
 | |
|       }
 | |
|       return accumulatedResults;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Asynchronously undo the transaction immediately after the current undo
 | |
|    * position in the transactions history in the reverse order, if any, and
 | |
|    * adjusts the undo position.
 | |
|    *
 | |
|    * @return {Promises).  The promise always resolves.
 | |
|    * @note All undo manager operations are queued. This means that transactions
 | |
|    * history may change by the time your request is fulfilled.
 | |
|    */
 | |
|   undo() {
 | |
|     lazy.logger.debug("undo() was invoked");
 | |
|     return TransactionsManager.undo();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Asynchronously redo the transaction immediately before the current undo
 | |
|    * position in the transactions history, if any, and adjusts the undo
 | |
|    * position.
 | |
|    *
 | |
|    * @return {Promises).  The promise always resolves.
 | |
|    * @note All undo manager operations are queued. This means that transactions
 | |
|    * history may change by the time your request is fulfilled.
 | |
|    */
 | |
|   redo() {
 | |
|     lazy.logger.debug("redo() was invoked");
 | |
|     return TransactionsManager.redo();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Asynchronously clear the undo, redo, or all entries from the transactions
 | |
|    * history.
 | |
|    *
 | |
|    * @param [optional] undoEntries
 | |
|    *        Whether or not to clear undo entries.  Default: true.
 | |
|    * @param [optional] redoEntries
 | |
|    *        Whether or not to clear undo entries.  Default: true.
 | |
|    *
 | |
|    * @return {Promises).  The promise always resolves.
 | |
|    * @throws if both aUndoEntries and aRedoEntries are false.
 | |
|    * @note All undo manager operations are queued. This means that transactions
 | |
|    * history may change by the time your request is fulfilled.
 | |
|    */
 | |
|   clearTransactionsHistory(undoEntries = true, redoEntries = true) {
 | |
|     lazy.logger.debug("clearTransactionsHistory() was invoked");
 | |
|     return TransactionsManager.clearTransactionsHistory(
 | |
|       undoEntries,
 | |
|       redoEntries
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The numbers of entries in the transactions history.
 | |
|    */
 | |
|   get length() {
 | |
|     return lazy.TransactionsHistory.length;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the transaction history entry at a given index.  Each entry consists
 | |
|    * of one or more transaction objects.
 | |
|    *
 | |
|    * @param index
 | |
|    *        the index of the entry to retrieve.
 | |
|    * @return an array of transaction objects in their undo order (that is,
 | |
|    * reversely to the order they were executed).
 | |
|    * @throw if aIndex is invalid (< 0 or >= length).
 | |
|    * @note the returned array is a clone of the history entry and is not
 | |
|    * kept in sync with the original entry if it changes.
 | |
|    */
 | |
|   entry(index) {
 | |
|     if (!Number.isInteger(index) || index < 0 || index >= this.length) {
 | |
|       throw new Error("Invalid index");
 | |
|     }
 | |
| 
 | |
|     return lazy.TransactionsHistory[index];
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The index of the top undo entry in the transactions history.
 | |
|    * If there are no undo entries, it equals to |length|.
 | |
|    * Entries past this point
 | |
|    * Entries at and past this point are redo entries.
 | |
|    */
 | |
|   get undoPosition() {
 | |
|     return lazy.TransactionsHistory.undoPosition;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shortcut for accessing the top undo entry in the transaction history.
 | |
|    */
 | |
|   get topUndoEntry() {
 | |
|     return lazy.TransactionsHistory.topUndoEntry;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shortcut for accessing the top redo entry in the transaction history.
 | |
|    */
 | |
|   get topRedoEntry() {
 | |
|     return lazy.TransactionsHistory.topRedoEntry;
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Helper for serializing the calls to TransactionsManager methods. It allows
 | |
|  * us to guarantee that the order in which TransactionsManager asynchronous
 | |
|  * methods are called also enforces the order in which they're executed, and
 | |
|  * that they are never executed in parallel.
 | |
|  *
 | |
|  * In other words: Enqueuer.enqueue(aFunc1); Enqueuer.enqueue(aFunc2) is roughly
 | |
|  * the same as Task.spawn(aFunc1).then(Task.spawn(aFunc2)).
 | |
|  */
 | |
| function Enqueuer(name) {
 | |
|   this._promise = Promise.resolve();
 | |
|   this._name = name;
 | |
| }
 | |
| Enqueuer.prototype = {
 | |
|   /**
 | |
|    * Spawn a functions once all previous functions enqueued are done running,
 | |
|    * and all promises passed to alsoWaitFor are no longer pending.
 | |
|    *
 | |
|    * @param   func
 | |
|    *          a function returning a promise.
 | |
|    * @return  a promise that resolves once aFunc is done running. The promise
 | |
|    *          "mirrors" the promise returned by aFunc.
 | |
|    */
 | |
|   enqueue(func) {
 | |
|     lazy.logger.debug(`${this._name} enqueing`);
 | |
|     // If a transaction awaits on a never resolved promise, or is mistakenly
 | |
|     // nested, it could hang the transactions queue forever.  Thus we timeout
 | |
|     // the execution after a meaningful amount of time, to ensure in any case
 | |
|     // we'll proceed after a while.
 | |
|     let timeoutPromise = new Promise((resolve, reject) => {
 | |
|       setTimeout(
 | |
|         () =>
 | |
|           reject(
 | |
|             new Error(
 | |
|               "PlacesTransaction timeout, most likely caused by unresolved pending work."
 | |
|             )
 | |
|           ),
 | |
|         TRANSACTIONS_QUEUE_TIMEOUT_MS
 | |
|       );
 | |
|     });
 | |
|     let promise = this._promise.then(() =>
 | |
|       Promise.race([func(), timeoutPromise])
 | |
|     );
 | |
| 
 | |
|     // Propagate exceptions to the caller, but dismiss them internally.
 | |
|     this._promise = promise.catch(lazy.logger.error);
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Same as above, but for a promise returned by a function that already run.
 | |
|    * This is useful, for example, for serializing transact calls with undo calls,
 | |
|    * even though transact has its own Enqueuer.
 | |
|    *
 | |
|    * @param {Promise} otherPromise
 | |
|    *        any promise.
 | |
|    * @param {string} source
 | |
|    *        source for logging purposes
 | |
|    */
 | |
|   alsoWaitFor(otherPromise, source) {
 | |
|     lazy.logger.debug(`${this._name} alsoWaitFor: ${source}`);
 | |
|     // We don't care if aPromise resolves or rejects, but just that is not
 | |
|     // pending anymore.
 | |
|     // If a transaction awaits on a never resolved promise, or is mistakenly
 | |
|     // nested, it could hang the transactions queue forever.  Thus we timeout
 | |
|     // the execution after a meaningful amount of time, to ensure in any case
 | |
|     // we'll proceed after a while.
 | |
|     let timeoutPromise = new Promise((resolve, reject) => {
 | |
|       setTimeout(
 | |
|         () =>
 | |
|           reject(
 | |
|             new Error(
 | |
|               "PlacesTransaction timeout, most likely caused by unresolved pending work."
 | |
|             )
 | |
|           ),
 | |
|         TRANSACTIONS_QUEUE_TIMEOUT_MS
 | |
|       );
 | |
|     });
 | |
|     let promise = Promise.race([otherPromise, timeoutPromise]).catch(
 | |
|       console.error
 | |
|     );
 | |
|     this._promise = Promise.all([this._promise, promise]);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The promise for this queue.
 | |
|    */
 | |
|   get promise() {
 | |
|     return this._promise;
 | |
|   },
 | |
| };
 | |
| 
 | |
| var TransactionsManager = {
 | |
|   // See the documentation at the top of this file. |transact| calls are not
 | |
|   // serialized with |batch| calls.
 | |
|   _mainEnqueuer: new Enqueuer("MainEnqueuer"),
 | |
|   _transactEnqueuer: new Enqueuer("TransactEnqueuer"),
 | |
| 
 | |
|   // Transactions object should never be recycled (that is, |execute| should
 | |
|   // only be called once (or not at all) after they're constructed.
 | |
|   // This keeps track of all transactions which were executed.
 | |
|   _executedTransactions: new WeakSet(),
 | |
| 
 | |
|   /**
 | |
|    * Execute a proxified transaction.
 | |
|    *
 | |
|    * @param {object} txnProxy The proxified transaction to execute.
 | |
|    * @param {boolean} [inBatch] Whether the transaction is part of a batch.
 | |
|    * @param {integer} [batchIndex] The index of the transaction in the batch array.
 | |
|    * @returns {Promise} resolved to the transaction return value once complete.
 | |
|    */
 | |
|   transact(txnProxy, inBatch = false, batchIndex = undefined) {
 | |
|     let rawTxn = lazy.TransactionsHistory.getRawTransaction(txnProxy);
 | |
|     if (!rawTxn) {
 | |
|       throw new Error("|transact| was called with an unexpected object");
 | |
|     }
 | |
| 
 | |
|     if (this._executedTransactions.has(rawTxn)) {
 | |
|       throw new Error("Transactions objects may not be recycled.");
 | |
|     }
 | |
| 
 | |
|     lazy.logger.debug(`transact() enqueue: ${txnProxy}`);
 | |
| 
 | |
|     // Add it in advance so one doesn't accidentally do
 | |
|     // sameTxn.transact(); sameTxn.transact();
 | |
|     this._executedTransactions.add(rawTxn);
 | |
| 
 | |
|     let task = async () => {
 | |
|       lazy.logger.debug(`transact execute(): ${txnProxy}`);
 | |
|       // Don't try to catch exceptions. If execute fails, we better not add the
 | |
|       // transaction to the undo stack.
 | |
|       let retval = await rawTxn.execute();
 | |
| 
 | |
|       let forceNewEntry = !inBatch || batchIndex === 0;
 | |
|       lazy.TransactionsHistory.add(txnProxy, forceNewEntry);
 | |
| 
 | |
|       this._updateCommandsOnActiveWindow();
 | |
|       return retval;
 | |
|     };
 | |
| 
 | |
|     if (prefs.USE_SINGLE_QUEUE) {
 | |
|       return task();
 | |
|     }
 | |
| 
 | |
|     let promise = this._transactEnqueuer.enqueue(task);
 | |
|     this._mainEnqueuer.alsoWaitFor(promise, "transact");
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   batch(task) {
 | |
|     return this._mainEnqueuer.enqueue(task);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Undo the top undo entry, if any, and update the undo position accordingly.
 | |
|    */
 | |
|   undo() {
 | |
|     let promise = this._mainEnqueuer.enqueue(async () => {
 | |
|       lazy.logger.debug("Undo execute");
 | |
|       let entry = lazy.TransactionsHistory.topUndoEntry;
 | |
|       if (!entry) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       for (let txnProxy of entry) {
 | |
|         try {
 | |
|           await lazy.TransactionsHistory.getRawTransaction(txnProxy).undo();
 | |
|         } catch (ex) {
 | |
|           // If one transaction is broken, it's not safe to work with any other
 | |
|           // undo entry.  Report the error and clear the undo history.
 | |
|           console.error(ex, "Can't undo a transaction, clearing undo entries.");
 | |
|           lazy.TransactionsHistory.clearUndoEntries();
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
|       lazy.TransactionsHistory._undoPosition++;
 | |
|       this._updateCommandsOnActiveWindow();
 | |
|     });
 | |
|     if (!prefs.USE_SINGLE_QUEUE) {
 | |
|       this._transactEnqueuer.alsoWaitFor(promise, "undo");
 | |
|     }
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Redo the top redo entry, if any, and update the undo position accordingly.
 | |
|    */
 | |
|   redo() {
 | |
|     let promise = this._mainEnqueuer.enqueue(async () => {
 | |
|       lazy.logger.debug("Redo execute");
 | |
|       let entry = lazy.TransactionsHistory.topRedoEntry;
 | |
|       if (!entry) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       for (let i = entry.length - 1; i >= 0; i--) {
 | |
|         let transaction = lazy.TransactionsHistory.getRawTransaction(entry[i]);
 | |
|         try {
 | |
|           if (transaction.redo) {
 | |
|             await transaction.redo();
 | |
|           } else {
 | |
|             await transaction.execute();
 | |
|           }
 | |
|         } catch (ex) {
 | |
|           // If one transaction is broken, it's not safe to work with any other
 | |
|           // redo entry. Report the error and clear the undo history.
 | |
|           console.error(ex, "Can't redo a transaction, clearing redo entries.");
 | |
|           lazy.TransactionsHistory.clearRedoEntries();
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
|       lazy.TransactionsHistory._undoPosition--;
 | |
|       this._updateCommandsOnActiveWindow();
 | |
|     });
 | |
|     if (!prefs.USE_SINGLE_QUEUE) {
 | |
|       this._transactEnqueuer.alsoWaitFor(promise, "redo");
 | |
|     }
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   clearTransactionsHistory(undoEntries, redoEntries) {
 | |
|     let promise = this._mainEnqueuer.enqueue(function () {
 | |
|       lazy.logger.debug(`ClearTransactionsHistory execute`);
 | |
|       if (undoEntries && redoEntries) {
 | |
|         lazy.TransactionsHistory.clearAllEntries();
 | |
|       } else if (undoEntries) {
 | |
|         lazy.TransactionsHistory.clearUndoEntries();
 | |
|       } else if (redoEntries) {
 | |
|         lazy.TransactionsHistory.clearRedoEntries();
 | |
|       } else {
 | |
|         throw new Error("either aUndoEntries or aRedoEntries should be true");
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     if (!prefs.USE_SINGLE_QUEUE) {
 | |
|       this._transactEnqueuer.alsoWaitFor(promise, "clearTransactionsHistory");
 | |
|     }
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   // Updates commands in the undo group of the active window commands.
 | |
|   // Inactive windows commands will be updated on focus.
 | |
|   _updateCommandsOnActiveWindow() {
 | |
|     // Updating "undo" will cause a group update including "redo".
 | |
|     try {
 | |
|       let win = Services.focus.activeWindow;
 | |
|       if (win) {
 | |
|         win.updateCommands("undo");
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       console.error(ex, "Couldn't update undo commands.");
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Internal helper for defining the standard transactions and their input.
 | |
|  * It takes the required and optional properties, and generates the public
 | |
|  * constructor (which takes the input in the form of a plain object) which,
 | |
|  * when called, creates the argument-less "public" |execute| method by binding
 | |
|  * the input properties to the function arguments (required properties first,
 | |
|  * then the optional properties).
 | |
|  *
 | |
|  * If this seems confusing, look at the consumers.
 | |
|  *
 | |
|  * This magic serves two purposes:
 | |
|  * (1) It completely hides the transactions' internals from the module
 | |
|  *     consumers.
 | |
|  * (2) It keeps each transaction implementation to what is about, bypassing
 | |
|  *     all this bureaucracy while still validating input appropriately.
 | |
|  */
 | |
| function DefineTransaction(requiredProps = [], optionalProps = []) {
 | |
|   for (let prop of [...requiredProps, ...optionalProps]) {
 | |
|     if (!DefineTransaction.inputProps.has(prop)) {
 | |
|       throw new Error("Property '" + prop + "' is not defined");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   let ctor = function (input) {
 | |
|     // We want to support both syntaxes:
 | |
|     // let t = new PlacesTransactions.NewBookmark(),
 | |
|     // let t = PlacesTransactions.NewBookmark()
 | |
|     if (this == PlacesTransactions) {
 | |
|       return new ctor(input);
 | |
|     }
 | |
| 
 | |
|     if (requiredProps.length || optionalProps.length) {
 | |
|       // Bind the input properties to the arguments of execute.
 | |
|       input = DefineTransaction.verifyInput(
 | |
|         input,
 | |
|         requiredProps,
 | |
|         optionalProps
 | |
|       );
 | |
|       this.execute = this.execute.bind(this, input);
 | |
|     }
 | |
|     return lazy.TransactionsHistory.proxifyTransaction(this);
 | |
|   };
 | |
|   return ctor;
 | |
| }
 | |
| 
 | |
| function simpleValidateFunc(checkFn) {
 | |
|   return v => {
 | |
|     if (!checkFn(v)) {
 | |
|       throw new Error("Invalid value");
 | |
|     }
 | |
|     return v;
 | |
|   };
 | |
| }
 | |
| 
 | |
| DefineTransaction.strValidate = simpleValidateFunc(v => typeof v == "string");
 | |
| DefineTransaction.strOrNullValidate = simpleValidateFunc(
 | |
|   v => typeof v == "string" || v === null
 | |
| );
 | |
| DefineTransaction.indexValidate = simpleValidateFunc(
 | |
|   v => Number.isInteger(v) && v >= PlacesUtils.bookmarks.DEFAULT_INDEX
 | |
| );
 | |
| DefineTransaction.guidValidate = simpleValidateFunc(v =>
 | |
|   /^[a-zA-Z0-9\-_]{12}$/.test(v)
 | |
| );
 | |
| 
 | |
| function isPrimitive(v) {
 | |
|   return v === null || (typeof v != "object" && typeof v != "function");
 | |
| }
 | |
| 
 | |
| function checkProperty(obj, prop, required, checkFn) {
 | |
|   if (prop in obj) {
 | |
|     return checkFn(obj[prop]);
 | |
|   }
 | |
| 
 | |
|   return !required;
 | |
| }
 | |
| 
 | |
| DefineTransaction.childObjectValidate = function (obj) {
 | |
|   if (
 | |
|     obj &&
 | |
|     checkProperty(obj, "title", false, v => typeof v == "string") &&
 | |
|     !("type" in obj && obj.type != PlacesUtils.bookmarks.TYPE_BOOKMARK)
 | |
|   ) {
 | |
|     obj.url = DefineTransaction.urlValidate(obj.url);
 | |
|     let validKeys = ["title", "url"];
 | |
|     if (Object.keys(obj).every(k => validKeys.includes(k))) {
 | |
|       return obj;
 | |
|     }
 | |
|   }
 | |
|   throw new Error("Invalid child object");
 | |
| };
 | |
| 
 | |
| DefineTransaction.urlValidate = function (url) {
 | |
|   if (url instanceof Ci.nsIURI) {
 | |
|     return URL.fromURI(url);
 | |
|   }
 | |
|   return new URL(url);
 | |
| };
 | |
| 
 | |
| DefineTransaction.inputProps = new Map();
 | |
| DefineTransaction.defineInputProps = function (
 | |
|   names,
 | |
|   validateFn,
 | |
|   defaultValue
 | |
| ) {
 | |
|   for (let name of names) {
 | |
|     this.inputProps.set(name, {
 | |
|       validateValue(value) {
 | |
|         if (value === undefined) {
 | |
|           return defaultValue;
 | |
|         }
 | |
|         try {
 | |
|           return validateFn(value);
 | |
|         } catch (ex) {
 | |
|           throw new Error(`Invalid value for input property ${name}: ${ex}`);
 | |
|         }
 | |
|       },
 | |
| 
 | |
|       validateInput(input, required) {
 | |
|         if (required && !(name in input)) {
 | |
|           throw new Error(`Required input property is missing: ${name}`);
 | |
|         }
 | |
|         return this.validateValue(input[name]);
 | |
|       },
 | |
| 
 | |
|       isArrayProperty: false,
 | |
|     });
 | |
|   }
 | |
| };
 | |
| 
 | |
| DefineTransaction.defineArrayInputProp = function (name, basePropertyName) {
 | |
|   let baseProp = this.inputProps.get(basePropertyName);
 | |
|   if (!baseProp) {
 | |
|     throw new Error(`Unknown input property: ${basePropertyName}`);
 | |
|   }
 | |
| 
 | |
|   this.inputProps.set(name, {
 | |
|     validateValue(aValue) {
 | |
|       if (aValue == undefined) {
 | |
|         return [];
 | |
|       }
 | |
| 
 | |
|       if (!Array.isArray(aValue)) {
 | |
|         throw new Error(`${name} input property value must be an array`);
 | |
|       }
 | |
| 
 | |
|       // We must create a new array in the local scope to avoid a memory leak due
 | |
|       // to the array global object. We can't use Cu.cloneInto as that doesn't
 | |
|       // handle the URIs. Slice & map also aren't good enough, so we start off
 | |
|       // with a clean array and insert what we need into it.
 | |
|       let newArray = [];
 | |
|       for (let item of aValue) {
 | |
|         newArray.push(baseProp.validateValue(item));
 | |
|       }
 | |
|       return newArray;
 | |
|     },
 | |
| 
 | |
|     // We allow setting either the array property itself (e.g. urls), or a
 | |
|     // single element of it (url, in that example), that is then transformed
 | |
|     // into a single-element array.
 | |
|     validateInput(input, required) {
 | |
|       if (name in input) {
 | |
|         // It's not allowed to set both though.
 | |
|         if (basePropertyName in input) {
 | |
|           throw new Error(`It is not allowed to set both ${name} and
 | |
|                           ${basePropertyName} as  input properties`);
 | |
|         }
 | |
|         let array = this.validateValue(input[name]);
 | |
|         if (required && !array.length) {
 | |
|           throw new Error(`Empty array passed for required input property:
 | |
|                            ${name}`);
 | |
|         }
 | |
|         return array;
 | |
|       }
 | |
|       // If the property is required and it's not set as is, check if the base
 | |
|       // property is set.
 | |
|       if (required && !(basePropertyName in input)) {
 | |
|         throw new Error(`Required input property is missing: ${name}`);
 | |
|       }
 | |
| 
 | |
|       if (basePropertyName in input) {
 | |
|         return [baseProp.validateValue(input[basePropertyName])];
 | |
|       }
 | |
| 
 | |
|       return [];
 | |
|     },
 | |
| 
 | |
|     isArrayProperty: true,
 | |
|   });
 | |
| };
 | |
| 
 | |
| DefineTransaction.validatePropertyValue = function (prop, input, required) {
 | |
|   return this.inputProps.get(prop).validateInput(input, required);
 | |
| };
 | |
| 
 | |
| DefineTransaction.getInputObjectForSingleValue = function (
 | |
|   input,
 | |
|   requiredProps,
 | |
|   optionalProps
 | |
| ) {
 | |
|   // The following input forms may be deduced from a single value:
 | |
|   // * a single required property with or without optional properties (the given
 | |
|   //   value is set to the required property).
 | |
|   // * a single optional property with no required properties.
 | |
|   if (
 | |
|     requiredProps.length > 1 ||
 | |
|     (!requiredProps.length && optionalProps.length > 1)
 | |
|   ) {
 | |
|     throw new Error("Transaction input isn't an object");
 | |
|   }
 | |
| 
 | |
|   let propName =
 | |
|     requiredProps.length == 1 ? requiredProps[0] : optionalProps[0];
 | |
|   let propValue =
 | |
|     this.inputProps.get(propName).isArrayProperty && !Array.isArray(input)
 | |
|       ? [input]
 | |
|       : input;
 | |
|   return { [propName]: propValue };
 | |
| };
 | |
| 
 | |
| DefineTransaction.verifyInput = function (
 | |
|   input,
 | |
|   requiredProps = [],
 | |
|   optionalProps = []
 | |
| ) {
 | |
|   if (!requiredProps.length && !optionalProps.length) {
 | |
|     return {};
 | |
|   }
 | |
| 
 | |
|   // If there's just a single required/optional property, we allow passing it
 | |
|   // as is, so, for example, one could do PlacesTransactions.Remove(myGuid)
 | |
|   // rather than PlacesTransactions.Remove({ guid: myGuid}).
 | |
|   // This shortcut isn't supported for "complex" properties, like objects (note
 | |
|   // there is no use case for this at the moment anyway).
 | |
|   let isSinglePropertyInput =
 | |
|     isPrimitive(input) ||
 | |
|     Array.isArray(input) ||
 | |
|     input instanceof Ci.nsISupports;
 | |
|   if (isSinglePropertyInput) {
 | |
|     input = this.getInputObjectForSingleValue(
 | |
|       input,
 | |
|       requiredProps,
 | |
|       optionalProps
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   let fixedInput = {};
 | |
|   for (let prop of requiredProps) {
 | |
|     fixedInput[prop] = this.validatePropertyValue(prop, input, true);
 | |
|   }
 | |
|   for (let prop of optionalProps) {
 | |
|     fixedInput[prop] = this.validatePropertyValue(prop, input, false);
 | |
|   }
 | |
| 
 | |
|   return fixedInput;
 | |
| };
 | |
| 
 | |
| // Update the documentation at the top of this module if you add or
 | |
| // remove properties.
 | |
| DefineTransaction.defineInputProps(
 | |
|   ["url"],
 | |
|   DefineTransaction.urlValidate,
 | |
|   null
 | |
| );
 | |
| DefineTransaction.defineInputProps(
 | |
|   ["guid", "parentGuid", "newParentGuid"],
 | |
|   DefineTransaction.guidValidate
 | |
| );
 | |
| DefineTransaction.defineInputProps(
 | |
|   ["title", "postData"],
 | |
|   DefineTransaction.strOrNullValidate,
 | |
|   null
 | |
| );
 | |
| DefineTransaction.defineInputProps(
 | |
|   ["keyword", "oldKeyword", "oldTag", "tag"],
 | |
|   DefineTransaction.strValidate,
 | |
|   ""
 | |
| );
 | |
| DefineTransaction.defineInputProps(
 | |
|   ["index", "newIndex"],
 | |
|   DefineTransaction.indexValidate,
 | |
|   PlacesUtils.bookmarks.DEFAULT_INDEX
 | |
| );
 | |
| DefineTransaction.defineInputProps(
 | |
|   ["child"],
 | |
|   DefineTransaction.childObjectValidate
 | |
| );
 | |
| DefineTransaction.defineArrayInputProp("guids", "guid");
 | |
| DefineTransaction.defineArrayInputProp("urls", "url");
 | |
| DefineTransaction.defineArrayInputProp("tags", "tag");
 | |
| DefineTransaction.defineArrayInputProp("children", "child");
 | |
| 
 | |
| /**
 | |
|  * Creates items (all types) from a bookmarks tree representation, as defined
 | |
|  * in PlacesUtils.promiseBookmarksTree.
 | |
|  *
 | |
|  * @param tree
 | |
|  *        the bookmarks tree object.  You may pass either a bookmarks tree
 | |
|  *        returned by promiseBookmarksTree, or a manually defined one.
 | |
|  * @param [optional] restoring (default: false)
 | |
|  *        Whether or not the items are restored.  Only in restore mode, are
 | |
|  *        the guid, dateAdded and lastModified properties honored.
 | |
|  * @note the id, root and charset properties of items in aBookmarksTree are
 | |
|  *       always ignored.  The index property is ignored for all items but the
 | |
|  *       root one.
 | |
|  * @return {Promise}
 | |
|  * @resolves to the guid of the new item.
 | |
|  */
 | |
| // TODO: Replace most of this with insertTree.
 | |
| function createItemsFromBookmarksTree(tree, restoring = false) {
 | |
|   async function createItem(
 | |
|     item,
 | |
|     parentGuid,
 | |
|     index = PlacesUtils.bookmarks.DEFAULT_INDEX
 | |
|   ) {
 | |
|     let guid;
 | |
|     let info = { parentGuid, index };
 | |
|     if (restoring) {
 | |
|       info.guid = item.guid;
 | |
|       info.dateAdded = PlacesUtils.toDate(item.dateAdded);
 | |
|       info.lastModified = PlacesUtils.toDate(item.lastModified);
 | |
|     }
 | |
|     let shouldResetLastModified = false;
 | |
|     switch (item.type) {
 | |
|       case PlacesUtils.TYPE_X_MOZ_PLACE: {
 | |
|         info.url = item.uri;
 | |
|         if (typeof item.title == "string") {
 | |
|           info.title = item.title;
 | |
|         }
 | |
| 
 | |
|         guid = (await PlacesUtils.bookmarks.insert(info)).guid;
 | |
| 
 | |
|         if ("keyword" in item) {
 | |
|           let { uri: url, keyword, postData } = item;
 | |
|           await PlacesUtils.keywords.insert({ url, keyword, postData });
 | |
|         }
 | |
|         if ("tags" in item) {
 | |
|           PlacesUtils.tagging.tagURI(
 | |
|             Services.io.newURI(item.uri),
 | |
|             item.tags.split(",")
 | |
|           );
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: {
 | |
|         info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
 | |
|         if (typeof item.title == "string") {
 | |
|           info.title = item.title;
 | |
|         }
 | |
|         guid = (await PlacesUtils.bookmarks.insert(info)).guid;
 | |
|         if ("children" in item) {
 | |
|           for (let child of item.children) {
 | |
|             await createItem(child, guid);
 | |
|           }
 | |
|         }
 | |
|         if (restoring) {
 | |
|           shouldResetLastModified = true;
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: {
 | |
|         info.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
 | |
|         guid = (await PlacesUtils.bookmarks.insert(info)).guid;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (shouldResetLastModified) {
 | |
|       let lastModified = PlacesUtils.toDate(item.lastModified);
 | |
|       await PlacesUtils.bookmarks.update({ guid, lastModified });
 | |
|     }
 | |
| 
 | |
|     return guid;
 | |
|   }
 | |
|   return createItem(tree, tree.parentGuid, tree.index);
 | |
| }
 | |
| 
 | |
| /** ***************************************************************************
 | |
|  * The Standard Places Transactions.
 | |
|  *
 | |
|  * See the documentation at the top of this file. The valid values for input
 | |
|  * are also documented there.
 | |
|  *****************************************************************************/
 | |
| 
 | |
| var PT = PlacesTransactions;
 | |
| 
 | |
| /**
 | |
|  * Transaction for creating a bookmark.
 | |
|  *
 | |
|  * Required Input Properties: url, parentGuid.
 | |
|  * Optional Input Properties: index, title, keyword, tags.
 | |
|  *
 | |
|  * When this transaction is executed, it's resolved to the new bookmark's GUID.
 | |
|  */
 | |
| PT.NewBookmark = DefineTransaction(
 | |
|   ["parentGuid", "url"],
 | |
|   ["index", "title", "tags"]
 | |
| );
 | |
| PT.NewBookmark.prototype = Object.seal({
 | |
|   async execute({ parentGuid, url, index, title, tags }) {
 | |
|     let info = { parentGuid, index, url, title };
 | |
|     // Filter tags to exclude already existing ones.
 | |
|     if (tags.length) {
 | |
|       let currentTags = PlacesUtils.tagging.getTagsForURI(url.URI);
 | |
|       tags = tags.filter(t => !currentTags.includes(t));
 | |
|     }
 | |
| 
 | |
|     async function createItem() {
 | |
|       info = await PlacesUtils.bookmarks.insert(info);
 | |
|       if (tags.length) {
 | |
|         PlacesUtils.tagging.tagURI(url.URI, tags);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     await createItem();
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       // Pick up the removed info so we have the accurate last-modified value.
 | |
|       await PlacesUtils.bookmarks.remove(info);
 | |
|       if (tags.length) {
 | |
|         PlacesUtils.tagging.untagURI(url.URI, tags);
 | |
|       }
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       await createItem();
 | |
|     };
 | |
|     return info.guid;
 | |
|   },
 | |
|   toString() {
 | |
|     return "NewBookmark";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for creating a folder.
 | |
|  *
 | |
|  * Required Input Properties: title, parentGuid.
 | |
|  * Optional Input Properties: index, children
 | |
|  *
 | |
|  * When this transaction is executed, it's resolved to the new folder's GUID.
 | |
|  */
 | |
| PT.NewFolder = DefineTransaction(
 | |
|   ["parentGuid", "title"],
 | |
|   ["index", "children"]
 | |
| );
 | |
| PT.NewFolder.prototype = Object.seal({
 | |
|   async execute({ parentGuid, title, index, children }) {
 | |
|     let folderGuid;
 | |
|     let info = {
 | |
|       children: [
 | |
|         {
 | |
|           // Ensure to specify a guid to be restored on redo.
 | |
|           guid: PlacesUtils.history.makeGuid(),
 | |
|           title,
 | |
|           type: PlacesUtils.bookmarks.TYPE_FOLDER,
 | |
|         },
 | |
|       ],
 | |
|       // insertTree uses guid as the parent for where it is being inserted
 | |
|       // into.
 | |
|       guid: parentGuid,
 | |
|     };
 | |
| 
 | |
|     if (children && children.length) {
 | |
|       // Ensure to specify a guid for each child to be restored on redo.
 | |
|       info.children[0].children = children.map(c => {
 | |
|         c.guid = PlacesUtils.history.makeGuid();
 | |
|         return c;
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     async function createItem() {
 | |
|       // Note, insertTree returns an array, rather than the folder/child structure.
 | |
|       // For simplicity, we only get the new folder id here. This means that
 | |
|       // an undo then redo won't retain exactly the same information for all
 | |
|       // the child bookmarks, but we believe that isn't important at the moment.
 | |
|       let bmInfo = await PlacesUtils.bookmarks.insertTree(info);
 | |
|       // insertTree returns an array, but we only need to deal with the folder guid.
 | |
|       folderGuid = bmInfo[0].guid;
 | |
| 
 | |
|       // Bug 1388097: insertTree doesn't handle inserting at a specific index for the folder,
 | |
|       // therefore we update the bookmark manually afterwards.
 | |
|       if (index != PlacesUtils.bookmarks.DEFAULT_INDEX) {
 | |
|         bmInfo[0].index = index;
 | |
|         bmInfo = await PlacesUtils.bookmarks.update(bmInfo[0]);
 | |
|       }
 | |
|     }
 | |
|     await createItem();
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       await PlacesUtils.bookmarks.remove(folderGuid);
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       await createItem();
 | |
|     };
 | |
|     return folderGuid;
 | |
|   },
 | |
|   toString() {
 | |
|     return "NewFolder";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for creating a separator.
 | |
|  *
 | |
|  * Required Input Properties: parentGuid.
 | |
|  * Optional Input Properties: index.
 | |
|  *
 | |
|  * When this transaction is executed, it's resolved to the new separator's
 | |
|  * GUID.
 | |
|  */
 | |
| PT.NewSeparator = DefineTransaction(["parentGuid"], ["index"]);
 | |
| PT.NewSeparator.prototype = Object.seal({
 | |
|   async execute(info) {
 | |
|     info.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
 | |
|     info = await PlacesUtils.bookmarks.insert(info);
 | |
|     this.undo = PlacesUtils.bookmarks.remove.bind(PlacesUtils.bookmarks, info);
 | |
|     this.redo = PlacesUtils.bookmarks.insert.bind(PlacesUtils.bookmarks, info);
 | |
|     return info.guid;
 | |
|   },
 | |
|   toString() {
 | |
|     return "NewSeparator";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for moving an item.
 | |
|  *
 | |
|  * Required Input Properties: guid, newParentGuid.
 | |
|  * Optional Input Properties  newIndex.
 | |
|  */
 | |
| PT.Move = DefineTransaction(["guids", "newParentGuid"], ["newIndex"]);
 | |
| PT.Move.prototype = Object.seal({
 | |
|   async execute({ guids, newParentGuid, newIndex }) {
 | |
|     let originalInfos = [];
 | |
|     let index = newIndex;
 | |
| 
 | |
|     for (let guid of guids) {
 | |
|       // We need to save the original data for undo.
 | |
|       let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
 | |
|       if (!originalInfo) {
 | |
|         throw new Error("Cannot move a non-existent item");
 | |
|       }
 | |
| 
 | |
|       originalInfos.push(originalInfo);
 | |
|     }
 | |
| 
 | |
|     await PlacesUtils.bookmarks.moveToFolder(guids, newParentGuid, index);
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       // Undo has the potential for moving multiple bookmarks to multiple different
 | |
|       // folders and positions, which is very complicated to manage. Therefore we do
 | |
|       // individual moves one at a time and hopefully everything is put back approximately
 | |
|       // where it should be.
 | |
|       for (let info of originalInfos) {
 | |
|         await PlacesUtils.bookmarks.update(info);
 | |
|       }
 | |
|     };
 | |
|     this.redo = PlacesUtils.bookmarks.moveToFolder.bind(
 | |
|       PlacesUtils.bookmarks,
 | |
|       guids,
 | |
|       newParentGuid,
 | |
|       index
 | |
|     );
 | |
|     return guids;
 | |
|   },
 | |
|   toString() {
 | |
|     return "Move";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for setting the title for an item.
 | |
|  *
 | |
|  * Required Input Properties: guid, title.
 | |
|  */
 | |
| PT.EditTitle = DefineTransaction(["guid", "title"]);
 | |
| PT.EditTitle.prototype = Object.seal({
 | |
|   async execute({ guid, title }) {
 | |
|     let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
 | |
|     if (!originalInfo) {
 | |
|       throw new Error("cannot update a non-existent item");
 | |
|     }
 | |
| 
 | |
|     let updateInfo = { guid, title };
 | |
|     updateInfo = await PlacesUtils.bookmarks.update(updateInfo);
 | |
| 
 | |
|     this.undo = PlacesUtils.bookmarks.update.bind(
 | |
|       PlacesUtils.bookmarks,
 | |
|       originalInfo
 | |
|     );
 | |
|     this.redo = PlacesUtils.bookmarks.update.bind(
 | |
|       PlacesUtils.bookmarks,
 | |
|       updateInfo
 | |
|     );
 | |
|   },
 | |
|   toString() {
 | |
|     return "EditTitle";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for setting the URI for an item.
 | |
|  *
 | |
|  * Required Input Properties: guid, url.
 | |
|  */
 | |
| PT.EditUrl = DefineTransaction(["guid", "url"]);
 | |
| PT.EditUrl.prototype = Object.seal({
 | |
|   async execute({ guid, url }) {
 | |
|     let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
 | |
|     if (!originalInfo) {
 | |
|       throw new Error("cannot update a non-existent item");
 | |
|     }
 | |
|     if (originalInfo.type != PlacesUtils.bookmarks.TYPE_BOOKMARK) {
 | |
|       throw new Error("Cannot edit url for non-bookmark items");
 | |
|     }
 | |
| 
 | |
|     let uri = url.URI;
 | |
|     let originalURI = originalInfo.url.URI;
 | |
|     let originalTags = PlacesUtils.tagging.getTagsForURI(originalURI);
 | |
|     let updatedInfo = { guid, url };
 | |
|     let newURIAdditionalTags = null;
 | |
| 
 | |
|     async function updateItem() {
 | |
|       updatedInfo = await PlacesUtils.bookmarks.update(updatedInfo);
 | |
|       // Move tags from the original URI to the new URI.
 | |
|       if (originalTags.length) {
 | |
|         // Untag the original URI only if this was the only bookmark.
 | |
|         if (!(await PlacesUtils.bookmarks.fetch({ url: originalInfo.url }))) {
 | |
|           PlacesUtils.tagging.untagURI(originalURI, originalTags);
 | |
|         }
 | |
|         let currentNewURITags = PlacesUtils.tagging.getTagsForURI(uri);
 | |
|         newURIAdditionalTags = originalTags.filter(
 | |
|           t => !currentNewURITags.includes(t)
 | |
|         );
 | |
|         if (newURIAdditionalTags && newURIAdditionalTags.length) {
 | |
|           PlacesUtils.tagging.tagURI(uri, newURIAdditionalTags);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     await updateItem();
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       await PlacesUtils.bookmarks.update(originalInfo);
 | |
|       // Move tags from new URI to original URI.
 | |
|       if (originalTags.length) {
 | |
|         // Only untag the new URI if this is the only bookmark.
 | |
|         if (
 | |
|           newURIAdditionalTags &&
 | |
|           !!newURIAdditionalTags.length &&
 | |
|           !(await PlacesUtils.bookmarks.fetch({ url }))
 | |
|         ) {
 | |
|           PlacesUtils.tagging.untagURI(uri, newURIAdditionalTags);
 | |
|         }
 | |
|         PlacesUtils.tagging.tagURI(originalURI, originalTags);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     this.redo = async function () {
 | |
|       updatedInfo = await updateItem();
 | |
|     };
 | |
|   },
 | |
|   toString() {
 | |
|     return "EditUrl";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for setting the keyword for a bookmark.
 | |
|  *
 | |
|  * Required Input Properties: guid, keyword.
 | |
|  * Optional Input Properties: postData, oldKeyword.
 | |
|  */
 | |
| PT.EditKeyword = DefineTransaction(
 | |
|   ["guid", "keyword"],
 | |
|   ["postData", "oldKeyword"]
 | |
| );
 | |
| PT.EditKeyword.prototype = Object.seal({
 | |
|   async execute({ guid, keyword, postData, oldKeyword }) {
 | |
|     let url;
 | |
|     let oldKeywordEntry;
 | |
|     if (oldKeyword) {
 | |
|       oldKeywordEntry = await PlacesUtils.keywords.fetch(oldKeyword);
 | |
|       url = oldKeywordEntry.url;
 | |
|       await PlacesUtils.keywords.remove(oldKeyword);
 | |
|     }
 | |
| 
 | |
|     if (keyword) {
 | |
|       if (!url) {
 | |
|         url = (await PlacesUtils.bookmarks.fetch(guid)).url;
 | |
|       }
 | |
|       await PlacesUtils.keywords.insert({
 | |
|         url,
 | |
|         keyword,
 | |
|         postData: postData || (oldKeywordEntry ? oldKeywordEntry.postData : ""),
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       if (keyword) {
 | |
|         await PlacesUtils.keywords.remove(keyword);
 | |
|       }
 | |
|       if (oldKeywordEntry) {
 | |
|         await PlacesUtils.keywords.insert(oldKeywordEntry);
 | |
|       }
 | |
|     };
 | |
|   },
 | |
|   toString() {
 | |
|     return "EditKeyword";
 | |
|   },
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Transaction for sorting a folder by name.
 | |
|  *
 | |
|  * Required Input Properties: guid.
 | |
|  */
 | |
| PT.SortByName = DefineTransaction(["guid"]);
 | |
| PT.SortByName.prototype = {
 | |
|   async execute({ guid }) {
 | |
|     let sortingMethod = (node_a, node_b) => {
 | |
|       if (
 | |
|         PlacesUtils.nodeIsContainer(node_a) &&
 | |
|         !PlacesUtils.nodeIsContainer(node_b)
 | |
|       ) {
 | |
|         return -1;
 | |
|       }
 | |
|       if (
 | |
|         !PlacesUtils.nodeIsContainer(node_a) &&
 | |
|         PlacesUtils.nodeIsContainer(node_b)
 | |
|       ) {
 | |
|         return 1;
 | |
|       }
 | |
|       return node_a.title.localeCompare(node_b.title);
 | |
|     };
 | |
|     let oldOrderGuids = [];
 | |
|     let newOrderGuids = [];
 | |
|     let preSepNodes = [];
 | |
| 
 | |
|     // This is not great, since it does main-thread IO.
 | |
|     // PromiseBookmarksTree can't be used, since it' won't stop at the first level'.
 | |
|     let root = PlacesUtils.getFolderContents(guid, false, false).root;
 | |
|     for (let i = 0; i < root.childCount; ++i) {
 | |
|       let node = root.getChild(i);
 | |
|       oldOrderGuids.push(node.bookmarkGuid);
 | |
|       if (PlacesUtils.nodeIsSeparator(node)) {
 | |
|         if (preSepNodes.length) {
 | |
|           preSepNodes.sort(sortingMethod);
 | |
|           newOrderGuids.push(...preSepNodes.map(n => n.bookmarkGuid));
 | |
|           preSepNodes = [];
 | |
|         }
 | |
|         newOrderGuids.push(node.bookmarkGuid);
 | |
|       } else {
 | |
|         preSepNodes.push(node);
 | |
|       }
 | |
|     }
 | |
|     root.containerOpen = false;
 | |
|     if (preSepNodes.length) {
 | |
|       preSepNodes.sort(sortingMethod);
 | |
|       newOrderGuids.push(...preSepNodes.map(n => n.bookmarkGuid));
 | |
|     }
 | |
|     await PlacesUtils.bookmarks.reorder(guid, newOrderGuids);
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       await PlacesUtils.bookmarks.reorder(guid, oldOrderGuids);
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       await PlacesUtils.bookmarks.reorder(guid, newOrderGuids);
 | |
|     };
 | |
|   },
 | |
|   toString() {
 | |
|     return "SortByName";
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Transaction for removing an item (any type).
 | |
|  *
 | |
|  * Required Input Properties: guids.
 | |
|  */
 | |
| PT.Remove = DefineTransaction(["guids"]);
 | |
| PT.Remove.prototype = {
 | |
|   async execute({ guids }) {
 | |
|     let removedItems = [];
 | |
| 
 | |
|     for (let guid of guids) {
 | |
|       try {
 | |
|         // Although we don't strictly need to get this information for the remove,
 | |
|         // we do need it for the possibility of undo().
 | |
|         removedItems.push(await PlacesUtils.promiseBookmarksTree(guid));
 | |
|       } catch (ex) {
 | |
|         if (!ex.becauseInvalidURL) {
 | |
|           throw new Error(`Failed to get info for the guid: ${guid}: ${ex}`);
 | |
|         }
 | |
|         removedItems.push({ guid });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let removeThem = async function () {
 | |
|       if (removedItems.length) {
 | |
|         // We have to pass just the guids as although remove() accepts full
 | |
|         // info items, promiseBookmarksTree returns dateAdded and lastModified
 | |
|         // as PRTime rather than date types.
 | |
|         await PlacesUtils.bookmarks.remove(
 | |
|           removedItems.map(info => ({ guid: info.guid }))
 | |
|         );
 | |
|       }
 | |
|     };
 | |
|     await removeThem();
 | |
| 
 | |
|     this.undo = async function () {
 | |
|       for (let info of removedItems) {
 | |
|         try {
 | |
|           await createItemsFromBookmarksTree(info, true);
 | |
|         } catch (ex) {
 | |
|           console.error(`Unable to undo removal of ${info.guid}`);
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     this.redo = removeThem;
 | |
|   },
 | |
|   toString() {
 | |
|     return "Remove";
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Transaction for tagging urls.
 | |
|  *
 | |
|  * Required Input Properties: urls, tags.
 | |
|  */
 | |
| PT.Tag = DefineTransaction(["urls", "tags"]);
 | |
| PT.Tag.prototype = {
 | |
|   async execute({ urls, tags }) {
 | |
|     let onUndo = [],
 | |
|       onRedo = [];
 | |
|     for (let url of urls) {
 | |
|       if (!(await PlacesUtils.bookmarks.fetch({ url }))) {
 | |
|         // Tagging is only allowed for bookmarked URIs (but see 424160).
 | |
|         let createTxn = lazy.TransactionsHistory.getRawTransaction(
 | |
|           PT.NewBookmark({
 | |
|             url,
 | |
|             tags,
 | |
|             parentGuid: PlacesUtils.bookmarks.unfiledGuid,
 | |
|           })
 | |
|         );
 | |
|         await createTxn.execute();
 | |
|         onUndo.unshift(createTxn.undo.bind(createTxn));
 | |
|         onRedo.push(createTxn.redo.bind(createTxn));
 | |
|       } else {
 | |
|         let uri = url.URI;
 | |
|         let currentTags = PlacesUtils.tagging.getTagsForURI(uri);
 | |
|         let newTags = tags.filter(t => !currentTags.includes(t));
 | |
|         if (newTags.length) {
 | |
|           PlacesUtils.tagging.tagURI(uri, newTags);
 | |
|           onUndo.unshift(() => {
 | |
|             PlacesUtils.tagging.untagURI(uri, newTags);
 | |
|           });
 | |
|           onRedo.push(() => {
 | |
|             PlacesUtils.tagging.tagURI(uri, newTags);
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     this.undo = async function () {
 | |
|       for (let f of onUndo) {
 | |
|         await f();
 | |
|       }
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       for (let f of onRedo) {
 | |
|         await f();
 | |
|       }
 | |
|     };
 | |
|   },
 | |
|   toString() {
 | |
|     return "Tag";
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Transaction for removing tags from a URI.
 | |
|  *
 | |
|  * Required Input Properties: urls.
 | |
|  * Optional Input Properties: tags.
 | |
|  *
 | |
|  * If |tags| is not set, all tags set for |url| are removed.
 | |
|  */
 | |
| PT.Untag = DefineTransaction(["urls"], ["tags"]);
 | |
| PT.Untag.prototype = {
 | |
|   execute({ urls, tags }) {
 | |
|     let onUndo = [],
 | |
|       onRedo = [];
 | |
|     for (let url of urls) {
 | |
|       let uri = url.URI;
 | |
|       let tagsToRemove;
 | |
|       let tagsSet = PlacesUtils.tagging.getTagsForURI(uri);
 | |
|       if (tags.length) {
 | |
|         tagsToRemove = tags.filter(t => tagsSet.includes(t));
 | |
|       } else {
 | |
|         tagsToRemove = tagsSet;
 | |
|       }
 | |
|       if (tagsToRemove.length) {
 | |
|         PlacesUtils.tagging.untagURI(uri, tagsToRemove);
 | |
|       }
 | |
|       onUndo.unshift(() => {
 | |
|         if (tagsToRemove.length) {
 | |
|           PlacesUtils.tagging.tagURI(uri, tagsToRemove);
 | |
|         }
 | |
|       });
 | |
|       onRedo.push(() => {
 | |
|         if (tagsToRemove.length) {
 | |
|           PlacesUtils.tagging.untagURI(uri, tagsToRemove);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|     this.undo = async function () {
 | |
|       for (let f of onUndo) {
 | |
|         await f();
 | |
|       }
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       for (let f of onRedo) {
 | |
|         await f();
 | |
|       }
 | |
|     };
 | |
|   },
 | |
|   toString() {
 | |
|     return "Untag";
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Transaction for renaming a tag.
 | |
|  *
 | |
|  * Required Input Properties: oldTag, tag.
 | |
|  */
 | |
| PT.RenameTag = DefineTransaction(["oldTag", "tag"]);
 | |
| PT.RenameTag.prototype = {
 | |
|   async execute({ oldTag, tag }) {
 | |
|     // For now this is implemented by untagging and tagging all the bookmarks.
 | |
|     // We should create a specialized bookmarking API to just rename the tag.
 | |
|     let onUndo = [],
 | |
|       onRedo = [];
 | |
|     let urls = new Set();
 | |
|     await PlacesUtils.bookmarks.fetch({ tags: [oldTag] }, b => urls.add(b.url));
 | |
|     if (urls.size > 0) {
 | |
|       urls = Array.from(urls);
 | |
|       let tagTxn = lazy.TransactionsHistory.getRawTransaction(
 | |
|         PT.Tag({ urls, tags: [tag] })
 | |
|       );
 | |
|       await tagTxn.execute();
 | |
|       onUndo.unshift(tagTxn.undo.bind(tagTxn));
 | |
|       onRedo.push(tagTxn.redo.bind(tagTxn));
 | |
|       let untagTxn = lazy.TransactionsHistory.getRawTransaction(
 | |
|         PT.Untag({ urls, tags: [oldTag] })
 | |
|       );
 | |
|       await untagTxn.execute();
 | |
|       onUndo.unshift(untagTxn.undo.bind(untagTxn));
 | |
|       onRedo.push(untagTxn.redo.bind(untagTxn));
 | |
| 
 | |
|       // Update all the place: queries that refer to this tag.
 | |
|       let db = await PlacesUtils.promiseDBConnection();
 | |
|       let rows = await db.executeCached(
 | |
|         `
 | |
|         SELECT h.url, b.guid, b.title
 | |
|         FROM moz_places h
 | |
|         JOIN moz_bookmarks b ON b.fk = h.id
 | |
|         WHERE url_hash BETWEEN hash("place", "prefix_lo")
 | |
|                            AND hash("place", "prefix_hi")
 | |
|           AND url LIKE :tagQuery
 | |
|       `,
 | |
|         { tagQuery: "%tag=%" }
 | |
|       );
 | |
|       for (let row of rows) {
 | |
|         let url = row.getResultByName("url");
 | |
|         try {
 | |
|           url = new URL(url);
 | |
|           let urlParams = new URLSearchParams(url.pathname);
 | |
|           let tags = urlParams.getAll("tag");
 | |
|           if (!tags.includes(oldTag)) {
 | |
|             continue;
 | |
|           }
 | |
|           if (tags.length > 1) {
 | |
|             // URLSearchParams cannot set more than 1 same-named param.
 | |
|             urlParams.delete("tag");
 | |
|             urlParams.set("tag", tag);
 | |
|             url = new URL(
 | |
|               url.protocol +
 | |
|                 urlParams +
 | |
|                 "&tag=" +
 | |
|                 tags.filter(t => t != oldTag).join("&tag=")
 | |
|             );
 | |
|           } else {
 | |
|             urlParams.set("tag", tag);
 | |
|             url = new URL(url.protocol + urlParams);
 | |
|           }
 | |
|         } catch (ex) {
 | |
|           console.error(
 | |
|             "Invalid bookmark url: " + row.getResultByName("url") + ": " + ex
 | |
|           );
 | |
|           continue;
 | |
|         }
 | |
|         let guid = row.getResultByName("guid");
 | |
|         let title = row.getResultByName("title");
 | |
| 
 | |
|         let editUrlTxn = lazy.TransactionsHistory.getRawTransaction(
 | |
|           PT.EditUrl({ guid, url })
 | |
|         );
 | |
|         await editUrlTxn.execute();
 | |
|         onUndo.unshift(editUrlTxn.undo.bind(editUrlTxn));
 | |
|         onRedo.push(editUrlTxn.redo.bind(editUrlTxn));
 | |
|         if (title == oldTag) {
 | |
|           let editTitleTxn = lazy.TransactionsHistory.getRawTransaction(
 | |
|             PT.EditTitle({ guid, title: tag })
 | |
|           );
 | |
|           await editTitleTxn.execute();
 | |
|           onUndo.unshift(editTitleTxn.undo.bind(editTitleTxn));
 | |
|           onRedo.push(editTitleTxn.redo.bind(editTitleTxn));
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     this.undo = async function () {
 | |
|       for (let f of onUndo) {
 | |
|         await f();
 | |
|       }
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       for (let f of onRedo) {
 | |
|         await f();
 | |
|       }
 | |
|     };
 | |
|   },
 | |
|   toString() {
 | |
|     return "RenameTag";
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Transaction for copying an item.
 | |
|  *
 | |
|  * Required Input Properties: guid, newParentGuid
 | |
|  * Optional Input Properties: newIndex.
 | |
|  */
 | |
| PT.Copy = DefineTransaction(["guid", "newParentGuid"], ["newIndex"]);
 | |
| PT.Copy.prototype = {
 | |
|   async execute({ guid, newParentGuid, newIndex }) {
 | |
|     let creationInfo = null;
 | |
|     try {
 | |
|       creationInfo = await PlacesUtils.promiseBookmarksTree(guid);
 | |
|     } catch (ex) {
 | |
|       throw new Error(
 | |
|         "Failed to get info for the specified item (guid: " +
 | |
|           guid +
 | |
|           "). Ex: " +
 | |
|           ex
 | |
|       );
 | |
|     }
 | |
|     creationInfo.parentGuid = newParentGuid;
 | |
|     creationInfo.index = newIndex;
 | |
| 
 | |
|     let newItemGuid = await createItemsFromBookmarksTree(creationInfo, false);
 | |
|     let newItemInfo = null;
 | |
|     this.undo = async function () {
 | |
|       if (!newItemInfo) {
 | |
|         newItemInfo = await PlacesUtils.promiseBookmarksTree(newItemGuid);
 | |
|       }
 | |
|       await PlacesUtils.bookmarks.remove(newItemGuid);
 | |
|     };
 | |
|     this.redo = async function () {
 | |
|       await createItemsFromBookmarksTree(newItemInfo, true);
 | |
|     };
 | |
| 
 | |
|     return newItemGuid;
 | |
|   },
 | |
|   toString() {
 | |
|     return "Copy";
 | |
|   },
 | |
| };
 | 
