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