--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -130,17 +130,17 @@ this.EXPORTED_SYMBOLS = ["PlacesTransact
*
* WARNING: "nested" batches are not supported, if you call batch while another
* batch is still running, the new batch is enqueued with all other PTM work
* and thus not run until the running batch ends. The same goes for undo, redo
* and clearTransactionsHistory (note batches cannot be done partially, meaning
* undo and redo calls that during a batch are just enqueued).
*
* *****************************************************************************
- * IT"S PARTICULARLY IMPORTANT NOT TO YIELD ANY PROMISE RETURNED BY ANY OF
+ * IT'S PARTICULARLY IMPORTANT NOT TO await ANY PROMISE RETURNED BY ANY OF
* THESE METHODS (undo, redo, clearTransactionsHistory) FROM A BATCH FUNCTION.
* UNTIL WE FIND A WAY TO THROW IN THAT CASE (SEE BUG 1091446) DOING SO WILL
* COMPLETELY BREAK PTM UNTIL SHUTDOWN, NOT ALLOWING THE EXECUTION OF ANY
* TRANSACTION!
* *****************************************************************************
*
* Serialization
* -------------
@@ -169,26 +169,33 @@ this.EXPORTED_SYMBOLS = ["PlacesTransact
* [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.
*/
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-Components.utils.import("resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+const TRANSACTIONS_QUEUE_TIMEOUT_MS = 240000; // 4 Mins.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
-Components.utils.importGlobalProperties(["URL"]);
+Cu.importGlobalProperties(["URL"]);
+
+function setTimeout(callback, ms) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
+}
var TransactionsHistory = [];
TransactionsHistory.__proto__ = {
__proto__: Array.prototype,
// The index of the first undo entry (if any) - See the documentation
// at the top of this file.
_undoPosition: 0,
@@ -207,74 +214,74 @@ TransactionsHistory.__proto__ = {
// 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.
proxifiedToRaw: new WeakMap(),
/**
* Proxify a transaction object for consumers.
- * @param aRawTransaction
+ * @param rawTransaction
* the raw transaction object.
* @return the proxified transaction object.
* @see getRawTransaction for retrieving the raw transaction.
*/
- proxifyTransaction(aRawTransaction) {
+ proxifyTransaction(rawTransaction) {
let proxy = Object.freeze({
transact() {
return TransactionsManager.transact(this);
}
});
- this.proxifiedToRaw.set(proxy, aRawTransaction);
+ 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(aValue) {
- return this.proxifiedToRaw.has(aValue);
+ 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(aProxy) {
- return this.proxifiedToRaw.get(aProxy);
+ 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(aProxifiedTransaction, aForceNewEntry = false) {
- if (!this.isProxifiedTransactionObject(aProxifiedTransaction))
+ add(proxifiedTransaction, forceNewEntry = false) {
+ if (!this.isProxifiedTransactionObject(proxifiedTransaction))
throw new Error("aProxifiedTransaction is not a proxified transaction");
- if (this.length == 0 || aForceNewEntry) {
+ if (this.length == 0 || forceNewEntry) {
this.clearRedoEntries();
- this.unshift([aProxifiedTransaction]);
+ this.unshift([proxifiedTransaction]);
} else {
- this[this.undoPosition].unshift(aProxifiedTransaction);
+ this[this.undoPosition].unshift(proxifiedTransaction);
}
},
/**
* Clear all undo entries.
*/
clearUndoEntries() {
if (this.undoPosition < this.length)
@@ -302,40 +309,40 @@ TransactionsHistory.__proto__ = {
}
};
var PlacesTransactions = {
/**
* @see Batches in the module documentation.
*/
- batch(aToBatch) {
- if (Array.isArray(aToBatch)) {
- if (aToBatch.length == 0)
- throw new Error("aToBatch must not be an empty array");
+ batch(transactionsToBatch) {
+ if (Array.isArray(transactionsToBatch)) {
+ if (transactionsToBatch.length == 0)
+ throw new Error("Must pass a non-empty array");
- if (aToBatch.some(
+ if (transactionsToBatch.some(
o => !TransactionsHistory.isProxifiedTransactionObject(o))) {
- throw new Error("aToBatch contains non-transaction element");
+ throw new Error("Must pass only transaction entries");
}
return TransactionsManager.batch(async function() {
- for (let txn of aToBatch) {
+ for (let txn of transactionsToBatch) {
try {
await txn.transact();
} catch (ex) {
console.error(ex);
}
}
});
}
- if (typeof(aToBatch) == "function") {
- return TransactionsManager.batch(aToBatch);
+ if (typeof(transactionsToBatch) == "function") {
+ return TransactionsManager.batch(transactionsToBatch);
}
- throw new Error("aToBatch must be either a function or a transactions array");
+ throw new Error("Must pass either a function or a transactions array");
},
/**
* 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.
@@ -358,54 +365,54 @@ var PlacesTransactions = {
redo() {
return TransactionsManager.redo();
},
/**
* Asynchronously clear the undo, redo, or all entries from the transactions
* history.
*
- * @param [optional] aUndoEntries
+ * @param [optional] undoEntries
* Whether or not to clear undo entries. Default: true.
- * @param [optional] aRedoEntries
+ * @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(aUndoEntries = true, aRedoEntries = true) {
- return TransactionsManager.clearTransactionsHistory(aUndoEntries, aRedoEntries);
+ clearTransactionsHistory(undoEntries = true, redoEntries = true) {
+ return TransactionsManager.clearTransactionsHistory(undoEntries, redoEntries);
},
/**
* The numbers of entries in the transactions history.
*/
get length() {
return TransactionsHistory.length;
},
/**
* Get the transaction history entry at a given index. Each entry consists
* of one or more transaction objects.
*
- * @param aIndex
+ * @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(aIndex) {
- if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
+ entry(index) {
+ if (!Number.isInteger(index) || index < 0 || index >= this.length)
throw new Error("Invalid index");
- return TransactionsHistory[aIndex];
+ return 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.
*/
@@ -440,41 +447,58 @@ var PlacesTransactions = {
function Enqueuer() {
this._promise = Promise.resolve();
}
Enqueuer.prototype = {
/**
* Spawn a functions once all previous functions enqueued are done running,
* and all promises passed to alsoWaitFor are no longer pending.
*
- * @param aFunc
+ * @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(aFunc) {
- let promise = this._promise.then(aFunc);
+ enqueue(func) {
+ // 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(console.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 aPromise
+ * @param otherPromise
* any promise.
*/
- alsoWaitFor(aPromise) {
+ alsoWaitFor(otherPromise) {
// We don't care if aPromise resolves or rejects, but just that is not
// pending anymore.
- let promise = aPromise.catch(console.error);
+ // 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;
@@ -496,53 +520,53 @@ var TransactionsManager = {
// executed successfully).
_createdBatchEntry: false,
// 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(),
- transact(aTxnProxy) {
- let rawTxn = TransactionsHistory.getRawTransaction(aTxnProxy);
+ transact(txnProxy) {
+ let rawTxn = 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.");
// Add it in advance so one doesn't accidentally do
// sameTxn.transact(); sameTxn.transact();
this._executedTransactions.add(rawTxn);
let promise = this._transactEnqueuer.enqueue(async () => {
// 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 = !this._batching || !this._createdBatchEntry;
- TransactionsHistory.add(aTxnProxy, forceNewEntry);
+ TransactionsHistory.add(txnProxy, forceNewEntry);
if (this._batching)
this._createdBatchEntry = true;
this._updateCommandsOnActiveWindow();
return retval;
});
this._mainEnqueuer.alsoWaitFor(promise);
return promise;
},
- batch(aTask) {
+ batch(task) {
return this._mainEnqueuer.enqueue(async () => {
this._batching = true;
this._createdBatchEntry = false;
let rv;
try {
// We should return here, but bug 958949 makes that impossible.
- rv = await aTask();
+ rv = await task();
} finally {
this._batching = false;
this._createdBatchEntry = false;
}
return rv;
});
},
@@ -556,18 +580,17 @@ var TransactionsManager = {
return;
for (let txnProxy of entry) {
try {
await 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,
- "Couldn't undo a transaction, clearing all undo entries.");
+ console.error(ex, "Can't undo a transaction, clearing undo entries.");
TransactionsHistory.clearUndoEntries();
return;
}
}
TransactionsHistory._undoPosition++;
this._updateCommandsOnActiveWindow();
});
this._transactEnqueuer.alsoWaitFor(promise);
@@ -581,44 +604,44 @@ var TransactionsManager = {
let promise = this._mainEnqueuer.enqueue(async () => {
let entry = TransactionsHistory.topRedoEntry;
if (!entry)
return;
for (let i = entry.length - 1; i >= 0; i--) {
let transaction = TransactionsHistory.getRawTransaction(entry[i]);
try {
- if (transaction.redo)
+ if (transaction.redo) {
await transaction.redo();
- else
+ } 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,
- "Couldn't redo a transaction, clearing all redo entries.");
+ console.error(ex, "Can't redo a transaction, clearing redo entries.");
TransactionsHistory.clearRedoEntries();
return;
}
}
TransactionsHistory._undoPosition--;
this._updateCommandsOnActiveWindow();
});
this._transactEnqueuer.alsoWaitFor(promise);
return promise;
},
- clearTransactionsHistory(aUndoEntries, aRedoEntries) {
+ clearTransactionsHistory(undoEntries, redoEntries) {
let promise = this._mainEnqueuer.enqueue(function() {
- if (aUndoEntries && aRedoEntries)
+ if (undoEntries && redoEntries)
TransactionsHistory.clearAllEntries();
- else if (aUndoEntries)
+ else if (undoEntries)
TransactionsHistory.clearUndoEntries();
- else if (aRedoEntries)
+ else if (redoEntries)
TransactionsHistory.clearRedoEntries();
else
throw new Error("either aUndoEntries or aRedoEntries should be true");
});
this._transactEnqueuer.alsoWaitFor(promise);
return promise;
},
@@ -626,17 +649,19 @@ var TransactionsManager = {
// 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"); }
+ } 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
@@ -646,222 +671,210 @@ var TransactionsManager = {
* 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(aRequiredProps = [], aOptionalProps = []) {
- for (let prop of [...aRequiredProps, ...aOptionalProps]) {
+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(aInput) {
+ 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(aInput);
+ return new ctor(input);
- if (aRequiredProps.length > 0 || aOptionalProps.length > 0) {
+ if (requiredProps.length > 0 || optionalProps.length > 0) {
// Bind the input properties to the arguments of execute.
- let input = DefineTransaction.verifyInput(aInput, aRequiredProps,
- aOptionalProps);
- let executeArgs = [this,
- ...aRequiredProps.map(prop => input[prop]),
- ...aOptionalProps.map(prop => input[prop])];
- this.execute = Function.bind.apply(this.execute, executeArgs);
+ input = DefineTransaction.verifyInput(input, requiredProps, optionalProps);
+ this.execute = this.execute.bind(this, input);
}
return TransactionsHistory.proxifyTransaction(this);
};
return ctor;
}
-function simpleValidateFunc(aCheck) {
+function simpleValidateFunc(checkFn) {
return v => {
- if (!aCheck(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);
+ 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");
}
DefineTransaction.annotationObjectValidate = function(obj) {
- let checkProperty = (aPropName, aRequired, aCheckFunc) => {
- if (aPropName in obj)
- return aCheckFunc(obj[aPropName]);
+ let checkProperty = (prop, required, checkFn) => {
+ if (prop in obj)
+ return checkFn(obj[prop]);
- return !aRequired;
+ return !required;
};
if (obj &&
checkProperty("name", true, v => typeof(v) == "string" && v.length > 0) &&
checkProperty("expires", false, Number.isInteger) &&
checkProperty("flags", false, Number.isInteger) &&
checkProperty("value", false, isPrimitive) ) {
// Nothing else should be set
let validKeys = ["name", "value", "flags", "expires"];
- if (Object.keys(obj).every( (k) => validKeys.includes(k)))
+ if (Object.keys(obj).every(k => validKeys.includes(k)))
return obj;
}
throw new Error("Invalid annotation object");
};
DefineTransaction.urlValidate = function(url) {
- // When this module is updated to use Bookmarks.jsm, we should actually
- // convert nsIURIs/spec to URL objects.
- if (url instanceof Components.interfaces.nsIURI)
- return url;
- let spec = url instanceof URL ? url.href : url;
- return NetUtil.newURI(spec);
+ if (url instanceof Ci.nsIURI)
+ return new URL(url.spec);
+ return new URL(url);
};
DefineTransaction.inputProps = new Map();
-DefineTransaction.defineInputProps =
-function(aNames, aValidationFunction, aDefaultValue) {
- for (let name of aNames) {
+DefineTransaction.defineInputProps = function(names, validateFn, defaultValue) {
+ for (let name of names) {
this.inputProps.set(name, {
- validateValue(aValue) {
- if (aValue === undefined)
- return aDefaultValue;
+ validateValue(value) {
+ if (value === undefined)
+ return defaultValue;
try {
- return aValidationFunction(aValue);
+ return validateFn(value);
} catch (ex) {
throw new Error(`Invalid value for input property ${name}`);
}
},
- validateInput(aInput, aRequired) {
- if (aRequired && !(name in aInput))
+ validateInput(input, required) {
+ if (required && !(name in input))
throw new Error(`Required input property is missing: ${name}`);
- return this.validateValue(aInput[name]);
+ return this.validateValue(input[name]);
},
isArrayProperty: false
});
}
};
-DefineTransaction.defineArrayInputProp =
-function(aName, aBasePropertyName) {
- let baseProp = this.inputProps.get(aBasePropertyName);
+DefineTransaction.defineArrayInputProp = function(name, basePropertyName) {
+ let baseProp = this.inputProps.get(basePropertyName);
if (!baseProp)
- throw new Error(`Unknown input property: ${aBasePropertyName}`);
+ throw new Error(`Unknown input property: ${basePropertyName}`);
- this.inputProps.set(aName, {
+ this.inputProps.set(name, {
validateValue(aValue) {
if (aValue == undefined)
return [];
if (!Array.isArray(aValue))
- throw new Error(`${aName} input property value must be an array`);
+ throw new Error(`${name} input property value must be an array`);
// This also takes care of abandoning the global scope of the input
// array (through Array.prototype).
return aValue.map(baseProp.validateValue);
},
// 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(aInput, aRequired) {
- if (aName in aInput) {
+ validateInput(input, required) {
+ if (name in input) {
// It's not allowed to set both though.
- if (aBasePropertyName in aInput) {
- throw new Error(`It is not allowed to set both ${aName} and
- ${aBasePropertyName} as input properties`);
+ if (basePropertyName in input) {
+ throw new Error(`It is not allowed to set both ${name} and
+ ${basePropertyName} as input properties`);
}
- let array = this.validateValue(aInput[aName]);
- if (aRequired && array.length == 0) {
+ let array = this.validateValue(input[name]);
+ if (required && array.length == 0) {
throw new Error(`Empty array passed for required input property:
- ${aName}`);
+ ${name}`);
}
return array;
}
// If the property is required and it's not set as is, check if the base
// property is set.
- if (aRequired && !(aBasePropertyName in aInput))
- throw new Error(`Required input property is missing: ${aName}`);
+ if (required && !(basePropertyName in input))
+ throw new Error(`Required input property is missing: ${name}`);
- if (aBasePropertyName in aInput)
- return [baseProp.validateValue(aInput[aBasePropertyName])];
+ if (basePropertyName in input)
+ return [baseProp.validateValue(input[basePropertyName])];
return [];
},
isArrayProperty: true
});
};
-DefineTransaction.validatePropertyValue =
-function(aProp, aInput, aRequired) {
- return this.inputProps.get(aProp).validateInput(aInput, aRequired);
+DefineTransaction.validatePropertyValue = function(prop, input, required) {
+ return this.inputProps.get(prop).validateInput(input, required);
};
-DefineTransaction.getInputObjectForSingleValue =
-function(aInput, aRequiredProps, aOptionalProps) {
+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 (aRequiredProps.length > 1 ||
- (aRequiredProps.length == 0 && aOptionalProps.length > 1)) {
+ if (requiredProps.length > 1 ||
+ (requiredProps.length == 0 && optionalProps.length > 1)) {
throw new Error("Transaction input isn't an object");
}
- let propName = aRequiredProps.length == 1 ?
- aRequiredProps[0] : aOptionalProps[0];
+ let propName = requiredProps.length == 1 ? requiredProps[0]
+ : optionalProps[0];
let propValue =
- this.inputProps.get(propName).isArrayProperty && !Array.isArray(aInput) ?
- [aInput] : aInput;
+ this.inputProps.get(propName).isArrayProperty && !Array.isArray(input) ?
+ [input] : input;
return { [propName]: propValue };
};
-DefineTransaction.verifyInput =
-function(aInput, aRequiredProps = [], aOptionalProps = []) {
- if (aRequiredProps.length == 0 && aOptionalProps.length == 0)
+DefineTransaction.verifyInput = function(input,
+ requiredProps = [],
+ optionalProps = []) {
+ if (requiredProps.length == 0 && optionalProps.length == 0)
return {};
// If there's just a single required/optional property, we allow passing it
- // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGuid)
- // rather than PlacesTransactions.RemoveItem({ guid: myGuid}).
+ // 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 - e.g. one cannot
// pass an annotation object this way (note there is no use case for this at
// the moment anyway).
- let input = aInput;
- let isSinglePropertyInput =
- isPrimitive(aInput) ||
- Array.isArray(aInput) ||
- (aInput instanceof Components.interfaces.nsISupports);
+ let isSinglePropertyInput = isPrimitive(input) ||
+ Array.isArray(input) ||
+ (input instanceof Ci.nsISupports);
if (isSinglePropertyInput) {
- input = this.getInputObjectForSingleValue(aInput,
- aRequiredProps,
- aOptionalProps);
+ input = this.getInputObjectForSingleValue(input, requiredProps, optionalProps);
}
- let fixedInput = { };
- for (let prop of aRequiredProps) {
+ let fixedInput = {};
+ for (let prop of requiredProps) {
fixedInput[prop] = this.validatePropertyValue(prop, input, true);
}
- for (let prop of aOptionalProps) {
+ 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.
@@ -882,176 +895,145 @@ DefineTransaction.defineInputProps(["ann
DefineTransaction.defineArrayInputProp("guids", "guid");
DefineTransaction.defineArrayInputProp("urls", "url");
DefineTransaction.defineArrayInputProp("tags", "tag");
DefineTransaction.defineArrayInputProp("annotations", "annotation");
DefineTransaction.defineArrayInputProp("excludingAnnotations",
"excludingAnnotation");
/**
- * Internal helper for implementing the execute method of NewBookmark, NewFolder
- * and NewSeparator.
- *
- * @param aTransaction
- * The transaction object
- * @param aParentGuid
- * The GUID of the parent folder
- * @param aCreateItemFunction(aParentId, aGuidToRestore)
- * The function to be called for creating the item on execute and redo.
- * It should return the itemId for the new item
- * - aGuidToRestore - the GUID to set for the item (used for redo).
- * @param [optional] aOnUndo
- * an additional function to call after undo
- * @param [optional] aOnRedo
- * an additional function to call after redo
- */
-async function ExecuteCreateItem(aTransaction, aParentGuid, aCreateItemFunction,
- aOnUndo = null, aOnRedo = null) {
- let parentId = await PlacesUtils.promiseItemId(aParentGuid),
- itemId = await aCreateItemFunction(parentId, ""),
- guid = await PlacesUtils.promiseItemGuid(itemId);
-
- // On redo, we'll restore the date-added and last-modified properties.
- let dateAdded = 0, lastModified = 0;
- aTransaction.undo = async function() {
- if (dateAdded == 0) {
- dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId);
- lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId);
- }
- PlacesUtils.bookmarks.removeItem(itemId);
- if (aOnUndo) {
- await aOnUndo();
- }
- };
- aTransaction.redo = async function() {
- parentId = await PlacesUtils.promiseItemId(aParentGuid);
- itemId = await aCreateItemFunction(parentId, guid);
- if (aOnRedo)
- await aOnRedo();
-
- // aOnRedo is called first to make sure it doesn't override
- // lastModified.
- PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded);
- PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
- PlacesUtils.bookmarks.setItemLastModified(parentId, dateAdded);
- };
- return guid;
-}
-
-/**
* Creates items (all types) from a bookmarks tree representation, as defined
* in PlacesUtils.promiseBookmarksTree.
*
- * @param aBookmarksTree
+ * @param tree
* the bookmarks tree object. You may pass either a bookmarks tree
* returned by promiseBookmarksTree, or a manually defined one.
- * @param [optional] aRestoring (default: false)
+ * @param [optional] restoring (default: false)
* Whether or not the items are restored. Only in restore mode, are
* the guid, dateAdded and lastModified properties honored.
- * @param [optional] aExcludingAnnotations
+ * @param [optional] excludingAnnotations
* Array of annotations names to ignore in aBookmarksTree. This argument
* is ignored if aRestoring is set.
* @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.
*/
-async function createItemsFromBookmarksTree(aBookmarksTree, aRestoring = false,
- aExcludingAnnotations = []) {
- function extractLivemarkDetails(aAnnos) {
+// TODO: Replace most of this with insertTree.
+async function createItemsFromBookmarksTree(tree, restoring = false,
+ excludingAnnotations = []) {
+ function extractLivemarkDetails(annos) {
let feedURI = null, siteURI = null;
- aAnnos = aAnnos.filter(
- aAnno => {
- switch (aAnno.name) {
+ annos = annos.filter(anno => {
+ switch (anno.name) {
case PlacesUtils.LMANNO_FEEDURI:
- feedURI = NetUtil.newURI(aAnno.value);
+ feedURI = Services.io.newURI(anno.value);
return false;
case PlacesUtils.LMANNO_SITEURI:
- siteURI = NetUtil.newURI(aAnno.value);
+ siteURI = Services.io.newURI(anno.value);
return false;
default:
return true;
- }
- } );
- return [feedURI, siteURI];
+ }
+ });
+ return [feedURI, siteURI, annos];
}
- async function createItem(aItem,
- aParentGuid,
- aIndex = PlacesUtils.bookmarks.DEFAULT_INDEX) {
- let itemId;
- let guid = aRestoring ? aItem.guid : undefined;
- let parentId = await PlacesUtils.promiseItemId(aParentGuid);
- let annos = aItem.annos ? [...aItem.annos] : [];
- switch (aItem.type) {
+ 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 annos = item.annos ? [...item.annos] : [];
+ let shouldResetLastModified = false;
+ switch (item.type) {
case PlacesUtils.TYPE_X_MOZ_PLACE: {
- let uri = NetUtil.newURI(aItem.uri);
- itemId = PlacesUtils.bookmarks.insertBookmark(
- parentId, uri, aIndex, aItem.title, guid);
- if ("keyword" in aItem) {
- await PlacesUtils.keywords.insert({
- keyword: aItem.keyword,
- url: uri.spec
- });
+ 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 aItem) {
- PlacesUtils.tagging.tagURI(uri, aItem.tags.split(","));
+ if ("tags" in item) {
+ PlacesUtils.tagging.tagURI(Services.io.newURI(item.uri),
+ item.tags.split(","));
+ if (restoring)
+ shouldResetLastModified = true;
}
break;
}
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: {
// Either a folder or a livemark
- let [feedURI, siteURI] = extractLivemarkDetails(annos);
+ let feedURI, siteURI;
+ [feedURI, siteURI, annos] = extractLivemarkDetails(annos);
if (!feedURI) {
- itemId = PlacesUtils.bookmarks.createFolder(
- parentId, aItem.title, aIndex, guid);
- if (guid === undefined)
- guid = await PlacesUtils.promiseItemGuid(itemId);
- if ("children" in aItem) {
- for (let child of aItem.children) {
+ 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;
} else {
- let livemark =
- await PlacesUtils.livemarks.addLivemark({ title: aItem.title,
- feedURI,
- siteURI,
- parentId,
- index: aIndex,
- guid});
- itemId = livemark.id;
+ info.parentId = await PlacesUtils.promiseItemId(parentGuid);
+ info.feedURI = feedURI;
+ info.siteURI = siteURI;
+ if (info.dateAdded)
+ info.dateAdded = PlacesUtils.toPRTime(info.dateAdded);
+ if (info.lastModified)
+ info.lastModified = PlacesUtils.toPRTime(info.lastModified);
+ if (typeof(item.title) == "string")
+ info.title = item.title;
+ guid = (await PlacesUtils.livemarks.addLivemark(info)).guid;
}
break;
}
case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: {
- itemId = PlacesUtils.bookmarks.insertSeparator(parentId, aIndex, guid);
+ info.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
+ guid = (await PlacesUtils.bookmarks.insert(info)).guid;
break;
}
}
if (annos.length > 0) {
- if (!aRestoring && aExcludingAnnotations.length > 0) {
- annos = annos.filter(a => !aExcludingAnnotations.includes(a.name));
-
+ if (!restoring && excludingAnnotations.length > 0) {
+ annos = annos.filter(a => !excludingAnnotations.includes(a.name));
}
- PlacesUtils.setAnnotationsForItem(itemId, annos);
+ if (annos.length > 0) {
+ let itemId = await PlacesUtils.promiseItemId(guid);
+ PlacesUtils.setAnnotationsForItem(itemId, annos);
+ if (restoring)
+ shouldResetLastModified = true;
+ }
}
- if (aRestoring) {
- if ("dateAdded" in aItem)
- PlacesUtils.bookmarks.setItemDateAdded(itemId, aItem.dateAdded);
- if ("lastModified" in aItem)
- PlacesUtils.bookmarks.setItemLastModified(itemId, aItem.lastModified);
+ if (shouldResetLastModified) {
+ let lastModified = PlacesUtils.toDate(item.lastModified);
+ await PlacesUtils.bookmarks.update({ guid, lastModified });
}
- return itemId;
+
+ return guid;
}
- return await createItem(aBookmarksTree,
- aBookmarksTree.parentGuid,
- aBookmarksTree.index);
+ return await 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.
*****************************************************************************/
@@ -1062,138 +1044,148 @@ var PT = PlacesTransactions;
* Transaction for creating a bookmark.
*
* Required Input Properties: url, parentGuid.
* Optional Input Properties: index, title, keyword, annotations, tags.
*
* When this transaction is executed, it's resolved to the new bookmark's GUID.
*/
PT.NewBookmark = DefineTransaction(["parentGuid", "url"],
- ["index", "title", "keyword", "postData",
- "annotations", "tags"]);
+ ["index", "title", "annotations", "tags"]);
PT.NewBookmark.prototype = Object.seal({
- execute(aParentGuid, aURI, aIndex, aTitle,
- aKeyword, aPostData, aAnnos, aTags) {
- return ExecuteCreateItem(this, aParentGuid,
- async function(parentId, guidToRestore = "") {
- let itemId = PlacesUtils.bookmarks.insertBookmark(
- parentId, aURI, aIndex, aTitle, guidToRestore);
+ async execute({ parentGuid, url, index, title, annotations, tags }) {
+ let info = { parentGuid, index, url, title };
+ // Filter tags to exclude already existing ones.
+ if (tags.length > 0) {
+ let currentTags = PlacesUtils.tagging
+ .getTagsForURI(Services.io.newURI(url.href));
+ tags = tags.filter(t => !currentTags.includes(t));
+ }
- if (aKeyword) {
- await PlacesUtils.keywords.insert({
- url: aURI.spec,
- keyword: aKeyword,
- postData: aPostData
- });
- }
- if (aAnnos.length) {
- PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
- }
- if (aTags.length > 0) {
- let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
- aTags = aTags.filter(t => !currentTags.includes(t));
- PlacesUtils.tagging.tagURI(aURI, aTags);
- }
+ async function createItem() {
+ info = await PlacesUtils.bookmarks.insert(info);
+ if (annotations.length > 0) {
+ let itemId = await PlacesUtils.promiseItemId(info.guid);
+ PlacesUtils.setAnnotationsForItem(itemId, annotations);
+ }
+ if (tags.length > 0) {
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url.href), tags);
+ }
+ }
+
+ await createItem();
- return itemId;
- },
- function _additionalOnUndo() {
- if (aTags.length > 0) {
- PlacesUtils.tagging.untagURI(aURI, aTags);
- }
- });
+ this.undo = async function() {
+ // Pick up the removed info so we have the accurate last-modified value,
+ // which could be affected by any annotation we set in createItem.
+ await PlacesUtils.bookmarks.remove(info);
+ if (tags.length > 0) {
+ PlacesUtils.tagging.untagURI(Services.io.newURI(url.href), tags);
+ }
+ };
+ this.redo = async function() {
+ await createItem();
+ // CreateItem will update the lastModified value if tags or annotations
+ // are present, but we don't care to restore it. The likely of a user
+ // creating a bookmark, undoing and redoing that, and still caring
+ // about lastModified is basically non-existant.
+ };
+ return info.guid;
}
});
/**
* Transaction for creating a folder.
*
* Required Input Properties: title, parentGuid.
* Optional Input Properties: index, annotations.
*
* When this transaction is executed, it's resolved to the new folder's GUID.
*/
PT.NewFolder = DefineTransaction(["parentGuid", "title"],
["index", "annotations"]);
PT.NewFolder.prototype = Object.seal({
- execute(aParentGuid, aTitle, aIndex, aAnnos) {
- return ExecuteCreateItem(this, aParentGuid,
- function(parentId, guidToRestore = "") {
- let itemId = PlacesUtils.bookmarks.createFolder(
- parentId, aTitle, aIndex, guidToRestore);
- if (aAnnos.length > 0)
- PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
- return itemId;
- });
+ async execute({ parentGuid, title, index, annotations }) {
+ let info = { type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid, index, title };
+
+ async function createItem() {
+ info = await PlacesUtils.bookmarks.insert(info);
+ if (annotations.length > 0) {
+ let itemId = await PlacesUtils.promiseItemId(info.guid);
+ PlacesUtils.setAnnotationsForItem(itemId, annotations);
+ }
+ }
+ await createItem();
+
+ this.undo = async function() {
+ await PlacesUtils.bookmarks.remove(info);
+ };
+ this.redo = async function() {
+ await createItem();
+ // See the reasoning in CreateItem for why we don't care
+ // about precisely resetting the lastModified value.
+ };
+ return info.guid;
}
});
/**
* 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({
- execute(aParentGuid, aIndex) {
- return ExecuteCreateItem(this, aParentGuid,
- function(parentId, guidToRestore = "") {
- let itemId = PlacesUtils.bookmarks.insertSeparator(
- parentId, aIndex, guidToRestore);
- return itemId;
- });
+ 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;
}
});
/**
* Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
* semantics).
*
* Required Input Properties: feedUrl, title, parentGuid.
* Optional Input Properties: siteUrl, index, annotations.
*
* When this transaction is executed, it's resolved to the new livemark's
* GUID.
*/
PT.NewLivemark = DefineTransaction(["feedUrl", "title", "parentGuid"],
["siteUrl", "index", "annotations"]);
PT.NewLivemark.prototype = Object.seal({
- async execute(aFeedURI, aTitle, aParentGuid, aSiteURI, aIndex, aAnnos) {
- let livemarkInfo = { title: aTitle,
- feedURI: aFeedURI,
- siteURI: aSiteURI,
- index: aIndex };
+ async execute({ feedUrl, title, parentGuid, siteUrl, index, annotations }) {
+ let livemarkInfo = { title,
+ feedURI: Services.io.newURI(feedUrl.href),
+ siteURI: siteUrl ? Services.io.newURI(siteUrl.href) : null,
+ index,
+ parentGuid};
let createItem = async function() {
- livemarkInfo.parentId = await PlacesUtils.promiseItemId(aParentGuid);
let livemark = await PlacesUtils.livemarks.addLivemark(livemarkInfo);
- if (aAnnos.length > 0)
- PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos);
-
- if ("dateAdded" in livemarkInfo) {
- PlacesUtils.bookmarks.setItemDateAdded(livemark.id,
- livemarkInfo.dateAdded);
- PlacesUtils.bookmarks.setItemLastModified(livemark.id,
- livemarkInfo.lastModified);
+ if (annotations.length > 0) {
+ PlacesUtils.setAnnotationsForItem(livemark.id, annotations);
}
return livemark;
};
let livemark = await createItem();
+ livemarkInfo.guid = livemark.guid;
+ livemarkInfo.dateAdded = livemark.dateAdded;
+ livemarkInfo.lastModified = livemark.lastModified;
+
this.undo = async function() {
- livemarkInfo.guid = livemark.guid;
- if (!("dateAdded" in livemarkInfo)) {
- livemarkInfo.dateAdded =
- PlacesUtils.bookmarks.getItemDateAdded(livemark.id);
- livemarkInfo.lastModified =
- PlacesUtils.bookmarks.getItemLastModified(livemark.id);
- }
await PlacesUtils.livemarks.removeLivemark(livemark);
};
this.redo = async function() {
livemark = await createItem();
};
return livemark.guid;
}
});
@@ -1201,323 +1193,331 @@ PT.NewLivemark.prototype = Object.seal({
/**
* Transaction for moving an item.
*
* Required Input Properties: guid, newParentGuid.
* Optional Input Properties newIndex.
*/
PT.Move = DefineTransaction(["guid", "newParentGuid"], ["newIndex"]);
PT.Move.prototype = Object.seal({
- async execute(aGuid, aNewParentGuid, aNewIndex) {
- let itemId = await PlacesUtils.promiseItemId(aGuid),
- oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId),
- oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId),
- newParentId = await PlacesUtils.promiseItemId(aNewParentGuid);
-
- PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex);
+ async execute({ guid, newParentGuid, newIndex }) {
+ let originalInfo = await PlacesUtils.bookmarks.fetch(guid);
+ if (!originalInfo)
+ throw new Error("Cannot move a non-existent item");
+ let updateInfo = { guid, parentGuid: newParentGuid, index: newIndex }
+ updateInfo = await PlacesUtils.bookmarks.update(updateInfo);
- let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId);
- this.undo = () => {
- // Moving down in the same parent takes in count removal of the item
- // so to revert positions we must move to oldIndex + 1
- if (newParentId == oldParentId && oldIndex > undoIndex)
- PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1);
- else
- PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex);
- };
- return aGuid;
+ // Moving down in the same parent takes in count removal of the item
+ // so to revert positions we must move to oldIndex + 1.
+ if (newParentGuid == originalInfo.parentGuid &&
+ originalInfo.index > updateInfo.index) {
+ originalInfo.index++;
+ }
+
+ this.undo = PlacesUtils.bookmarks.update.bind(PlacesUtils.bookmarks, originalInfo);
+ this.redo = PlacesUtils.bookmarks.update.bind(PlacesUtils.bookmarks, updateInfo);
+ return guid;
}
});
/**
* 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(aGuid, aTitle) {
- let itemId = await PlacesUtils.promiseItemId(aGuid),
- oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
- PlacesUtils.bookmarks.setItemTitle(itemId, aTitle);
- this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); };
+ 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);
}
});
/**
* 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(aGuid, aURI) {
- let itemId = await PlacesUtils.promiseItemId(aGuid),
- oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
- oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI),
- newURIAdditionalTags = null;
- PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI);
+ 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");
- // Move tags from old URI to new URI.
- if (oldURITags.length > 0) {
- // Only untag the old URI if this is the only bookmark.
- if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0)
- PlacesUtils.tagging.untagURI(oldURI, oldURITags);
+ let uri = Services.io.newURI(url.href);
+ let originalURI = Services.io.newURI(originalInfo.url.href);
+ let originalTags = PlacesUtils.tagging.getTagsForURI(originalURI);
+ let updatedInfo = { guid, url };
+ let newURIAdditionalTags = null;
- let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI);
- newURIAdditionalTags = oldURITags.filter(t => !currentNewURITags.includes(t));
- if (newURIAdditionalTags)
- PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags);
+ async function updateItem() {
+ updatedInfo = await PlacesUtils.bookmarks.update(updatedInfo);
+ // Move tags from the original URI to the new URI.
+ if (originalTags.length > 0) {
+ // 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)
+ PlacesUtils.tagging.tagURI(uri, newURIAdditionalTags);
+ }
}
+ await updateItem();
- this.undo = () => {
- PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI);
- // Move tags from new URI to old URI.
- if (oldURITags.length > 0) {
- // Only untag the new URI if this is the only bookmark.
- if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
- PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) {
- PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags);
- }
+ this.undo = async function() {
+ await PlacesUtils.bookmarks.update(originalInfo);
+ // Move tags from new URI to original URI.
+ if (originalTags.length > 0) {
+ // Only untag the new URI if this is the only bookmark.
+ if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
+ !(await PlacesUtils.bookmarks.fetch({ url }))) {
+ PlacesUtils.tagging.untagURI(uri, newURIAdditionalTags);
+ }
+ PlacesUtils.tagging.tagURI(originalURI, originalTags);
+ }
+ };
- PlacesUtils.tagging.tagURI(oldURI, oldURITags);
- }
+ this.redo = async function() {
+ updatedInfo = await updateItem();
};
}
});
/**
* Transaction for setting annotations for an item.
*
* Required Input Properties: guid, annotationObject
*/
PT.Annotate = DefineTransaction(["guids", "annotations"]);
PT.Annotate.prototype = {
- async execute(aGuids, aNewAnnos) {
- let undoAnnosForItem = new Map(); // itemId => undoAnnos;
- for (let guid of aGuids) {
+ async execute({ guids, annotations }) {
+ let undoAnnosForItemId = new Map();
+ for (let guid of guids) {
let itemId = await PlacesUtils.promiseItemId(guid);
let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
let undoAnnos = [];
- for (let newAnno of aNewAnnos) {
+ for (let newAnno of annotations) {
let currentAnno = currentAnnos.find(a => a.name == newAnno.name);
if (currentAnno) {
undoAnnos.push(currentAnno);
} else {
// An unset value removes the annotation.
undoAnnos.push({ name: newAnno.name });
}
}
- undoAnnosForItem.set(itemId, undoAnnos);
+ undoAnnosForItemId.set(itemId, undoAnnos);
- PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+ PlacesUtils.setAnnotationsForItem(itemId, annotations);
}
this.undo = function() {
- for (let [itemId, undoAnnos] of undoAnnosForItem) {
+ for (let [itemId, undoAnnos] of undoAnnosForItemId) {
PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
}
};
this.redo = async function() {
- for (let guid of aGuids) {
+ for (let guid of guids) {
let itemId = await PlacesUtils.promiseItemId(guid);
- PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+ PlacesUtils.setAnnotationsForItem(itemId, annotations);
}
};
}
};
/**
* 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(aGuid, aKeyword, aPostData, aOldKeyword) {
+ async execute({ guid, keyword, postData, oldKeyword }) {
let url;
let oldKeywordEntry;
- if (aOldKeyword) {
- oldKeywordEntry = await PlacesUtils.keywords.fetch(aOldKeyword);
+ if (oldKeyword) {
+ oldKeywordEntry = await PlacesUtils.keywords.fetch(oldKeyword);
url = oldKeywordEntry.url;
- await PlacesUtils.keywords.remove(aOldKeyword);
+ await PlacesUtils.keywords.remove(oldKeyword);
}
- if (aKeyword) {
+ if (keyword) {
if (!url) {
- url = (await PlacesUtils.bookmarks.fetch(aGuid)).url;
+ url = (await PlacesUtils.bookmarks.fetch(guid)).url;
}
await PlacesUtils.keywords.insert({
url,
- keyword: aKeyword,
- postData: aPostData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
+ keyword,
+ postData: postData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
});
}
this.undo = async function() {
- if (aKeyword) {
- await PlacesUtils.keywords.remove(aKeyword);
+ if (keyword) {
+ await PlacesUtils.keywords.remove(keyword);
}
if (oldKeywordEntry) {
await PlacesUtils.keywords.insert(oldKeywordEntry);
}
};
}
});
/**
* Transaction for sorting a folder by name.
*
* Required Input Properties: guid.
*/
PT.SortByName = DefineTransaction(["guid"]);
PT.SortByName.prototype = {
- async execute(aGuid) {
- let itemId = await PlacesUtils.promiseItemId(aGuid),
- oldOrder = [], // [itemId] = old index
- contents = PlacesUtils.getFolderContents(itemId, false, false).root,
- count = contents.childCount;
-
- // Sort between separators.
- let newOrder = [], // nodes, in the new order.
- preSep = []; // Temporary array for sorting each group of nodes.
- let sortingMethod = (a, b) => {
- if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
+ async execute({ guid }) {
+ let sortingMethod = (node_a, node_b) => {
+ if (PlacesUtils.nodeIsContainer(node_a) && !PlacesUtils.nodeIsContainer(node_b))
return -1;
- if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
+ if (!PlacesUtils.nodeIsContainer(node_a) && PlacesUtils.nodeIsContainer(node_b))
return 1;
- return a.title.localeCompare(b.title);
+ return node_a.title.localeCompare(node_b.title);
};
+ let oldOrderGuids = [];
+ let newOrderGuids = [];
+ let preSepNodes = [];
- for (let i = 0; i < count; ++i) {
- let node = contents.getChild(i);
- oldOrder[node.itemId] = i;
+ // 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 folderId = await PlacesUtils.promiseItemId(guid);
+ let root = PlacesUtils.getFolderContents(folderId, 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 (preSep.length > 0) {
- preSep.sort(sortingMethod);
- newOrder = newOrder.concat(preSep);
- preSep.splice(0, preSep.length);
+ if (preSepNodes.length > 0) {
+ preSepNodes.sort(sortingMethod);
+ newOrderGuids.push(...preSepNodes.map(n => n.bookmarkGuid));
+ preSepNodes = [];
}
- newOrder.push(node);
- } else
- preSep.push(node);
+ newOrderGuids.push(node.bookmarkGuid);
+ } else {
+ preSepNodes.push(node);
+ }
}
- contents.containerOpen = false;
-
- if (preSep.length > 0) {
- preSep.sort(sortingMethod);
- newOrder = newOrder.concat(preSep);
+ root.containerOpen = false;
+ if (preSepNodes.length > 0) {
+ preSepNodes.sort(sortingMethod);
+ newOrderGuids.push(...preSepNodes.map(n => n.bookmarkGuid));
}
+ await PlacesUtils.bookmarks.reorder(guid, newOrderGuids);
- // Set the nex indexes.
- let callback = {
- runBatched() {
- for (let i = 0; i < newOrder.length; ++i) {
- PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
- }
- }
+ this.undo = async function() {
+ await PlacesUtils.bookmarks.reorder(guid, oldOrderGuids);
};
- PlacesUtils.bookmarks.runInBatchMode(callback, null);
-
- this.undo = () => {
- let callback = {
- runBatched() {
- for (let item in oldOrder) {
- PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]);
- }
- }
- };
- PlacesUtils.bookmarks.runInBatchMode(callback, null);
+ this.redo = async function() {
+ await PlacesUtils.bookmarks.reorder(guid, newOrderGuids);
};
}
};
/**
* Transaction for removing an item (any type).
*
* Required Input Properties: guids.
*/
PT.Remove = DefineTransaction(["guids"]);
PT.Remove.prototype = {
- async execute(aGuids) {
- function promiseBookmarksTree(guid) {
+ async execute({ guids }) {
+ let promiseBookmarksTree = async function(guid) {
+ let tree;
try {
- return PlacesUtils.promiseBookmarksTree(guid);
+ tree = await PlacesUtils.promiseBookmarksTree(guid);
} catch (ex) {
throw new Error("Failed to get info for the specified item (guid: " +
- guid + "). Ex: " + ex);
+ guid + "): " + ex);
}
+ return tree;
+ };
+ let removedItems = [];
+ for (let guid of guids) {
+ removedItems.push(await promiseBookmarksTree(guid));
}
-
- let toRestore = [];
- for (let guid of aGuids) {
- toRestore.push(await promiseBookmarksTree(guid));
- }
-
let removeThem = async function() {
- for (let guid of aGuids) {
- PlacesUtils.bookmarks.removeItem(await PlacesUtils.promiseItemId(guid));
+ for (let info of removedItems) {
+ if (info.annos &&
+ info.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
+ await PlacesUtils.livemarks.removeLivemark({ guid: info.guid });
+ } else {
+ await PlacesUtils.bookmarks.remove({ guid: info.guid });
+ }
}
};
await removeThem();
this.undo = async function() {
- for (let info of toRestore) {
+ for (let info of removedItems) {
await createItemsFromBookmarksTree(info, true);
}
};
this.redo = removeThem;
}
};
/**
* Transactions for removing all bookmarks for one or more urls.
*
* Required Input Properties: urls.
*/
PT.RemoveBookmarksForUrls = DefineTransaction(["urls"]);
PT.RemoveBookmarksForUrls.prototype = {
- async execute(aUrls) {
+ async execute({ urls }) {
let guids = [];
- for (let url of aUrls) {
- await PlacesUtils.bookmarks.fetch({ url }, info => {
- guids.push(info.guid);
- });
+ for (let url of urls) {
+ await PlacesUtils.bookmarks.fetch({ url }, b => guids.push(b.guid));
}
let removeTxn = TransactionsHistory.getRawTransaction(PT.Remove(guids));
await removeTxn.execute();
this.undo = removeTxn.undo.bind(removeTxn);
this.redo = removeTxn.redo.bind(removeTxn);
}
};
/**
* Transaction for tagging urls.
*
* Required Input Properties: urls, tags.
*/
PT.Tag = DefineTransaction(["urls", "tags"]);
PT.Tag.prototype = {
- async execute(aURIs, aTags) {
+ async execute({ urls, tags }) {
let onUndo = [], onRedo = [];
- for (let uri of aURIs) {
- if (!(await PlacesUtils.bookmarks.fetch({ url: uri }))) {
+ for (let url of urls) {
+ if (!(await PlacesUtils.bookmarks.fetch({ url }))) {
// Tagging is only allowed for bookmarked URIs (but see 424160).
let createTxn = TransactionsHistory.getRawTransaction(
- PT.NewBookmark({ url: uri,
- tags: aTags,
- parentGuid: PlacesUtils.bookmarks.unfiledGuid }));
+ 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 = Services.io.newURI(url.href);
let currentTags = PlacesUtils.tagging.getTagsForURI(uri);
- let newTags = aTags.filter(t => !currentTags.includes(t));
+ let newTags = tags.filter(t => !currentTags.includes(t));
PlacesUtils.tagging.tagURI(uri, newTags);
onUndo.unshift(() => {
PlacesUtils.tagging.untagURI(uri, newTags);
});
onRedo.push(() => {
PlacesUtils.tagging.tagURI(uri, newTags);
});
}
@@ -1540,25 +1540,27 @@ PT.Tag.prototype = {
*
* 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(aURIs, aTags) {
+ async execute({ urls, tags }) {
let onUndo = [], onRedo = [];
- for (let uri of aURIs) {
+ for (let url of urls) {
+ let uri = Services.io.newURI(url.href);
let tagsToRemove;
let tagsSet = PlacesUtils.tagging.getTagsForURI(uri);
- if (aTags.length > 0)
- tagsToRemove = aTags.filter(t => tagsSet.includes(t));
- else
+ if (tags.length > 0) {
+ tagsToRemove = tags.filter(t => tagsSet.includes(t));
+ } else {
tagsToRemove = tagsSet;
+ }
PlacesUtils.tagging.untagURI(uri, tagsToRemove);
onUndo.unshift(() => {
PlacesUtils.tagging.tagURI(uri, tagsToRemove);
});
onRedo.push(() => {
PlacesUtils.tagging.untagURI(uri, tagsToRemove);
});
}
@@ -1579,37 +1581,35 @@ PT.Untag.prototype = {
* Transaction for copying an item.
*
* Required Input Properties: guid, newParentGuid
* Optional Input Properties: newIndex, excludingAnnotations.
*/
PT.Copy = DefineTransaction(["guid", "newParentGuid"],
["newIndex", "excludingAnnotations"]);
PT.Copy.prototype = {
- async execute(aGuid, aNewParentGuid, aNewIndex, aExcludingAnnotations) {
+ async execute({ guid, newParentGuid, newIndex, excludingAnnotations }) {
let creationInfo = null;
try {
- creationInfo = await PlacesUtils.promiseBookmarksTree(aGuid);
+ creationInfo = await PlacesUtils.promiseBookmarksTree(guid);
} catch (ex) {
throw new Error("Failed to get info for the specified item (guid: " +
- aGuid + "). Ex: " + ex);
+ guid + "). Ex: " + ex);
}
- creationInfo.parentGuid = aNewParentGuid;
- creationInfo.index = aNewIndex;
+ creationInfo.parentGuid = newParentGuid;
+ creationInfo.index = newIndex;
- let newItemId =
- await createItemsFromBookmarksTree(creationInfo, false,
- aExcludingAnnotations);
+ let newItemGuid = await createItemsFromBookmarksTree(creationInfo, false,
+ excludingAnnotations);
let newItemInfo = null;
this.undo = async function() {
if (!newItemInfo) {
- let newItemGuid = await PlacesUtils.promiseItemGuid(newItemId);
newItemInfo = await PlacesUtils.promiseBookmarksTree(newItemGuid);
}
- PlacesUtils.bookmarks.removeItem(newItemId);
+ await PlacesUtils.bookmarks.remove(newItemGuid);
};
this.redo = async function() {
- newItemId = await createItemsFromBookmarksTree(newItemInfo, true);
+ await createItemsFromBookmarksTree(newItemInfo, true);
}
- return await PlacesUtils.promiseItemGuid(newItemId);
+ return newItemGuid;
}
};
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -258,23 +258,26 @@ async function ensureLivemarkCreatedByAd
// This throws otherwise.
await PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid });
}
// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the
// same.
// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set.
async function ensureEqualBookmarksTrees(aOriginal,
- aNew,
- aIsRestoredItem = true,
- aCheckParentAndPosition = false) {
+ aNew,
+ aIsRestoredItem = true,
+ aCheckParentAndPosition = false) {
// Note "id" is not-enumerable, and is therefore skipped by Object.keys (both
// ours and the one at deepEqual). This is fine for us because ids are not
// restored by Redo.
if (aIsRestoredItem) {
+ // Ignore lastModified for newly created items, for performance reasons.
+ if (!aOriginal.lastModified)
+ aNew.lastModified = aOriginal.lastModified;
Assert.deepEqual(aOriginal, aNew);
if (isLivemarkTree(aNew))
await ensureLivemarkCreatedByAddLivemark(aNew.guid);
return;
}
for (let property of Object.keys(aOriginal)) {
if (property == "children") {
@@ -368,22 +371,26 @@ add_task(async function test_recycled_tr
add_task(async function test_new_folder_with_annotation() {
const ANNO = { name: "TestAnno", value: "TestValue" };
let folder_info = createTestFolderInfo();
folder_info.index = bmStartIndex;
folder_info.annotations = [ANNO];
ensureUndoState();
let txn = PT.NewFolder(folder_info);
folder_info.guid = await txn.transact();
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(folder_info.guid);
let ensureDo = async function(aRedo = false) {
ensureUndoState([[txn]], 0);
await ensureItemsAdded(folder_info);
ensureAnnotationsSet(folder_info.guid, [ANNO]);
- if (aRedo)
- ensureTimestampsUpdated(folder_info.guid, true);
+ if (aRedo) {
+ // Ignore lastModified in the comparison, for performance reasons.
+ originalInfo.lastModified = null;
+ await ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ }
observer.reset();
};
let ensureUndo = () => {
ensureUndoState([[txn]], 1);
ensureItemsRemoved({ guid: folder_info.guid,
parentGuid: folder_info.parentGuid,
index: bmStartIndex });
@@ -406,21 +413,22 @@ add_task(async function test_new_bookmar
url: NetUtil.newURI("http://test_create_item.com"),
index: bmStartIndex,
title: "Test creating an item" };
ensureUndoState();
let txn = PT.NewBookmark(bm_info);
bm_info.guid = await txn.transact();
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(bm_info.guid);
let ensureDo = async function(aRedo = false) {
ensureUndoState([[txn]], 0);
await ensureItemsAdded(bm_info);
if (aRedo)
- ensureTimestampsUpdated(bm_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(originalInfo);
observer.reset();
};
let ensureUndo = () => {
ensureUndoState([[txn]], 1);
ensureItemsRemoved({ guid: bm_info.guid,
parentGuid: bm_info.parentGuid,
index: bmStartIndex });
observer.reset();
@@ -594,64 +602,69 @@ add_task(async function test_remove_fold
let folder_level_1_txn = PT.NewFolder(folder_level_1_info);
folder_level_1_info.guid = await folder_level_1_txn.transact();
folder_level_2_info.parentGuid = folder_level_1_info.guid;
let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
folder_level_2_info.guid = await folder_level_2_txn.transact();
return [folder_level_1_txn, folder_level_2_txn];
});
+ let original_folder_level_1_tree =
+ await PlacesUtils.promiseBookmarksTree(folder_level_1_info.guid);
+ let original_folder_level_2_tree =
+ Object.assign({ parentGuid: original_folder_level_1_tree.guid },
+ original_folder_level_1_tree.children[0]);
+
ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]);
await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
observer.reset();
let remove_folder_2_txn = PT.Remove(folder_level_2_info);
await remove_folder_2_txn.transact();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ]);
await ensureItemsRemoved(folder_level_2_info);
// Undo Remove "Folder Level 2"
await PT.undo();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
await ensureItemsAdded(folder_level_2_info);
- ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
observer.reset();
// Redo Remove "Folder Level 2"
await PT.redo();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ]);
await ensureItemsRemoved(folder_level_2_info);
observer.reset();
// Undo it again
await PT.undo();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
await ensureItemsAdded(folder_level_2_info);
- ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
observer.reset();
// Undo the creation of both folders
await PT.undo();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ], 2);
await ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
observer.reset();
// Redo the creation of both folders
await PT.redo();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
- ensureTimestampsUpdated(folder_level_1_info.guid, true);
- ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_1_tree);
observer.reset();
// Redo Remove "Folder Level 2"
await PT.redo();
ensureUndoState([ [remove_folder_2_txn],
[folder_level_2_txn_result, folder_level_1_txn_result] ]);
await ensureItemsRemoved(folder_level_2_info);
observer.reset();
@@ -672,103 +685,91 @@ add_task(async function test_remove_fold
await PT.clearTransactionsHistory();
ensureUndoState();
});
add_task(async function test_add_and_remove_bookmarks_with_additional_info() {
const testURI = NetUtil.newURI("http://add.remove.tag");
const TAG_1 = "TestTag1";
const TAG_2 = "TestTag2";
- const KEYWORD = "test_keyword";
- const POST_DATA = "post_data";
const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
let folder_info = createTestFolderInfo();
folder_info.guid = await PT.NewFolder(folder_info).transact();
let ensureTags = ensureTagsForURI.bind(null, testURI);
// Check that the NewBookmark transaction preserves tags.
observer.reset();
let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
b1_info.guid = await PT.NewBookmark(b1_info).transact();
+ let b1_originalInfo = await PlacesUtils.promiseBookmarksTree(b1_info.guid);
ensureTags([TAG_1]);
await PT.undo();
ensureTags([]);
observer.reset();
await PT.redo();
- ensureTimestampsUpdated(b1_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
ensureTags([TAG_1]);
// Check if the Remove transaction removes and restores tags of children
// correctly.
await PT.Remove(folder_info.guid).transact();
ensureTags([]);
observer.reset();
await PT.undo();
- ensureTimestampsUpdated(b1_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
ensureTags([TAG_1]);
await PT.redo();
ensureTags([]);
observer.reset();
await PT.undo();
- ensureTimestampsUpdated(b1_info.guid, true);
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
ensureTags([TAG_1]);
// * Check that no-op tagging (the uri is already tagged with TAG_1) is
// also a no-op on undo.
- // * Test the "keyword" property of the NewBookmark transaction.
observer.reset();
let b2_info = { parentGuid: folder_info.guid,
- url: testURI, tags: [TAG_1, TAG_2],
- keyword: KEYWORD,
- postData: POST_DATA,
+ url: testURI,
+ tags: [TAG_1, TAG_2],
annotations: [ANNO] };
b2_info.guid = await PT.NewBookmark(b2_info).transact();
let b2_post_creation_changes = [
{ guid: b2_info.guid,
isAnnoProperty: true,
property: ANNO.name,
- newValue: ANNO.value },
- { guid: b2_info.guid,
- property: "keyword",
- newValue: KEYWORD } ];
+ newValue: ANNO.value } ];
ensureItemsChanged(...b2_post_creation_changes);
ensureTags([TAG_1, TAG_2]);
observer.reset();
await PT.undo();
await ensureItemsRemoved(b2_info);
ensureTags([TAG_1]);
- // Check if Remove correctly restores keywords, tags and annotations.
- // Since both bookmarks share the same uri, they also share the keyword that
- // is not removed along with one of the bookmarks.
+ // Check if Remove correctly restores tags and annotations.
observer.reset();
await PT.redo();
ensureItemsChanged({ guid: b2_info.guid,
isAnnoProperty: true,
property: ANNO.name,
newValue: ANNO.value });
ensureTags([TAG_1, TAG_2]);
// Test Remove for multiple items.
observer.reset();
await PT.Remove(b1_info.guid).transact();
await PT.Remove(b2_info.guid).transact();
await PT.Remove(folder_info.guid).transact();
await ensureItemsRemoved(b1_info, b2_info, folder_info);
ensureTags([]);
- // There is no keyword removal notification cause all bookmarks are removed
- // before the keyword itself, so there's no one to notify.
- let entry = await PlacesUtils.keywords.fetch(KEYWORD);
- Assert.equal(entry, null, "keyword has been removed");
observer.reset();
await PT.undo();
await ensureItemsAdded(folder_info);
ensureTags([]);
observer.reset();
await PT.undo();
@@ -875,16 +876,17 @@ add_task(async function test_add_and_rem
let guid = await createLivemarkTxn.transact();
let originalInfo = await PlacesUtils.promiseBookmarksTree(guid);
Assert.ok(originalInfo);
await ensureLivemarkCreatedByAddLivemark(guid);
let removeTxn = PT.Remove(guid);
await removeTxn.transact();
await ensureNonExistent(guid);
+
async function undo() {
ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
await PT.undo();
ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
await ensureBookmarksTreeRestoredCorrectly(originalInfo);
await PT.undo();
ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
await ensureNonExistent(guid);