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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7/**
8 * This module provides an asynchronous API for managing bookmarks.
9 *
10 * Bookmarks are organized in a tree structure, and include URLs, folders and
11 * separators.  Multiple bookmarks for the same URL are allowed.
12 *
13 * Note that if you are handling bookmarks operations in the UI, you should
14 * not use this API directly, but rather use PlacesTransactions.jsm, so that
15 * any operation is undo/redo-able.
16 *
17 * Each bookmark-item is represented by an object having the following
18 * properties:
19 *
20 *  - guid (string)
21 *      The globally unique identifier of the item.
22 *  - parentGuid (string)
23 *      The globally unique identifier of the folder containing the item.
24 *      This will be an empty string for the Places root folder.
25 *  - index (number)
26 *      The 0-based position of the item in the parent folder.
27 *  - dateAdded (Date)
28 *      The time at which the item was added.
29 *  - lastModified (Date)
30 *      The time at which the item was last modified.
31 *  - type (number)
32 *      The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
33 *
34 *  The following properties are only valid for URLs or folders.
35 *
36 *  - title (string)
37 *      The item's title, if any.  Empty titles and null titles are considered
38 *      the same. Titles longer than DB_TITLE_LENGTH_MAX will be truncated.
39 *
40 *  The following properties are only valid for URLs:
41 *
42 *  - url (URL, href or nsIURI)
43 *      The item's URL.  Note that while input objects can contains either
44 *      an URL object, an href string, or an nsIURI, output objects will always
45 *      contain an URL object.
46 *      An URL cannot be longer than DB_URL_LENGTH_MAX, methods will throw if a
47 *      longer value is provided.
48 *
49 * Each successful operation notifies through the nsINavBookmarksObserver
50 * interface.  To listen to such notifications you must register using
51 * nsINavBookmarksService addObserver and removeObserver methods.
52 * Note that bookmark addition or order changes won't notify onItemMoved for
53 * items that have their indexes changed.
54 * Similarly, lastModified changes not done explicitly (like changing another
55 * property) won't fire an onItemChanged notification for the lastModified
56 * property.
57 * @see nsINavBookmarkObserver
58 */
59
60var EXPORTED_SYMBOLS = [ "Bookmarks" ];
61
62Cu.importGlobalProperties(["URL"]);
63
64ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
65ChromeUtils.defineModuleGetter(this, "Services",
66                               "resource://gre/modules/Services.jsm");
67ChromeUtils.defineModuleGetter(this, "NetUtil",
68                               "resource://gre/modules/NetUtil.jsm");
69ChromeUtils.defineModuleGetter(this, "Sqlite",
70                               "resource://gre/modules/Sqlite.jsm");
71ChromeUtils.defineModuleGetter(this, "PlacesUtils",
72                               "resource://gre/modules/PlacesUtils.jsm");
73ChromeUtils.defineModuleGetter(this, "PlacesSyncUtils",
74                               "resource://gre/modules/PlacesSyncUtils.jsm");
75
76// This is an helper to temporarily cover the need to know the tags folder
77// itemId until bug 424160 is fixed.  This exists so that startup paths won't
78// pay the price to initialize the bookmarks service just to fetch this value.
79// If the method is already initing the bookmarks service for other reasons
80// (most of the writing methods will invoke getObservers() already) it can
81// directly use the PlacesUtils.tagsFolderId property.
82var gTagsFolderId;
83async function promiseTagsFolderId() {
84  if (gTagsFolderId)
85    return gTagsFolderId;
86  let db =  await PlacesUtils.promiseDBConnection();
87  let rows = await db.execute(
88    "SELECT id FROM moz_bookmarks WHERE guid = :guid",
89    { guid: Bookmarks.tagsGuid }
90  );
91  return gTagsFolderId = rows[0].getResultByName("id");
92}
93
94const MATCH_ANYWHERE_UNMODIFIED = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
95const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
96const SQLITE_MAX_VARIABLE_NUMBER = 999;
97
98var Bookmarks = Object.freeze({
99  /**
100   * Item's type constants.
101   * These should stay consistent with nsINavBookmarksService.idl
102   */
103  TYPE_BOOKMARK: 1,
104  TYPE_FOLDER: 2,
105  TYPE_SEPARATOR: 3,
106
107  /**
108   * Sync status constants, stored for each item.
109   */
110  SYNC_STATUS: {
111    UNKNOWN: Ci.nsINavBookmarksService.SYNC_STATUS_UNKNOWN,
112    NEW: Ci.nsINavBookmarksService.SYNC_STATUS_NEW,
113    NORMAL: Ci.nsINavBookmarksService.SYNC_STATUS_NORMAL,
114  },
115
116  /**
117   * Default index used to append a bookmark-item at the end of a folder.
118   * This should stay consistent with nsINavBookmarksService.idl
119   */
120  DEFAULT_INDEX: -1,
121
122  /**
123   * Bookmark change source constants, passed as optional properties and
124   * forwarded to observers. See nsINavBookmarksService.idl for an explanation.
125   */
126  SOURCES: {
127    DEFAULT: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
128    SYNC: Ci.nsINavBookmarksService.SOURCE_SYNC,
129    IMPORT: Ci.nsINavBookmarksService.SOURCE_IMPORT,
130    IMPORT_REPLACE: Ci.nsINavBookmarksService.SOURCE_IMPORT_REPLACE,
131    SYNC_REPARENT_REMOVED_FOLDER_CHILDREN: Ci.nsINavBookmarksService.SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
132  },
133
134  /**
135   * Special GUIDs associated with bookmark roots.
136   * It's guaranteed that the roots will always have these guids.
137   */
138  rootGuid:    "root________",
139  menuGuid:    "menu________",
140  toolbarGuid: "toolbar_____",
141  unfiledGuid: "unfiled_____",
142  mobileGuid:  "mobile______",
143
144  // With bug 424160, tags will stop being bookmarks, thus this root will
145  // be removed.  Do not rely on this, rather use the tagging service API.
146  tagsGuid:    "tags________",
147
148  /**
149   * The GUIDs of the user content root folders that we support, for easy access
150   * as a set.
151   */
152  userContentRoots: ["toolbar_____", "menu________", "unfiled_____", "mobile______"],
153
154  /**
155   * GUIDs associated with virtual queries that are used for display in the left
156   * pane.
157   */
158  virtualMenuGuid: "menu_______v",
159  virtualToolbarGuid: "toolbar____v",
160  virtualUnfiledGuid: "unfiled___v",
161  virtualMobileGuid: "mobile____v",
162
163  /**
164   * Checks if a guid is a virtual root.
165   *
166   * @param {String} guid The guid of the item to look for.
167   * @returns {Boolean} true if guid is a virtual root, false otherwise.
168   */
169  isVirtualRootItem(guid) {
170    return guid == PlacesUtils.bookmarks.virtualMenuGuid ||
171           guid == PlacesUtils.bookmarks.virtualToolbarGuid ||
172           guid == PlacesUtils.bookmarks.virtualUnfiledGuid ||
173           guid == PlacesUtils.bookmarks.virtualMobileGuid;
174  },
175
176  /**
177   * Returns the title to use on the UI for a bookmark item. Root folders
178   * in the database don't store fully localised versions of the title. To
179   * get those this function should be called.
180   *
181   * Hence, this function should only be called if a root folder object is
182   * likely to be displayed to the user.
183   *
184   * @param {Object} info An object representing a bookmark-item.
185   * @returns {String} The correct string.
186   * @throws {Error} If the guid in PlacesUtils.bookmarks.userContentRoots is
187   *                 not supported.
188   */
189  getLocalizedTitle(info) {
190    if (!PlacesUtils.bookmarks.userContentRoots.includes(info.guid)) {
191      return info.title;
192    }
193
194    switch (info.guid) {
195      case PlacesUtils.bookmarks.toolbarGuid:
196        return PlacesUtils.getString("BookmarksToolbarFolderTitle");
197      case PlacesUtils.bookmarks.menuGuid:
198        return PlacesUtils.getString("BookmarksMenuFolderTitle");
199      case PlacesUtils.bookmarks.unfiledGuid:
200        return PlacesUtils.getString("OtherBookmarksFolderTitle");
201      case PlacesUtils.bookmarks.mobileGuid:
202        return PlacesUtils.getString("MobileBookmarksFolderTitle");
203      default:
204        throw new Error(`Unsupported guid ${info.guid} passed to getLocalizedTitle!`);
205    }
206  },
207
208  /**
209   * Inserts a bookmark-item into the bookmarks tree.
210   *
211   * For creating a bookmark, the following set of properties is required:
212   *  - type
213   *  - parentGuid
214   *  - url, only for bookmarked URLs
215   *
216   * If an index is not specified, it defaults to appending.
217   * It's also possible to pass a non-existent GUID to force creation of an
218   * item with the given GUID, but unless you have a very sound reason, such as
219   * an undo manager implementation or synchronization, don't do that.
220   *
221   * Note that any known properties that don't apply to the specific item type
222   * cause an exception.
223   *
224   * @param info
225   *        object representing a bookmark-item.
226   *
227   * @return {Promise} resolved when the creation is complete.
228   * @resolves to an object representing the created bookmark.
229   * @rejects if it's not possible to create the requested bookmark.
230   * @throws if the arguments are invalid.
231   */
232  insert(info) {
233    let now = new Date();
234    let addedTime = (info && info.dateAdded) || now;
235    let modTime = addedTime;
236    if (addedTime > now) {
237      modTime = now;
238    }
239    let insertInfo = validateBookmarkObject("Bookmarks.jsm: insert", info,
240      { type: { defaultValue: this.TYPE_BOOKMARK },
241        index: { defaultValue: this.DEFAULT_INDEX },
242        url: { requiredIf: b => b.type == this.TYPE_BOOKMARK,
243               validIf: b => b.type == this.TYPE_BOOKMARK },
244        parentGuid: { required: true },
245        title: { defaultValue: "",
246                 validIf: b => b.type == this.TYPE_BOOKMARK ||
247                               b.type == this.TYPE_FOLDER ||
248                               b.title === "" },
249        dateAdded: { defaultValue: addedTime },
250        lastModified: { defaultValue: modTime,
251                        validIf: b => b.lastModified >= now || (b.dateAdded && b.lastModified >= b.dateAdded) },
252        source: { defaultValue: this.SOURCES.DEFAULT }
253      });
254
255    return (async () => {
256      // Ensure the parent exists.
257      let parent = await fetchBookmark({ guid: insertInfo.parentGuid });
258      if (!parent)
259        throw new Error("parentGuid must be valid");
260
261      // Set index in the appending case.
262      if (insertInfo.index == this.DEFAULT_INDEX ||
263          insertInfo.index > parent._childCount) {
264        insertInfo.index = parent._childCount;
265      }
266
267      let item = await insertBookmark(insertInfo, parent);
268
269      // Notify onItemAdded to listeners.
270      let observers = PlacesUtils.bookmarks.getObservers();
271      // We need the itemId to notify, though once the switch to guids is
272      // complete we may stop using it.
273      let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
274      let itemId = await PlacesUtils.promiseItemId(item.guid);
275
276      // Pass tagging information for the observers to skip over these notifications when needed.
277      let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
278      let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
279      notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
280                                         item.type, uri, item.title,
281                                         PlacesUtils.toPRTime(item.dateAdded), item.guid,
282                                         item.parentGuid, item.source ],
283                                       { isTagging: isTagging || isTagsFolder });
284
285      // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
286      if (isTagging) {
287        for (let entry of (await fetchBookmarksByURL(item, true))) {
288          notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
289                                               PlacesUtils.toPRTime(entry.lastModified),
290                                               entry.type, entry._parentId,
291                                               entry.guid, entry.parentGuid,
292                                               "", item.source ]);
293        }
294      }
295
296      // Remove non-enumerable properties.
297      delete item.source;
298      return Object.assign({}, item);
299    })();
300  },
301
302
303  /**
304   * Inserts a bookmark-tree into the existing bookmarks tree.
305   *
306   * All the specified folders and bookmarks will be inserted as new, even
307   * if duplicates. There's no merge support at this time.
308   *
309   * The input should be of the form:
310   * {
311   *   guid: "<some-existing-guid-to-use-as-parent>",
312   *   source: "<some valid source>", (optional)
313   *   children: [
314   *     ... valid bookmark objects.
315   *   ]
316   * }
317   *
318   * Children will be appended to any existing children of the parent
319   * that is specified. The source specified on the root of the tree
320   * will be used for all the items inserted. Any indices or custom parentGuids
321   * set on children will be ignored and overwritten.
322   *
323   * @param {Object} tree
324   *        object representing a tree of bookmark items to insert.
325   * @param {Object} options [optional]
326   *        object with properties representing options.  Current options are:
327   *         - fixupOrSkipInvalidEntries: makes the insert more lenient to
328   *           mistakes in the input tree.  Properties of an entry that are
329   *           fixable will be corrected, otherwise the entry will be skipped.
330   *           This is particularly convenient for import/restore operations,
331   *           but should not be abused for common inserts, since it may hide
332   *           bugs in the calling code.
333   *
334   * @return {Promise} resolved when the creation is complete.
335   * @resolves to an object representing the created bookmark.
336   * @rejects if it's not possible to create the requested bookmark.
337   * @throws if the arguments are invalid.
338   */
339  insertTree(tree, options) {
340    if (!tree || typeof tree != "object") {
341      throw new Error("Should be provided a valid tree object.");
342    }
343    if (!Array.isArray(tree.children) || !tree.children.length) {
344      throw new Error("Should have a non-zero number of children to insert.");
345    }
346    if (!PlacesUtils.isValidGuid(tree.guid)) {
347      throw new Error(`The parent guid is not valid (${tree.guid} ${tree.title}).`);
348    }
349    if (tree.guid == this.rootGuid) {
350      throw new Error("Can't insert into the root.");
351    }
352    if (tree.guid == this.tagsGuid) {
353      throw new Error("Can't use insertTree to insert tags.");
354    }
355    if (tree.hasOwnProperty("source") &&
356        !Object.values(this.SOURCES).includes(tree.source)) {
357      throw new Error("Can't use source value " + tree.source);
358    }
359    if (options && typeof options != "object") {
360      throw new Error("Options should be a valid object");
361    }
362    let fixupOrSkipInvalidEntries = options && !!options.fixupOrSkipInvalidEntries;
363
364    // Serialize the tree into an array of items to insert into the db.
365    let insertInfos = [];
366    let insertLivemarkInfos = [];
367    let urlsThatMightNeedPlaces = [];
368
369    // We want to use the same 'last added' time for all the entries
370    // we import (so they won't differ by a few ms based on where
371    // they are in the tree, and so we don't needlessly construct
372    // multiple dates).
373    let fallbackLastAdded = new Date();
374
375    const {TYPE_BOOKMARK, TYPE_FOLDER, SOURCES} = this;
376
377    // Reuse the 'source' property for all the entries.
378    let source = tree.source || SOURCES.DEFAULT;
379
380    // This is recursive.
381    function appendInsertionInfoForInfoArray(infos, indexToUse, parentGuid) {
382      // We want to keep the index of items that will be inserted into the root
383      // NULL, and then use a subquery to select the right index, to avoid
384      // races where other consumers might add items between when we determine
385      // the index and when we insert. However, the validator does not allow
386      // NULL values in in the index, so we fake it while validating and then
387      // correct later. Keep track of whether we're doing this:
388      let shouldUseNullIndices = false;
389      if (indexToUse === null) {
390        shouldUseNullIndices = true;
391        indexToUse = 0;
392      }
393
394      // When a folder gets an item added, its last modified date is updated
395      // to be equal to the date we added the item (if that date is newer).
396      // Because we're inserting a tree, we keep track of this date for the
397      // loop, updating it for inserted items as well as from any subfolders
398      // we insert.
399      let lastAddedForParent = new Date(0);
400      for (let info of infos) {
401        // Ensure to use the same date for dateAdded and lastModified, even if
402        // dateAdded may be imposed by the caller.
403        let time = (info && info.dateAdded) || fallbackLastAdded;
404        let insertInfo = {
405          guid: { defaultValue: PlacesUtils.history.makeGuid() },
406          type: { defaultValue: TYPE_BOOKMARK },
407          url: { requiredIf: b => b.type == TYPE_BOOKMARK,
408                 validIf: b => b.type == TYPE_BOOKMARK },
409          parentGuid: { replaceWith: parentGuid }, // Set the correct parent guid.
410          title: { defaultValue: "",
411                   validIf: b => b.type == TYPE_BOOKMARK ||
412                                 b.type == TYPE_FOLDER ||
413                                 b.title === "" },
414          dateAdded: { defaultValue: time,
415                       validIf: b => !b.lastModified ||
416                                     b.dateAdded <= b.lastModified },
417          lastModified: { defaultValue: time,
418                          validIf: b => (!b.dateAdded && b.lastModified >= time) ||
419                                        (b.dateAdded && b.lastModified >= b.dateAdded) },
420          index: { replaceWith: indexToUse++ },
421          source: { replaceWith: source },
422          annos: {},
423          keyword: { validIf: b => b.type == TYPE_BOOKMARK },
424          charset: { validIf: b => b.type == TYPE_BOOKMARK },
425          postData: { validIf: b => b.type == TYPE_BOOKMARK },
426          tags: { validIf: b => b.type == TYPE_BOOKMARK },
427          children: { validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children) }
428        };
429        if (fixupOrSkipInvalidEntries) {
430          insertInfo.guid.fixup = b => b.guid = PlacesUtils.history.makeGuid();
431          insertInfo.dateAdded.fixup = insertInfo.lastModified.fixup =
432            b => b.lastModified = b.dateAdded = fallbackLastAdded;
433        }
434        try {
435          insertInfo = validateBookmarkObject("Bookmarks.jsm: insertTree", info, insertInfo);
436        } catch (ex) {
437          if (fixupOrSkipInvalidEntries) {
438            indexToUse--;
439            continue;
440          } else {
441            throw ex;
442          }
443        }
444
445        if (shouldUseNullIndices) {
446          insertInfo.index = null;
447        }
448        // Store the URL if this is a bookmark, so we can ensure we create an
449        // entry in moz_places for it.
450        if (insertInfo.type == Bookmarks.TYPE_BOOKMARK) {
451          urlsThatMightNeedPlaces.push(insertInfo.url);
452        }
453
454        // As we don't track indexes for children of root folders, and we
455        // insert livemarks separately, we create a temporary placeholder in
456        // the bookmarks, and later we'll replace it by the real livemark.
457        if (isLivemark(insertInfo)) {
458          // Make the current insertInfo item a placeholder.
459          let livemarkInfo = Object.assign({}, insertInfo);
460
461          // Delete the annotations that make it a livemark.
462          delete insertInfo.annos;
463
464          // Now save the livemark info for later.
465          insertLivemarkInfos.push(livemarkInfo);
466        }
467
468        insertInfos.push(insertInfo);
469        // Process any children. We have to use info.children here rather than
470        // insertInfo.children because validateBookmarkObject doesn't copy over
471        // the children ref, as the default bookmark validators object doesn't
472        // know about children.
473        if (info.children) {
474          // start children of this item off at index 0.
475          let childrenLastAdded = appendInsertionInfoForInfoArray(info.children, 0, insertInfo.guid);
476          if (childrenLastAdded > insertInfo.lastModified) {
477            insertInfo.lastModified = childrenLastAdded;
478          }
479          if (childrenLastAdded > lastAddedForParent) {
480            lastAddedForParent = childrenLastAdded;
481          }
482        }
483
484        // Ensure we track what time to update the parent to.
485        if (insertInfo.dateAdded > lastAddedForParent) {
486          lastAddedForParent = insertInfo.dateAdded;
487        }
488      }
489      return lastAddedForParent;
490    }
491
492    // We want to validate synchronously, but we can't know the index at which
493    // we're inserting into the parent. We just use NULL instead,
494    // and the SQL query with which we insert will update it as necessary.
495    let lastAddedForParent = appendInsertionInfoForInfoArray(tree.children, null, tree.guid);
496
497    return (async function() {
498      let treeParent = await fetchBookmark({ guid: tree.guid });
499      if (!treeParent) {
500        throw new Error("The parent you specified doesn't exist.");
501      }
502
503      if (treeParent._parentId == PlacesUtils.tagsFolderId) {
504        throw new Error("Can't use insertTree to insert tags.");
505      }
506
507      await insertBookmarkTree(insertInfos, source, treeParent,
508                               urlsThatMightNeedPlaces, lastAddedForParent);
509
510      for (let info of insertLivemarkInfos) {
511        try {
512          await insertLivemarkData(info);
513        } catch (ex) {
514          // This can arguably fail, if some of the livemarks data is invalid.
515          if (fixupOrSkipInvalidEntries) {
516            // The placeholder should have been removed at this point, thus we
517            // can avoid to notify about it.
518            let placeholderIndex = insertInfos.findIndex(item => item.guid == info.guid);
519            if (placeholderIndex != -1) {
520              insertInfos.splice(placeholderIndex, 1);
521            }
522          } else {
523            // Throw if we're not lenient to input mistakes.
524            throw ex;
525          }
526        }
527      }
528
529      // Now update the indices of root items in the objects we return.
530      // These may be wrong if someone else modified the table between
531      // when we fetched the parent and inserted our items, but the actual
532      // inserts will have been correct, and we don't want to query the DB
533      // again if we don't have to. bug 1347230 covers improving this.
534      let rootIndex = treeParent._childCount;
535      for (let insertInfo of insertInfos) {
536        if (insertInfo.parentGuid == tree.guid) {
537          insertInfo.index += rootIndex++;
538        }
539      }
540      // We need the itemIds to notify, though once the switch to guids is
541      // complete we may stop using them.
542      let itemIdMap = await PlacesUtils.promiseManyItemIds(insertInfos.map(info => info.guid));
543      // Notify onItemAdded to listeners.
544      let observers = PlacesUtils.bookmarks.getObservers();
545      for (let i = 0; i < insertInfos.length; i++) {
546        let item = insertInfos[i];
547        let itemId = itemIdMap.get(item.guid);
548        let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
549        // For sub-folders, we need to make sure their children have the correct parent ids.
550        let parentId;
551        if (item.parentGuid === treeParent.guid) {
552          // This is a direct child of the tree parent, so we can use the
553          // existing parent's id.
554          parentId = treeParent._id;
555        } else {
556          // This is a parent folder that's been updated, so we need to
557          // use the new item id.
558          parentId = itemIdMap.get(item.parentGuid);
559        }
560
561        notify(observers, "onItemAdded", [ itemId, parentId, item.index,
562                                           item.type, uri, item.title,
563                                           PlacesUtils.toPRTime(item.dateAdded), item.guid,
564                                           item.parentGuid, item.source ],
565                                         { isTagging: false });
566        // Note, annotations for livemark data are deleted from insertInfo
567        // within appendInsertionInfoForInfoArray, so we won't be duplicating
568        // the insertions here.
569        try {
570          await handleBookmarkItemSpecialData(itemId, item);
571        } catch (ex) {
572          // This is not critical, regardless the bookmark has been created
573          // and we should continue notifying the next ones.
574          Cu.reportError(`An error occured while handling special bookmark data: ${ex}`);
575        }
576
577        // Remove non-enumerable properties.
578        delete item.source;
579
580        insertInfos[i] = Object.assign({}, item);
581      }
582      return insertInfos;
583    })();
584  },
585
586  /**
587   * Updates a bookmark-item.
588   *
589   * Only set the properties which should be changed (undefined properties
590   * won't be taken into account).
591   * Moreover, the item's type or dateAdded cannot be changed, since they are
592   * immutable after creation.  Trying to change them will reject.
593   *
594   * Note that any known properties that don't apply to the specific item type
595   * cause an exception.
596   *
597   * @param info
598   *        object representing a bookmark-item, as defined above.
599   *
600   * @return {Promise} resolved when the update is complete.
601   * @resolves to an object representing the updated bookmark.
602   * @rejects if it's not possible to update the given bookmark.
603   * @throws if the arguments are invalid.
604   */
605  update(info) {
606    // The info object is first validated here to ensure it's consistent, then
607    // it's compared to the existing item to remove any properties that don't
608    // need to be updated.
609    let updateInfo = validateBookmarkObject("Bookmarks.jsm: update", info,
610      { guid: { required: true },
611        index: { requiredIf: b => b.hasOwnProperty("parentGuid"),
612                 validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX },
613        source: { defaultValue: this.SOURCES.DEFAULT }
614      });
615
616    // There should be at last one more property in addition to guid and source.
617    if (Object.keys(updateInfo).length < 3)
618      throw new Error("Not enough properties to update");
619
620    return (async () => {
621      // Ensure the item exists.
622      let item = await fetchBookmark(updateInfo);
623      if (!item)
624        throw new Error("No bookmarks found for the provided GUID");
625      if (updateInfo.hasOwnProperty("type") && updateInfo.type != item.type)
626        throw new Error("The bookmark type cannot be changed");
627
628      // Remove any property that will stay the same.
629      removeSameValueProperties(updateInfo, item);
630      // Check if anything should still be updated.
631      if (Object.keys(updateInfo).length < 3) {
632        // Remove non-enumerable properties.
633        return Object.assign({}, item);
634      }
635      const now = new Date();
636      let lastModifiedDefault = now;
637      // In the case where `dateAdded` is specified, but `lastModified` is not,
638      // we only update `lastModified` if it is older than the new `dateAdded`.
639      if (!("lastModified" in updateInfo) &&
640          "dateAdded" in updateInfo) {
641        lastModifiedDefault = new Date(Math.max(item.lastModified, updateInfo.dateAdded));
642      }
643      updateInfo = validateBookmarkObject("Bookmarks.jsm: update", updateInfo,
644        { url: { validIf: () => item.type == this.TYPE_BOOKMARK },
645          title: { validIf: () => [ this.TYPE_BOOKMARK,
646                                    this.TYPE_FOLDER ].includes(item.type) },
647          lastModified: { defaultValue: lastModifiedDefault,
648                          validIf: b => b.lastModified >= now ||
649                                        b.lastModified >= (b.dateAdded || item.dateAdded) },
650          dateAdded: { defaultValue: item.dateAdded }
651        });
652
653      return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: update",
654        async db => {
655        let parent;
656        if (updateInfo.hasOwnProperty("parentGuid")) {
657          if (PlacesUtils.isRootItem(item.guid)) {
658            throw new Error("It's not possible to move Places root folders.");
659          }
660          if (item.type == this.TYPE_FOLDER) {
661            // Make sure we are not moving a folder into itself or one of its
662            // descendants.
663            let rows = await db.executeCached(
664              `WITH RECURSIVE
665               descendants(did) AS (
666                 VALUES(:id)
667                 UNION ALL
668                 SELECT id FROM moz_bookmarks
669                 JOIN descendants ON parent = did
670                 WHERE type = :type
671               )
672               SELECT guid FROM moz_bookmarks
673               WHERE id IN descendants
674              `, { id: item._id, type: this.TYPE_FOLDER });
675            if (rows.map(r => r.getResultByName("guid")).includes(updateInfo.parentGuid))
676              throw new Error("Cannot insert a folder into itself or one of its descendants");
677          }
678
679          parent = await fetchBookmark({ guid: updateInfo.parentGuid });
680          if (!parent)
681            throw new Error("No bookmarks found for the provided parentGuid");
682        }
683
684        if (updateInfo.hasOwnProperty("index")) {
685          if (PlacesUtils.isRootItem(item.guid)) {
686            throw new Error("It's not possible to move Places root folders.");
687          }
688          // If at this point we don't have a parent yet, we are moving into
689          // the same container.  Thus we know it exists.
690          if (!parent)
691            parent = await fetchBookmark({ guid: item.parentGuid });
692
693          if (updateInfo.index >= parent._childCount ||
694              updateInfo.index == this.DEFAULT_INDEX) {
695             updateInfo.index = parent._childCount;
696
697            // Fix the index when moving within the same container.
698            if (parent.guid == item.parentGuid)
699               updateInfo.index--;
700          }
701        }
702
703        let updatedItem = await updateBookmark(updateInfo, item, parent);
704
705        if (item.type == this.TYPE_BOOKMARK &&
706            item.url.href != updatedItem.url.href) {
707          // ...though we don't wait for the calculation.
708          updateFrecency(db, [item.url]).catch(Cu.reportError);
709          updateFrecency(db, [updatedItem.url]).catch(Cu.reportError);
710        }
711
712        // Notify onItemChanged to listeners.
713        let observers = PlacesUtils.bookmarks.getObservers();
714        // For lastModified, we only care about the original input, since we
715        // should not notify implciit lastModified changes.
716        if (info.hasOwnProperty("lastModified") &&
717            updateInfo.hasOwnProperty("lastModified") &&
718            item.lastModified != updatedItem.lastModified) {
719          notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
720                                               false,
721                                               `${PlacesUtils.toPRTime(updatedItem.lastModified)}`,
722                                               PlacesUtils.toPRTime(updatedItem.lastModified),
723                                               updatedItem.type,
724                                               updatedItem._parentId,
725                                               updatedItem.guid,
726                                               updatedItem.parentGuid, "",
727                                               updatedItem.source ]);
728        }
729        if (info.hasOwnProperty("dateAdded") &&
730            updateInfo.hasOwnProperty("dateAdded") &&
731            item.dateAdded != updatedItem.dateAdded) {
732          notify(observers, "onItemChanged", [ updatedItem._id, "dateAdded",
733                                               false, `${PlacesUtils.toPRTime(updatedItem.dateAdded)}`,
734                                               PlacesUtils.toPRTime(updatedItem.lastModified),
735                                               updatedItem.type,
736                                               updatedItem._parentId,
737                                               updatedItem.guid,
738                                               updatedItem.parentGuid,
739                                               "",
740                                               updatedItem.source ]);
741        }
742        if (updateInfo.hasOwnProperty("title")) {
743          let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
744          notify(observers, "onItemChanged", [ updatedItem._id, "title",
745                                               false, updatedItem.title,
746                                               PlacesUtils.toPRTime(updatedItem.lastModified),
747                                               updatedItem.type,
748                                               updatedItem._parentId,
749                                               updatedItem.guid,
750                                               updatedItem.parentGuid, "",
751                                               updatedItem.source ],
752                                               { isTagging });
753          // If we're updating a tag, we must notify all the tagged bookmarks
754          // about the change.
755          if (isTagging) {
756            let URIs = PlacesUtils.tagging.getURIsForTag(updatedItem.title);
757            for (let uri of URIs) {
758              for (let entry of (await fetchBookmarksByURL({ url: new URL(uri.spec) }, true))) {
759                notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
760                                                     PlacesUtils.toPRTime(entry.lastModified),
761                                                     entry.type, entry._parentId,
762                                                     entry.guid, entry.parentGuid,
763                                                     "", updatedItem.source ]);
764              }
765            }
766          }
767        }
768        if (updateInfo.hasOwnProperty("url")) {
769          await PlacesUtils.keywords.reassign(item.url, updatedItem.url,
770                                              updatedItem.source);
771          notify(observers, "onItemChanged", [ updatedItem._id, "uri",
772                                               false, updatedItem.url.href,
773                                               PlacesUtils.toPRTime(updatedItem.lastModified),
774                                               updatedItem.type,
775                                               updatedItem._parentId,
776                                               updatedItem.guid,
777                                               updatedItem.parentGuid,
778                                               item.url.href,
779                                               updatedItem.source ]);
780        }
781        // If the item was moved, notify onItemMoved.
782        if (item.parentGuid != updatedItem.parentGuid ||
783            item.index != updatedItem.index) {
784          notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
785                                             item.index, updatedItem._parentId,
786                                             updatedItem.index, updatedItem.type,
787                                             updatedItem.guid, item.parentGuid,
788                                             updatedItem.parentGuid,
789                                             updatedItem.source ]);
790        }
791
792        // Remove non-enumerable properties.
793        delete updatedItem.source;
794        return Object.assign({}, updatedItem);
795      });
796    })();
797  },
798
799  /**
800   * Removes one or more bookmark-items.
801   *
802   * @param guidOrInfo This may be:
803   *        - The globally unique identifier of the item to remove
804   *        - an object representing the item, as defined above
805   *        - an array of objects representing the items to be removed
806   * @param {Object} [options={}]
807   *        Additional options that can be passed to the function.
808   *        Currently supports the following properties:
809   *         - preventRemovalOfNonEmptyFolders: Causes an exception to be
810   *           thrown when attempting to remove a folder that is not empty.
811   *         - source: The change source, forwarded to all bookmark observers.
812   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
813   *
814   * @return {Promise}
815   * @resolves when the removal is complete
816   * @rejects if the provided guid doesn't match any existing bookmark.
817   * @throws if the arguments are invalid.
818   */
819  remove(guidOrInfo, options = {}) {
820    let infos = guidOrInfo;
821    if (!infos)
822      throw new Error("Input should be a valid object");
823    if (!Array.isArray(guidOrInfo)) {
824      if (typeof(guidOrInfo) != "object") {
825        infos = [{ guid: guidOrInfo }];
826      } else {
827        infos = [guidOrInfo];
828      }
829    }
830
831    if (!("source" in options)) {
832      options.source = Bookmarks.SOURCES.DEFAULT;
833    }
834
835    let removeInfos = [];
836    for (let info of infos) {
837      // Disallow removing the root folders.
838      if ([
839        Bookmarks.rootGuid, Bookmarks.menuGuid, Bookmarks.toolbarGuid,
840        Bookmarks.unfiledGuid, Bookmarks.tagsGuid, Bookmarks.mobileGuid
841      ].includes(info.guid)) {
842        throw new Error("It's not possible to remove Places root folders.");
843      }
844
845      // Even if we ignore any other unneeded property, we still validate any
846      // known property to reduce likelihood of hidden bugs.
847      let removeInfo = validateBookmarkObject("Bookmarks.jsm: remove", info);
848      removeInfos.push(removeInfo);
849    }
850
851    return (async function() {
852      let removeItems = [];
853      for (let info of removeInfos) {
854        let item = await fetchBookmark(info);
855        if (!item)
856          throw new Error("No bookmarks found for the provided GUID.");
857
858        removeItems.push(item);
859      }
860
861      await removeBookmarks(removeItems, options);
862
863      // Notify onItemRemoved to listeners.
864      for (let item of removeItems) {
865        let observers = PlacesUtils.bookmarks.getObservers();
866        let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
867        let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
868        notify(observers, "onItemRemoved", [ item._id, item._parentId, item.index,
869                                             item.type, uri, item.guid,
870                                             item.parentGuid,
871                                             options.source ],
872                                           { isTagging: isUntagging });
873
874        if (isUntagging) {
875          for (let entry of (await fetchBookmarksByURL(item, true))) {
876            notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
877                                                 PlacesUtils.toPRTime(entry.lastModified),
878                                                 entry.type, entry._parentId,
879                                                 entry.guid, entry.parentGuid,
880                                                 "", options.source ]);
881          }
882        }
883      }
884    })();
885  },
886
887  /**
888   * Removes ALL bookmarks, resetting the bookmarks storage to an empty tree.
889   *
890   * Note that roots are preserved, only their children will be removed.
891   *
892   * @param {Object} [options={}]
893   *        Additional options. Currently supports the following properties:
894   *         - source: The change source, forwarded to all bookmark observers.
895   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
896   *
897   * @return {Promise} resolved when the removal is complete.
898   * @resolves once the removal is complete.
899   */
900  eraseEverything(options = {}) {
901    if (!options.source) {
902      options.source = Bookmarks.SOURCES.DEFAULT;
903    }
904
905    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
906      async function(db) {
907        let urls;
908
909        await db.executeTransaction(async function() {
910          urls = await removeFoldersContents(db, Bookmarks.userContentRoots, options);
911          const time = PlacesUtils.toPRTime(new Date());
912          const syncChangeDelta =
913            PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
914          for (let folderGuid of Bookmarks.userContentRoots) {
915            await db.executeCached(
916              `UPDATE moz_bookmarks SET lastModified = :time,
917                                        syncChangeCounter = syncChangeCounter + :syncChangeDelta
918               WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
919              `, { folderGuid, time, syncChangeDelta });
920          }
921        });
922
923        // We don't wait for the frecency calculation.
924        if (urls && urls.length) {
925          await PlacesUtils.keywords.eraseEverything();
926          updateFrecency(db, urls, true).catch(Cu.reportError);
927        }
928      }
929    );
930  },
931
932  /**
933   * Returns a list of recently bookmarked items.
934   * Only includes actual bookmarks. Excludes folders, separators and queries.
935   *
936   * @param {integer} numberOfItems
937   *        The maximum number of bookmark items to return.
938   *
939   * @return {Promise} resolved when the listing is complete.
940   * @resolves to an array of recent bookmark-items.
941   * @rejects if an error happens while querying.
942   */
943  getRecent(numberOfItems) {
944    if (numberOfItems === undefined) {
945      throw new Error("numberOfItems argument is required");
946    }
947    if (!typeof numberOfItems === "number" || (numberOfItems % 1) !== 0) {
948      throw new Error("numberOfItems argument must be an integer");
949    }
950    if (numberOfItems <= 0) {
951      throw new Error("numberOfItems argument must be greater than zero");
952    }
953
954    return fetchRecentBookmarks(numberOfItems);
955  },
956
957  /**
958   * Fetches information about a bookmark-item.
959   *
960   * REMARK: any successful call to this method resolves to a single
961   *         bookmark-item (or null), even when multiple bookmarks may exist
962   *         (e.g. fetching by url).  If you wish to retrieve all of the
963   *         bookmarks for a given match, use the callback instead.
964   *
965   * Input can be either a guid or an object with one, and only one, of these
966   * filtering properties set:
967   *  - guid
968   *      retrieves the item with the specified guid.
969   *  - parentGuid and index
970   *      retrieves the item by its position.
971   *  - url
972   *      retrieves the most recent bookmark having the given URL.
973   *      To retrieve ALL of the bookmarks for that URL, you must pass in an
974   *      onResult callback, that will be invoked once for each found bookmark.
975   *  - guidPrefix
976   *      retrieves the most recent item with the specified guid prefix.
977   *      To retrieve ALL of the bookmarks for that guid prefix, you must pass
978   *      in an onResult callback, that will be invoked once for each bookmark.
979   *
980   * @param guidOrInfo
981   *        The globally unique identifier of the item to fetch, or an
982   *        object representing it, as defined above.
983   * @param onResult [optional]
984   *        Callback invoked for each found bookmark.
985   * @param options [optional]
986   *        an optional object whose properties describe options for the fetch:
987   *         - concurrent: fetches concurrently to any writes, returning results
988   *                       faster. On the negative side, it may return stale
989   *                       information missing the currently ongoing write.
990   *
991   * @return {Promise} resolved when the fetch is complete.
992   * @resolves to an object representing the found item, as described above, or
993   *           an array of such objects.  if no item is found, the returned
994   *           promise is resolved to null.
995   * @rejects if an error happens while fetching.
996   * @throws if the arguments are invalid.
997   *
998   * @note Any unknown property in the info object is ignored.  Known properties
999   *       may be overwritten.
1000   */
1001  fetch(guidOrInfo, onResult = null, options = {}) {
1002    if (!("concurrent" in options)) {
1003      options.concurrent = false;
1004    }
1005    if (onResult && typeof onResult != "function")
1006      throw new Error("onResult callback must be a valid function");
1007    let info = guidOrInfo;
1008    if (!info)
1009      throw new Error("Input should be a valid object");
1010    if (typeof(info) != "object") {
1011      info = { guid: guidOrInfo };
1012    } else if (Object.keys(info).length == 1) {
1013      // Just a faster code path.
1014      if (!["url", "guid", "parentGuid", "index", "guidPrefix"].includes(Object.keys(info)[0]))
1015        throw new Error(`Unexpected number of conditions provided: 0`);
1016    } else {
1017      // Only one condition at a time can be provided.
1018      let conditionsCount = [
1019        v => v.hasOwnProperty("guid"),
1020        v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
1021        v => v.hasOwnProperty("url"),
1022        v => v.hasOwnProperty("guidPrefix")
1023      ].reduce((old, fn) => old + fn(info) | 0, 0);
1024      if (conditionsCount != 1)
1025        throw new Error(`Unexpected number of conditions provided: ${conditionsCount}`);
1026    }
1027
1028    let behavior = {};
1029    if (info.hasOwnProperty("parentGuid") || info.hasOwnProperty("index")) {
1030      behavior = {
1031        parentGuid: { requiredIf: b => b.hasOwnProperty("index") },
1032        index: { requiredIf: b => b.hasOwnProperty("parentGuid"),
1033                 validIf: b => typeof(b.index) == "number" &&
1034                               b.index >= 0 || b.index == this.DEFAULT_INDEX }
1035      };
1036    }
1037
1038    // Even if we ignore any other unneeded property, we still validate any
1039    // known property to reduce likelihood of hidden bugs.
1040    let fetchInfo = validateBookmarkObject("Bookmarks.jsm: fetch", info,
1041                                           behavior);
1042
1043    return (async function() {
1044      let results;
1045      if (fetchInfo.hasOwnProperty("url"))
1046        results = await fetchBookmarksByURL(fetchInfo, options && options.concurrent);
1047      else if (fetchInfo.hasOwnProperty("guid"))
1048        results = await fetchBookmark(fetchInfo, options && options.concurrent);
1049      else if (fetchInfo.hasOwnProperty("parentGuid") && fetchInfo.hasOwnProperty("index"))
1050        results = await fetchBookmarkByPosition(fetchInfo, options && options.concurrent);
1051      else if (fetchInfo.hasOwnProperty("guidPrefix"))
1052        results = await fetchBookmarksByGUIDPrefix(fetchInfo, options && options.concurrent);
1053
1054      if (!results)
1055        return null;
1056
1057      if (!Array.isArray(results))
1058        results = [results];
1059      // Remove non-enumerable properties.
1060      results = results.map(r => Object.assign({}, r));
1061
1062      // Ideally this should handle an incremental behavior and thus be invoked
1063      // while we fetch.  Though, the likelihood of 2 or more bookmarks for the
1064      // same match is very low, so it's not worth the added code complication.
1065      if (onResult) {
1066        for (let result of results) {
1067          try {
1068            onResult(result);
1069          } catch (ex) {
1070            Cu.reportError(ex);
1071          }
1072        }
1073      }
1074
1075      return results[0];
1076    })();
1077  },
1078
1079  /**
1080   * Retrieves an object representation of a bookmark-item, along with all of
1081   * its descendants, if any.
1082   *
1083   * Each node in the tree is an object that extends the item representation
1084   * described above with some additional properties:
1085   *
1086   *  - [deprecated] id (number)
1087   *      the item's id.  Defined only if aOptions.includeItemIds is set.
1088   *  - annos (array)
1089   *      the item's annotations.  This is not set if there are no annotations
1090   *      set for the item.
1091   *
1092   * The root object of the tree also has the following properties set:
1093   *  - itemsCount (number, not enumerable)
1094   *      the number of items, including the root item itself, which are
1095   *      represented in the resolved object.
1096   *
1097   * Bookmarked URLs may also have the following properties:
1098   *  - tags (string)
1099   *      csv string of the bookmark's tags, if any.
1100   *  - charset (string)
1101   *      the last known charset of the bookmark, if any.
1102   *  - iconurl (URL)
1103   *      the bookmark's favicon URL, if any.
1104   *
1105   * Folders may also have the following properties:
1106   *  - children (array)
1107   *      the folder's children information, each of them having the same set of
1108   *      properties as above.
1109   *
1110   * @param [optional] guid
1111   *        the topmost item to be queried.  If it's not passed, the Places
1112   *        root folder is queried: that is, you get a representation of the
1113   *        entire bookmarks hierarchy.
1114   * @param [optional] options
1115   *        Options for customizing the query behavior, in the form of an
1116   *        object with any of the following properties:
1117   *         - excludeItemsCallback: a function for excluding items, along with
1118   *           their descendants.  Given an item object (that has everything set
1119   *           apart its potential children data), it should return true if the
1120   *           item should be excluded.  Once an item is excluded, the function
1121   *           isn't called for any of its descendants.  This isn't called for
1122   *           the root item.
1123   *           WARNING: since the function may be called for each item, using
1124   *           this option can slow down the process significantly if the
1125   *           callback does anything that's not relatively trivial.  It is
1126   *           highly recommended to avoid any synchronous I/O or DB queries.
1127   *         - includeItemIds: opt-in to include the deprecated id property.
1128   *           Use it if you must. It'll be removed once the switch to guids is
1129   *           complete.
1130   *
1131   * @return {Promise} resolved when the fetch is complete.
1132   * @resolves to an object that represents either a single item or a
1133   *           bookmarks tree.  if guid points to a non-existent item, the
1134   *           returned promise is resolved to null.
1135   * @rejects if an error happens while fetching.
1136   * @throws if the arguments are invalid.
1137   */
1138  // TODO must implement these methods yet:
1139  // PlacesUtils.promiseBookmarksTree()
1140  fetchTree(guid = "", options = {}) {
1141    throw new Error("Not yet implemented");
1142  },
1143
1144  /**
1145   * Reorders contents of a folder based on a provided array of GUIDs.
1146   *
1147   * @param parentGuid
1148   *        The globally unique identifier of the folder whose contents should
1149   *        be reordered.
1150   * @param orderedChildrenGuids
1151   *        Ordered array of the children's GUIDs.  If this list contains
1152   *        non-existing entries they will be ignored.  If the list is
1153   *        incomplete, and the current child list is already in order with
1154   *        respect to orderedChildrenGuids, no change is made. Otherwise, the
1155   *        new items are appended but maintain their current order relative to
1156   *        eachother.
1157   * @param {Object} [options={}]
1158   *        Additional options. Currently supports the following properties:
1159   *         - source: The change source, forwarded to all bookmark observers.
1160   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1161   *
1162   * @return {Promise} resolved when reordering is complete.
1163   * @rejects if an error happens while reordering.
1164   * @throws if the arguments are invalid.
1165   */
1166  reorder(parentGuid, orderedChildrenGuids, options = {}) {
1167    let info = { guid: parentGuid };
1168    info = validateBookmarkObject("Bookmarks.jsm: reorder", info,
1169                                  { guid: { required: true } });
1170
1171    if (!Array.isArray(orderedChildrenGuids) || !orderedChildrenGuids.length)
1172      throw new Error("Must provide a sorted array of children GUIDs.");
1173    try {
1174      orderedChildrenGuids.forEach(PlacesUtils.BOOKMARK_VALIDATORS.guid);
1175    } catch (ex) {
1176      throw new Error("Invalid GUID found in the sorted children array.");
1177    }
1178
1179    if (!("source" in options)) {
1180      options.source = Bookmarks.SOURCES.DEFAULT;
1181    }
1182
1183    return (async () => {
1184      let parent = await fetchBookmark(info);
1185      if (!parent || parent.type != this.TYPE_FOLDER)
1186        throw new Error("No folder found for the provided GUID.");
1187
1188      let sortedChildren = await reorderChildren(parent, orderedChildrenGuids,
1189                                                 options);
1190
1191      let { source = Bookmarks.SOURCES.DEFAULT } = options;
1192      let observers = PlacesUtils.bookmarks.getObservers();
1193      // Note that child.index is the old index.
1194      for (let i = 0; i < sortedChildren.length; ++i) {
1195        let child = sortedChildren[i];
1196        notify(observers, "onItemMoved", [ child._id, child._parentId,
1197                                           child.index, child._parentId,
1198                                           i, child.type,
1199                                           child.guid, child.parentGuid,
1200                                           child.parentGuid,
1201                                           source ]);
1202      }
1203    })();
1204  },
1205
1206  /**
1207   * Searches a list of bookmark-items by a search term, url or title.
1208   *
1209   * IMPORTANT:
1210   * This is intended as an interim API for the web-extensions implementation.
1211   * It will be removed as soon as we have a new querying API.
1212   *
1213   * Note also that this used to exclude separators but no longer does so.
1214   *
1215   * If you just want to search bookmarks by URL, use .fetch() instead.
1216   *
1217   * @param query
1218   *        Either a string to use as search term, or an object
1219   *        containing any of these keys: query, title or url with the
1220   *        corresponding string to match as value.
1221   *        The url property can be either a string or an nsIURI.
1222   *
1223   * @return {Promise} resolved when the search is complete.
1224   * @resolves to an array of found bookmark-items.
1225   * @rejects if an error happens while searching.
1226   * @throws if the arguments are invalid.
1227   *
1228   * @note Any unknown property in the query object is ignored.
1229   *       Known properties may be overwritten.
1230   */
1231  search(query) {
1232    if (!query) {
1233      throw new Error("Query object is required");
1234    }
1235    if (typeof query === "string") {
1236      query = { query };
1237    }
1238    if (typeof query !== "object") {
1239      throw new Error("Query must be an object or a string");
1240    }
1241    if (query.query && typeof query.query !== "string") {
1242      throw new Error("Query option must be a string");
1243    }
1244    if (query.title && typeof query.title !== "string") {
1245      throw new Error("Title option must be a string");
1246    }
1247
1248    if (query.url) {
1249      if (typeof query.url === "string" || (query.url instanceof URL)) {
1250        query.url = new URL(query.url).href;
1251      } else if (query.url instanceof Ci.nsIURI) {
1252        query.url = query.url.spec;
1253      } else {
1254        throw new Error("Url option must be a string or a URL object");
1255      }
1256    }
1257
1258    return queryBookmarks(query);
1259  },
1260});
1261
1262// Globals.
1263
1264/**
1265 * Sends a bookmarks notification through the given observers.
1266 *
1267 * @param {Array} observers
1268 *        array of nsINavBookmarkObserver objects.
1269 * @param {String} notification
1270 *        the notification name.
1271 * @param {Array} [args]
1272 *        array of arguments to pass to the notification.
1273 * @param {Object} [information]
1274 *        Information about the notification, so we can filter based
1275 *        based on the observer's preferences.
1276 */
1277function notify(observers, notification, args = [], information = {}) {
1278  for (let observer of observers) {
1279    if (information.isTagging && observer.skipTags) {
1280      continue;
1281    }
1282
1283    if (information.isDescendantRemoval && observer.skipDescendantsOnItemRemoval &&
1284        !(PlacesUtils.bookmarks.userContentRoots.includes(information.parentGuid))) {
1285      continue;
1286    }
1287
1288    try {
1289      observer[notification](...args);
1290    } catch (ex) {}
1291  }
1292}
1293
1294// Update implementation.
1295
1296function updateBookmark(info, item, newParent) {
1297  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
1298    async function(db) {
1299
1300    let tuples = new Map();
1301    tuples.set("lastModified", { value: PlacesUtils.toPRTime(info.lastModified) });
1302    if (info.hasOwnProperty("title"))
1303      tuples.set("title", { value: info.title,
1304                            fragment: `title = NULLIF(:title, "")` });
1305    if (info.hasOwnProperty("dateAdded"))
1306      tuples.set("dateAdded", { value: PlacesUtils.toPRTime(info.dateAdded) });
1307
1308    await db.executeTransaction(async function() {
1309      let isTagging = item._grandParentId == PlacesUtils.tagsFolderId;
1310      let syncChangeDelta =
1311        PlacesSyncUtils.bookmarks.determineSyncChangeDelta(info.source);
1312
1313      if (info.hasOwnProperty("url")) {
1314        // Ensure a page exists in moz_places for this URL.
1315        await maybeInsertPlace(db, info.url);
1316        // Update tuples for the update query.
1317        tuples.set("url", { value: info.url.href,
1318                            fragment: "fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)" });
1319      }
1320
1321      let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
1322      if (newParent) {
1323        // For simplicity, update the index regardless.
1324        tuples.set("position", { value: newIndex });
1325
1326        if (newParent.guid == item.parentGuid) {
1327          // Moving inside the original container.
1328          // When moving "up", add 1 to each index in the interval.
1329          // Otherwise when moving down, we subtract 1.
1330          // Only the parent needs a sync change, which is handled in
1331          // `setAncestorsLastModified`.
1332          let sign = newIndex < item.index ? +1 : -1;
1333          await db.executeCached(
1334            `UPDATE moz_bookmarks SET position = position + :sign
1335             WHERE parent = :newParentId
1336               AND position BETWEEN :lowIndex AND :highIndex
1337            `, { sign, newParentId: newParent._id,
1338                 lowIndex: Math.min(item.index, newIndex),
1339                 highIndex: Math.max(item.index, newIndex) });
1340        } else {
1341          // Moving across different containers. In this case, both parents and
1342          // the child need sync changes. `setAncestorsLastModified` handles the
1343          // parents; the `needsSyncChange` check below handles the child.
1344          tuples.set("parent", { value: newParent._id} );
1345          await db.executeCached(
1346            `UPDATE moz_bookmarks SET position = position + :sign
1347             WHERE parent = :oldParentId
1348               AND position >= :oldIndex
1349            `, { sign: -1, oldParentId: item._parentId, oldIndex: item.index });
1350          await db.executeCached(
1351            `UPDATE moz_bookmarks SET position = position + :sign
1352             WHERE parent = :newParentId
1353               AND position >= :newIndex
1354            `, { sign: +1, newParentId: newParent._id, newIndex });
1355
1356          await setAncestorsLastModified(db, item.parentGuid, info.lastModified,
1357                                         syncChangeDelta);
1358        }
1359        await setAncestorsLastModified(db, newParent.guid, info.lastModified,
1360                                       syncChangeDelta);
1361      }
1362
1363      if (syncChangeDelta) {
1364        // Sync stores child indices in the parent's record, so we only bump the
1365        // item's counter if we're updating at least one more property in
1366        // addition to the index, last modified time, and dateAdded.
1367        let sizeThreshold = 1;
1368        if (info.hasOwnProperty("index") && info.index != item.index) {
1369          ++sizeThreshold;
1370        }
1371        if (tuples.has("dateAdded")) {
1372          ++sizeThreshold;
1373        }
1374        let needsSyncChange = tuples.size > sizeThreshold;
1375        if (needsSyncChange) {
1376          tuples.set("syncChangeDelta", { value: syncChangeDelta,
1377                                          fragment: "syncChangeCounter = syncChangeCounter + :syncChangeDelta" });
1378        }
1379      }
1380
1381      if (isTagging) {
1382        // If we're updating a tag entry, bump the sync change counter for
1383        // bookmarks with the tagged URL.
1384        await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1385          db, item.url, syncChangeDelta);
1386        if (info.hasOwnProperty("url")) {
1387          // Changing the URL of a tag entry is equivalent to untagging the
1388          // old URL and tagging the new one, so we bump the change counter
1389          // for the new URL here.
1390          await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1391            db, info.url, syncChangeDelta);
1392        }
1393      }
1394
1395      let isChangingTagFolder = item._parentId == PlacesUtils.tagsFolderId;
1396      if (isChangingTagFolder) {
1397        // If we're updating a tag folder (for example, changing a tag's title),
1398        // bump the change counter for all tagged bookmarks.
1399        await addSyncChangesForBookmarksInFolder(db, item, syncChangeDelta);
1400      }
1401
1402      await db.executeCached(
1403        `UPDATE moz_bookmarks
1404         SET ${Array.from(tuples.keys()).map(v => tuples.get(v).fragment || `${v} = :${v}`).join(", ")}
1405         WHERE guid = :guid
1406        `, Object.assign({ guid: info.guid },
1407                         [...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
1408
1409      if (newParent) {
1410        if (newParent.guid == item.parentGuid) {
1411          // Mark all affected separators as changed
1412          // Also bumps the change counter if the item itself is a separator
1413          const startIndex = Math.min(newIndex, item.index);
1414          await adjustSeparatorsSyncCounter(db, newParent._id, startIndex, syncChangeDelta);
1415        } else {
1416          // Mark all affected separators as changed
1417          await adjustSeparatorsSyncCounter(db, item._parentId, item.index, syncChangeDelta);
1418          await adjustSeparatorsSyncCounter(db, newParent._id, newIndex, syncChangeDelta);
1419        }
1420        // Remove the Sync orphan annotation from reparented items. We don't
1421        // notify annotation observers about this because this is a temporary,
1422        // internal anno that's only used by Sync.
1423        await db.executeCached(
1424          `DELETE FROM moz_items_annos
1425           WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes
1426                                      WHERE name = :orphanAnno) AND
1427                 item_id = :id`,
1428          { orphanAnno: PlacesSyncUtils.bookmarks.SYNC_PARENT_ANNO,
1429            id: item._id });
1430      }
1431    });
1432
1433    // If the parent changed, update related non-enumerable properties.
1434    let additionalParentInfo = {};
1435    if (newParent) {
1436      Object.defineProperty(additionalParentInfo, "_parentId",
1437                            { value: newParent._id, enumerable: false });
1438      Object.defineProperty(additionalParentInfo, "_grandParentId",
1439                            { value: newParent._parentId, enumerable: false });
1440    }
1441
1442    let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
1443
1444    return updatedItem;
1445  });
1446}
1447
1448// Insert implementation.
1449
1450function insertBookmark(item, parent) {
1451  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmark",
1452    async function(db) {
1453
1454    // If a guid was not provided, generate one, so we won't need to fetch the
1455    // bookmark just after having created it.
1456    let hasExistingGuid = item.hasOwnProperty("guid");
1457    if (!hasExistingGuid)
1458      item.guid = PlacesUtils.history.makeGuid();
1459
1460    let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
1461
1462    await db.executeTransaction(async function transaction() {
1463      if (item.type == Bookmarks.TYPE_BOOKMARK) {
1464        // Ensure a page exists in moz_places for this URL.
1465        // The IGNORE conflict can trigger on `guid`.
1466        await maybeInsertPlace(db, item.url);
1467      }
1468
1469      // Adjust indices.
1470      await db.executeCached(
1471        `UPDATE moz_bookmarks SET position = position + 1
1472         WHERE parent = :parent
1473         AND position >= :index
1474        `, { parent: parent._id, index: item.index });
1475
1476      let syncChangeDelta =
1477        PlacesSyncUtils.bookmarks.determineSyncChangeDelta(item.source);
1478      let syncStatus =
1479        PlacesSyncUtils.bookmarks.determineInitialSyncStatus(item.source);
1480
1481      // Insert the bookmark into the database.
1482      await db.executeCached(
1483        `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
1484                                    dateAdded, lastModified, guid,
1485                                    syncChangeCounter, syncStatus)
1486         VALUES (CASE WHEN :url ISNULL THEN NULL ELSE (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) END,
1487                 :type, :parent, :index, NULLIF(:title, ""), :date_added,
1488                 :last_modified, :guid, :syncChangeCounter, :syncStatus)
1489        `, { url: item.hasOwnProperty("url") ? item.url.href : null,
1490             type: item.type, parent: parent._id, index: item.index,
1491             title: item.title, date_added: PlacesUtils.toPRTime(item.dateAdded),
1492             last_modified: PlacesUtils.toPRTime(item.lastModified), guid: item.guid,
1493             syncChangeCounter: syncChangeDelta, syncStatus });
1494
1495      // Mark all affected separators as changed
1496      await adjustSeparatorsSyncCounter(db, parent._id, item.index + 1, syncChangeDelta);
1497
1498      if (hasExistingGuid) {
1499        // Remove stale tombstones if we're reinserting an item.
1500        await db.executeCached(
1501          `DELETE FROM moz_bookmarks_deleted WHERE guid = :guid`,
1502          { guid: item.guid });
1503      }
1504
1505      if (isTagging) {
1506        // New tag entry; bump the change counter for bookmarks with the
1507        // tagged URL.
1508        await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1509          db, item.url, syncChangeDelta);
1510      }
1511
1512      await setAncestorsLastModified(db, item.parentGuid, item.dateAdded,
1513                                     syncChangeDelta);
1514    });
1515
1516    // If not a tag recalculate frecency...
1517    if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
1518      // ...though we don't wait for the calculation.
1519      updateFrecency(db, [item.url]).catch(Cu.reportError);
1520    }
1521
1522    return item;
1523  });
1524}
1525
1526/**
1527 * Determines if a bookmark is a Livemark depending on how it is annotated.
1528 *
1529 * @param {Object} node The bookmark node to check.
1530 * @returns {Boolean} True if the node is a Livemark, false otherwise.
1531 */
1532function isLivemark(node) {
1533  return node.type == Bookmarks.TYPE_FOLDER && node.annos &&
1534         node.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI);
1535}
1536
1537function insertBookmarkTree(items, source, parent, urls, lastAddedForParent) {
1538  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmarkTree", async function(db) {
1539    await db.executeTransaction(async function transaction() {
1540      await maybeInsertManyPlaces(db, urls);
1541
1542      let syncChangeDelta =
1543        PlacesSyncUtils.bookmarks.determineSyncChangeDelta(source);
1544      let syncStatus =
1545        PlacesSyncUtils.bookmarks.determineInitialSyncStatus(source);
1546
1547      let rootId = parent._id;
1548
1549      items = items.map(item => ({
1550        url: item.url && item.url.href,
1551        type: item.type, parentGuid: item.parentGuid, index: item.index,
1552        title: item.title, date_added: PlacesUtils.toPRTime(item.dateAdded),
1553        last_modified: PlacesUtils.toPRTime(item.lastModified), guid: item.guid,
1554        syncChangeCounter: syncChangeDelta, syncStatus, rootId
1555      }));
1556      await db.executeCached(
1557        `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
1558                                    dateAdded, lastModified, guid,
1559                                    syncChangeCounter, syncStatus)
1560         VALUES (CASE WHEN :url ISNULL THEN NULL ELSE (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) END, :type,
1561         (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid),
1562         IFNULL(:index, (SELECT count(*) FROM moz_bookmarks WHERE parent = :rootId)),
1563         NULLIF(:title, ""), :date_added, :last_modified, :guid,
1564         :syncChangeCounter, :syncStatus)`, items);
1565
1566      // Remove stale tombstones for new items.
1567      for (let chunk of chunkArray(items, SQLITE_MAX_VARIABLE_NUMBER)) {
1568        await db.executeCached(
1569          `DELETE FROM moz_bookmarks_deleted WHERE guid IN (${
1570            new Array(chunk.length).fill("?").join(",")})`,
1571          chunk.map(item => item.guid)
1572        );
1573      }
1574
1575      await setAncestorsLastModified(db, parent.guid, lastAddedForParent,
1576                                     syncChangeDelta);
1577    });
1578
1579    // We don't wait for the frecency calculation.
1580    updateFrecency(db, urls, true).catch(Cu.reportError);
1581
1582    return items;
1583  });
1584}
1585
1586/**
1587 * Handles data for a Livemark insert.
1588 *
1589 * @param {Object} item Livemark item that need to be added.
1590 */
1591async function insertLivemarkData(item) {
1592  // Delete the placeholder but note the index of it, so that we can insert the
1593  // livemark item at the right place.
1594  let placeholder = await Bookmarks.fetch(item.guid);
1595  let index = placeholder.index;
1596  await removeBookmarks([item], {source: item.source});
1597
1598  let feedURI = null;
1599  let siteURI = null;
1600  item.annos = item.annos.filter(function(aAnno) {
1601    switch (aAnno.name) {
1602      case PlacesUtils.LMANNO_FEEDURI:
1603        feedURI = NetUtil.newURI(aAnno.value);
1604        return false;
1605      case PlacesUtils.LMANNO_SITEURI:
1606        siteURI = NetUtil.newURI(aAnno.value);
1607        return false;
1608      default:
1609        return true;
1610    }
1611  });
1612
1613  if (feedURI) {
1614    item.feedURI = feedURI;
1615    item.siteURI = siteURI;
1616    item.index = index;
1617
1618    if (item.dateAdded) {
1619      item.dateAdded = PlacesUtils.toPRTime(item.dateAdded);
1620    }
1621    if (item.lastModified) {
1622      item.lastModified = PlacesUtils.toPRTime(item.lastModified);
1623    }
1624
1625    let livemark = await PlacesUtils.livemarks.addLivemark(item);
1626
1627    let id = livemark.id;
1628    if (item.annos && item.annos.length) {
1629      // Note: for annotations, we intentionally skip updating the last modified
1630      // value for the bookmark, to avoid a second update of the added bookmark.
1631      PlacesUtils.setAnnotationsForItem(id, item.annos, item.source, true);
1632    }
1633  }
1634}
1635
1636/**
1637 * Handles special data on a bookmark, e.g. annotations, keywords, tags, charsets,
1638 * inserting the data into the appropriate place.
1639 *
1640 * @param {Integer} itemId The ID of the item within the bookmarks database.
1641 * @param {Object} item The bookmark item with possible special data to be inserted.
1642 */
1643async function handleBookmarkItemSpecialData(itemId, item) {
1644  if (item.annos && item.annos.length) {
1645    // Note: for annotations, we intentionally skip updating the last modified
1646    // value for the bookmark, to avoid a second update of the added bookmark.
1647    try {
1648      PlacesUtils.setAnnotationsForItem(itemId, item.annos, item.source, true);
1649    } catch (ex) {
1650      Cu.reportError(`Failed to insert annotations for item: ${ex}`);
1651    }
1652  }
1653  if ("keyword" in item && item.keyword) {
1654    // POST data could be set in 2 ways:
1655    // 1. new backups have a postData property
1656    // 2. old backups have an item annotation
1657    let postDataAnno = item.annos &&
1658                       item.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
1659    let postData = item.postData || (postDataAnno && postDataAnno.value);
1660    try {
1661      await PlacesUtils.keywords.insert({
1662        keyword: item.keyword,
1663        url: item.url,
1664        postData,
1665        source: item.source
1666      });
1667    } catch (ex) {
1668      Cu.reportError(`Failed to insert keyword "${item.keyword} for ${item.url}": ${ex}`);
1669    }
1670  }
1671  if ("tags" in item) {
1672    try {
1673      PlacesUtils.tagging.tagURI(NetUtil.newURI(item.url), item.tags, item.source);
1674    } catch (ex) {
1675      // Invalid tag child, skip it.
1676      Cu.reportError(`Unable to set tags "${item.tags.join(", ")}" for ${item.url}: ${ex}`);
1677    }
1678  }
1679  if ("charset" in item && item.charset) {
1680    try {
1681      await PlacesUtils.setCharsetForURI(NetUtil.newURI(item.url), item.charset);
1682    } catch (ex) {
1683      Cu.reportError(`Failed to set charset "${item.charset}" for ${item.url}: ${ex}`);
1684    }
1685  }
1686}
1687
1688// Query implementation.
1689
1690async function queryBookmarks(info) {
1691  let queryParams = {
1692    tags_folder: await promiseTagsFolderId(),
1693  };
1694  // We're searching for bookmarks, so exclude tags.
1695  let queryString = "WHERE b.parent <> :tags_folder";
1696  queryString += " AND p.parent <> :tags_folder";
1697
1698  if (info.title) {
1699    queryString += " AND b.title = :title";
1700    queryParams.title = info.title;
1701  }
1702
1703  if (info.url) {
1704    queryString += " AND h.url_hash = hash(:url) AND h.url = :url";
1705    queryParams.url = info.url;
1706  }
1707
1708  if (info.query) {
1709    queryString += " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior) ";
1710    queryParams.query = info.query;
1711    queryParams.matchBehavior = MATCH_ANYWHERE_UNMODIFIED;
1712    queryParams.searchBehavior = BEHAVIOR_BOOKMARK;
1713  }
1714
1715  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks",
1716    async function(db) {
1717      // _id, _childCount, _grandParentId and _parentId fields
1718      // are required to be in the result by the converting function
1719      // hence setting them to NULL
1720      let rows = await db.executeCached(
1721        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1722                b.dateAdded, b.lastModified, b.type,
1723                IFNULL(b.title, "") AS title, h.url AS url, b.parent, p.parent,
1724                NULL AS _id,
1725                NULL AS _childCount,
1726                NULL AS _grandParentId,
1727                NULL AS _parentId,
1728                NULL AS _syncStatus
1729         FROM moz_bookmarks b
1730         LEFT JOIN moz_bookmarks p ON p.id = b.parent
1731         LEFT JOIN moz_places h ON h.id = b.fk
1732         ${queryString}
1733        `, queryParams);
1734
1735      return rowsToItemsArray(rows);
1736    }
1737  );
1738}
1739
1740
1741// Fetch implementation.
1742
1743async function fetchBookmark(info, concurrent) {
1744  let query = async function(db) {
1745    let rows = await db.executeCached(
1746      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1747              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
1748              h.url AS url, b.id AS _id, b.parent AS _parentId,
1749              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
1750              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
1751       FROM moz_bookmarks b
1752       LEFT JOIN moz_bookmarks p ON p.id = b.parent
1753       LEFT JOIN moz_places h ON h.id = b.fk
1754       WHERE b.guid = :guid
1755      `, { guid: info.guid });
1756
1757    return rows.length ? rowsToItemsArray(rows)[0] : null;
1758  };
1759  if (concurrent) {
1760    let db = await PlacesUtils.promiseDBConnection();
1761    return query(db);
1762  }
1763  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmark",
1764                                           query);
1765}
1766
1767async function fetchBookmarkByPosition(info, concurrent) {
1768  let query = async function(db) {
1769    let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
1770    let rows = await db.executeCached(
1771      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1772              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
1773              h.url AS url, b.id AS _id, b.parent AS _parentId,
1774              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
1775              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
1776       FROM moz_bookmarks b
1777       LEFT JOIN moz_bookmarks p ON p.id = b.parent
1778       LEFT JOIN moz_places h ON h.id = b.fk
1779       WHERE p.guid = :parentGuid
1780       AND b.position = IFNULL(:index, (SELECT count(*) - 1
1781                                        FROM moz_bookmarks
1782                                        WHERE parent = p.id))
1783      `, { parentGuid: info.parentGuid, index });
1784
1785    return rows.length ? rowsToItemsArray(rows)[0] : null;
1786  };
1787  if (concurrent) {
1788    let db = await PlacesUtils.promiseDBConnection();
1789    return query(db);
1790  }
1791  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarkByPosition",
1792                                           query);
1793}
1794
1795async function fetchBookmarksByGUIDPrefix(info, concurrent) {
1796  let query = async function(db) {
1797    let rows = await db.executeCached(
1798      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1799              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
1800              h.url AS url, b.id AS _id, b.parent AS _parentId,
1801              NULL AS _childCount,
1802              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
1803       FROM moz_bookmarks b
1804       LEFT JOIN moz_bookmarks p ON p.id = b.parent
1805       LEFT JOIN moz_places h ON h.id = b.fk
1806       WHERE b.guid LIKE :guidPrefix
1807       ORDER BY b.lastModified DESC
1808      `, { guidPrefix: info.guidPrefix + "%" });
1809
1810    return rows.length ? rowsToItemsArray(rows) : null;
1811  };
1812
1813  if (concurrent) {
1814    let db = await PlacesUtils.promiseDBConnection();
1815    return query(db);
1816  }
1817  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByGUIDPrefix",
1818                                           query);
1819}
1820
1821async function fetchBookmarksByURL(info, concurrent) {
1822  let query = async function(db) {
1823    let tagsFolderId = await promiseTagsFolderId();
1824    let rows = await db.executeCached(
1825      `/* do not warn (bug no): not worth to add an index */
1826      SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1827              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
1828              h.url AS url, b.id AS _id, b.parent AS _parentId,
1829              NULL AS _childCount, /* Unused for now */
1830              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
1831      FROM moz_bookmarks b
1832      JOIN moz_bookmarks p ON p.id = b.parent
1833      JOIN moz_places h ON h.id = b.fk
1834      WHERE h.url_hash = hash(:url) AND h.url = :url
1835      AND _grandParentId <> :tagsFolderId
1836      ORDER BY b.lastModified DESC
1837      `, { url: info.url.href, tagsFolderId });
1838
1839    return rows.length ? rowsToItemsArray(rows) : null;
1840  };
1841
1842  if (concurrent) {
1843    let db = await PlacesUtils.promiseDBConnection();
1844    return query(db);
1845  }
1846  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByURL",
1847                                           query);
1848}
1849
1850function fetchRecentBookmarks(numberOfItems) {
1851  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchRecentBookmarks",
1852    async function(db) {
1853      let tagsFolderId = await promiseTagsFolderId();
1854      let rows = await db.executeCached(
1855        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1856                b.dateAdded, b.lastModified, b.type,
1857                IFNULL(b.title, "") AS title, h.url AS url, NULL AS _id,
1858                NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId,
1859                NULL AS _syncStatus
1860        FROM moz_bookmarks b
1861        JOIN moz_bookmarks p ON p.id = b.parent
1862        JOIN moz_places h ON h.id = b.fk
1863        WHERE p.parent <> :tagsFolderId
1864        AND b.type = :type
1865        AND url_hash NOT BETWEEN hash("place", "prefix_lo")
1866                              AND hash("place", "prefix_hi")
1867        ORDER BY b.dateAdded DESC, b.ROWID DESC
1868        LIMIT :numberOfItems
1869        `, {
1870          tagsFolderId,
1871          type: Bookmarks.TYPE_BOOKMARK,
1872          numberOfItems,
1873        });
1874
1875      return rows.length ? rowsToItemsArray(rows) : [];
1876    }
1877  );
1878}
1879
1880async function fetchBookmarksByParent(db, info) {
1881  let rows = await db.executeCached(
1882    `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
1883            b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
1884            h.url AS url, b.id AS _id, b.parent AS _parentId,
1885            (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
1886            p.parent AS _grandParentId, b.syncStatus AS _syncStatus
1887     FROM moz_bookmarks b
1888     LEFT JOIN moz_bookmarks p ON p.id = b.parent
1889     LEFT JOIN moz_places h ON h.id = b.fk
1890     WHERE p.guid = :parentGuid
1891     ORDER BY b.position ASC
1892    `, { parentGuid: info.parentGuid });
1893
1894  return rowsToItemsArray(rows);
1895}
1896
1897// Remove implementation.
1898
1899function removeBookmarks(items, options) {
1900  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: removeBookmarks",
1901    async function(db) {
1902    let urls = [];
1903
1904    await db.executeTransaction(async function transaction() {
1905      let parentGuids = new Set();
1906      let syncChangeDelta =
1907        PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
1908
1909      for (let item of items) {
1910        parentGuids.add(item.parentGuid);
1911
1912        // If it's a folder, remove its contents first.
1913        if (item.type == Bookmarks.TYPE_FOLDER) {
1914          if (options.preventRemovalOfNonEmptyFolders && item._childCount > 0) {
1915            throw new Error("Cannot remove a non-empty folder.");
1916          }
1917          urls = urls.concat(await removeFoldersContents(db, [item.guid], options));
1918        }
1919      }
1920
1921      for (let chunk of chunkArray(items, SQLITE_MAX_VARIABLE_NUMBER)) {
1922        // We don't go through the annotations service for this cause otherwise
1923        // we'd get a pointless onItemChanged notification and it would also
1924        // set lastModified to an unexpected value.
1925        await removeAnnotationsForItems(db, chunk);
1926
1927        // Remove the bookmarks.
1928        await db.executeCached(
1929          `DELETE FROM moz_bookmarks WHERE guid IN (${
1930            new Array(chunk.length).fill("?").join(",")})`,
1931          chunk.map(item => item.guid)
1932        );
1933      }
1934
1935      for (let item of items) {
1936        // Fix indices in the parent.
1937        await db.executeCached(
1938          `UPDATE moz_bookmarks SET position = position - 1 WHERE
1939           parent = :parentId AND position > :index
1940          `, { parentId: item._parentId, index: item.index });
1941
1942        if (item._grandParentId == PlacesUtils.tagsFolderId) {
1943          // If we're removing a tag entry, increment the change counter for all
1944          // bookmarks with the tagged URL.
1945          await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1946            db, item.url, syncChangeDelta);
1947        }
1948
1949        await adjustSeparatorsSyncCounter(db, item._parentId, item.index, syncChangeDelta);
1950      }
1951
1952      for (let guid of parentGuids) {
1953        // Mark all affected parents as changed.
1954        await setAncestorsLastModified(db, guid, new Date(), syncChangeDelta);
1955      }
1956
1957      // Write tombstones for the removed items.
1958      await insertTombstones(db, items, syncChangeDelta);
1959    });
1960
1961    // Update the frecencies outside of the transaction, excluding tags, so that
1962    // the updates can progress in the background.
1963    urls = urls.concat(items.filter(item => {
1964      let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
1965      return !isUntagging && "url" in item;
1966    }).map(item => item.url));
1967
1968    if (urls.length) {
1969      await PlacesUtils.keywords.removeFromURLsIfNotBookmarked(urls);
1970      updateFrecency(db, urls, urls.length > 1).catch(Cu.reportError);
1971    }
1972  });
1973}
1974
1975// Reorder implementation.
1976
1977function reorderChildren(parent, orderedChildrenGuids, options) {
1978  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: reorderChildren",
1979    db => db.executeTransaction(async function() {
1980      // Select all of the direct children for the given parent.
1981      let children = await fetchBookmarksByParent(db, { parentGuid: parent.guid });
1982      if (!children.length) {
1983        return [];
1984      }
1985
1986      // Maps of GUIDs to indices for fast lookups in the comparator function.
1987      let guidIndices = new Map();
1988      let currentIndices = new Map();
1989      for (let i = 0; i < orderedChildrenGuids.length; ++i) {
1990        let guid = orderedChildrenGuids[i];
1991        guidIndices.set(guid, i);
1992      }
1993
1994      // If we got an incomplete list but everything we have is in the right
1995      // order, we do nothing.
1996      let needReorder = true;
1997      let requestedChildIndices = [];
1998      for (let i = 0; i < children.length; ++i) {
1999        // Take the opportunity to build currentIndices here, since we already
2000        // are iterating over the children array.
2001        currentIndices.set(children[i].guid, i);
2002
2003        if (guidIndices.has(children[i].guid)) {
2004          let index = guidIndices.get(children[i].guid);
2005          requestedChildIndices.push(index);
2006        }
2007      }
2008
2009      if (requestedChildIndices.length) {
2010        needReorder = false;
2011        for (let i = 1; i < requestedChildIndices.length; ++i) {
2012          if (requestedChildIndices[i - 1] > requestedChildIndices[i]) {
2013            needReorder = true;
2014            break;
2015          }
2016        }
2017      }
2018
2019      if (needReorder) {
2020
2021
2022        // Reorder the children array according to the specified order, provided
2023        // GUIDs come first, others are appended in somehow random order.
2024        children.sort((a, b) => {
2025          // This works provided fetchBookmarksByParent returns sorted children.
2026          if (!guidIndices.has(a.guid) && !guidIndices.has(b.guid)) {
2027            return currentIndices.get(a.guid) < currentIndices.get(b.guid) ? -1 : 1;
2028          }
2029          if (!guidIndices.has(a.guid)) {
2030            return 1;
2031          }
2032          if (!guidIndices.has(b.guid)) {
2033            return -1;
2034          }
2035          return guidIndices.get(a.guid) < guidIndices.get(b.guid) ? -1 : 1;
2036        });
2037
2038        // Update the bookmarks position now.  If any unknown guid have been
2039        // inserted meanwhile, its position will be set to -position, and we'll
2040        // handle it later.
2041        // To do the update in a single step, we build a VALUES (guid, position)
2042        // table.  We then use count() in the sorting table to avoid skipping values
2043        // when no more existing GUIDs have been provided.
2044        let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
2045                                  .join();
2046        await db.execute(
2047          `WITH sorting(g, p) AS (
2048             VALUES ${valuesTable}
2049           )
2050           UPDATE moz_bookmarks SET position = (
2051             SELECT CASE count(*) WHEN 0 THEN -position
2052                                         ELSE count(*) - 1
2053                    END
2054             FROM sorting a
2055             JOIN sorting b ON b.p <= a.p
2056             WHERE a.g = guid
2057           )
2058           WHERE parent = :parentId
2059          `, { parentId: parent._id});
2060
2061        let syncChangeDelta =
2062          PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
2063        if (syncChangeDelta) {
2064          // Flag the parent as having a change.
2065          await db.executeCached(`
2066            UPDATE moz_bookmarks SET
2067              syncChangeCounter = syncChangeCounter + :syncChangeDelta
2068            WHERE id = :parentId`,
2069            { parentId: parent._id, syncChangeDelta });
2070        }
2071
2072        // Update position of items that could have been inserted in the meanwhile.
2073        // Since this can happen rarely and it's only done for schema coherence
2074        // resonds, we won't notify about these changes.
2075        await db.executeCached(
2076          `CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
2077             AFTER UPDATE OF position ON moz_bookmarks
2078             WHEN NEW.position = -1
2079           BEGIN
2080             UPDATE moz_bookmarks
2081             SET position = (SELECT MAX(position) FROM moz_bookmarks
2082                             WHERE parent = NEW.parent) +
2083                            (SELECT count(*) FROM moz_bookmarks
2084                             WHERE parent = NEW.parent
2085                               AND position BETWEEN OLD.position AND -1)
2086             WHERE guid = NEW.guid;
2087           END
2088          `);
2089
2090        await db.executeCached(
2091          `UPDATE moz_bookmarks SET position = -1 WHERE position < 0`);
2092
2093        await db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
2094      }
2095
2096      // Remove the Sync orphan annotation from the reordered children, so that
2097      // Sync doesn't try to reparent them once it sees the original parents. We
2098      // only do this for explicitly ordered children, to avoid removing orphan
2099      // annos set by Sync.
2100      let possibleOrphanIds = [];
2101      for (let child of children) {
2102        if (guidIndices.has(child.guid)) {
2103          possibleOrphanIds.push(child._id);
2104        }
2105      }
2106      await db.executeCached(
2107        `DELETE FROM moz_items_annos
2108         WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes
2109                                    WHERE name = :orphanAnno) AND
2110               item_id IN (${possibleOrphanIds.join(", ")})`,
2111        { orphanAnno: PlacesSyncUtils.bookmarks.SYNC_PARENT_ANNO });
2112
2113      return children;
2114    })
2115  );
2116}
2117
2118// Helpers.
2119
2120/**
2121 * Merges objects into a new object, included non-enumerable properties.
2122 *
2123 * @param sources
2124 *        source objects to merge.
2125 * @return a new object including all properties from the source objects.
2126 */
2127function mergeIntoNewObject(...sources) {
2128  let dest = {};
2129  for (let src of sources) {
2130    for (let prop of Object.getOwnPropertyNames(src)) {
2131      Object.defineProperty(dest, prop, Object.getOwnPropertyDescriptor(src, prop));
2132    }
2133  }
2134  return dest;
2135}
2136
2137/**
2138 * Remove properties that have the same value across two bookmark objects.
2139 *
2140 * @param dest
2141 *        destination bookmark object.
2142 * @param src
2143 *        source bookmark object.
2144 * @return a cleaned up bookmark object.
2145 * @note "guid" is never removed.
2146 */
2147function removeSameValueProperties(dest, src) {
2148  for (let prop in dest) {
2149    let remove = false;
2150    switch (prop) {
2151      case "lastModified":
2152      case "dateAdded":
2153        remove = src.hasOwnProperty(prop) && dest[prop].getTime() == src[prop].getTime();
2154        break;
2155      case "url":
2156        remove = src.hasOwnProperty(prop) && dest[prop].href == src[prop].href;
2157        break;
2158      default:
2159        remove = dest[prop] == src[prop];
2160    }
2161    if (remove && prop != "guid")
2162      delete dest[prop];
2163  }
2164}
2165
2166/**
2167 * Convert an array of mozIStorageRow objects to an array of bookmark objects.
2168 *
2169 * @param rows
2170 *        the array of mozIStorageRow objects.
2171 * @return an array of bookmark objects.
2172 */
2173function rowsToItemsArray(rows) {
2174  return rows.map(row => {
2175    let item = {};
2176    for (let prop of ["guid", "index", "type", "title"]) {
2177      item[prop] = row.getResultByName(prop);
2178    }
2179    for (let prop of ["dateAdded", "lastModified"]) {
2180      let value = row.getResultByName(prop);
2181      if (value)
2182        item[prop] = PlacesUtils.toDate(value);
2183    }
2184    let parentGuid = row.getResultByName("parentGuid");
2185    if (parentGuid) {
2186      item.parentGuid = parentGuid;
2187    }
2188    let url = row.getResultByName("url");
2189    if (url) {
2190      item.url = new URL(url);
2191    }
2192
2193    for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId",
2194                      "_syncStatus"]) {
2195      let val = row.getResultByName(prop);
2196      if (val !== null) {
2197        // These properties should not be returned to the API consumer, thus
2198        // they are non-enumerable and removed through Object.assign just before
2199        // the object is returned.
2200        // Configurable is set to support mergeIntoNewObject overwrites.
2201        Object.defineProperty(item, prop, { value: val, enumerable: false,
2202                                                        configurable: true });
2203      }
2204    }
2205
2206    return item;
2207  });
2208}
2209
2210function validateBookmarkObject(name, input, behavior) {
2211  return PlacesUtils.validateItemProperties(name,
2212    PlacesUtils.BOOKMARK_VALIDATORS, input, behavior);
2213}
2214
2215/**
2216 * Updates frecency for a list of URLs.
2217 *
2218 * @param db
2219 *        the Sqlite.jsm connection handle.
2220 * @param urls
2221 *        the array of URLs to update.
2222 * @param [optional] collapseNotifications
2223 *        whether we can send just one onManyFrecenciesChanged
2224 *        notification instead of sending one notification for every URL.
2225 */
2226var updateFrecency = async function(db, urls, collapseNotifications = false) {
2227  let urlQuery = 'hash("' + urls.map(url => url.href).join('"), hash("') + '")';
2228
2229  let frecencyClause = "CALCULATE_FRECENCY(id)";
2230  if (!collapseNotifications) {
2231    frecencyClause = "NOTIFY_FRECENCY(" + frecencyClause +
2232                     ", url, guid, hidden, last_visit_date)";
2233  }
2234  // We just use the hashes, since updating a few additional urls won't hurt.
2235  await db.execute(
2236    `UPDATE moz_places
2237     SET hidden = (url_hash BETWEEN hash("place", "prefix_lo") AND hash("place", "prefix_hi")),
2238         frecency = ${frecencyClause}
2239     WHERE url_hash IN ( ${urlQuery} )
2240    `);
2241  if (collapseNotifications) {
2242    let observers = PlacesUtils.history.getObservers();
2243    notify(observers, "onManyFrecenciesChanged");
2244  }
2245};
2246
2247/**
2248 * Removes any orphan annotation entries.
2249 *
2250 * @param db
2251 *        the Sqlite.jsm connection handle.
2252 */
2253var removeOrphanAnnotations = async function(db) {
2254  await db.executeCached(
2255    `DELETE FROM moz_items_annos
2256     WHERE id IN (SELECT a.id from moz_items_annos a
2257                  LEFT JOIN moz_bookmarks b ON a.item_id = b.id
2258                  WHERE b.id ISNULL)
2259    `);
2260  await db.executeCached(
2261    `DELETE FROM moz_anno_attributes
2262     WHERE id IN (SELECT n.id from moz_anno_attributes n
2263                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
2264                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
2265                  WHERE a1.id ISNULL AND a2.id ISNULL)
2266    `);
2267};
2268
2269/**
2270 * Removes annotations for a given item.
2271 *
2272 * @param db
2273 *        the Sqlite.jsm connection handle.
2274 * @param items
2275 *        The items for which to remove annotations.
2276 */
2277var removeAnnotationsForItems = async function(db, items) {
2278  // Remove the annotations.
2279  let ids = sqlList(items.map(item => item._id));
2280  await db.executeCached(
2281    `DELETE FROM moz_items_annos WHERE item_id IN (${ids})`,
2282  );
2283  await db.executeCached(
2284    `DELETE FROM moz_anno_attributes
2285     WHERE id IN (SELECT n.id from moz_anno_attributes n
2286                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
2287                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
2288                  WHERE a1.id ISNULL AND a2.id ISNULL)
2289    `);
2290};
2291
2292/**
2293 * Updates lastModified for all the ancestors of a given folder GUID.
2294 *
2295 * @param db
2296 *        the Sqlite.jsm connection handle.
2297 * @param folderGuid
2298 *        the GUID of the folder whose ancestors should be updated.
2299 * @param time
2300 *        a Date object to use for the update.
2301 *
2302 * @note the folder itself is also updated.
2303 */
2304var setAncestorsLastModified = async function(db, folderGuid, time, syncChangeDelta) {
2305  await db.executeCached(
2306    `WITH RECURSIVE
2307     ancestors(aid) AS (
2308       SELECT id FROM moz_bookmarks WHERE guid = :guid
2309       UNION ALL
2310       SELECT parent FROM moz_bookmarks
2311       JOIN ancestors ON id = aid
2312       WHERE type = :type
2313     )
2314     UPDATE moz_bookmarks SET lastModified = :time
2315     WHERE id IN ancestors
2316    `, { guid: folderGuid, type: Bookmarks.TYPE_FOLDER,
2317         time: PlacesUtils.toPRTime(time) });
2318
2319  if (syncChangeDelta) {
2320    // Flag the folder as having a change.
2321    await db.executeCached(`
2322      UPDATE moz_bookmarks SET
2323        syncChangeCounter = syncChangeCounter + :syncChangeDelta
2324      WHERE guid = :guid`,
2325      { guid: folderGuid, syncChangeDelta });
2326  }
2327};
2328
2329/**
2330 * Remove all descendants of one or more bookmark folders.
2331 *
2332 * @param {Object} db
2333 *        the Sqlite.jsm connection handle.
2334 * @param {Array} folderGuids
2335 *        array of folder guids.
2336 * @return {Array}
2337 *         An array of urls that will need to be updated for frecency. These
2338 *         are returned rather than updated immediately so that the caller
2339 *         can decide when they need to be updated - they do not need to
2340 *         stop this function from completing.
2341 */
2342var removeFoldersContents =
2343async function(db, folderGuids, options) {
2344  let syncChangeDelta =
2345    PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
2346
2347  let itemsRemoved = [];
2348  for (let folderGuid of folderGuids) {
2349    let rows = await db.executeCached(
2350      `WITH RECURSIVE
2351       descendants(did) AS (
2352         SELECT b.id FROM moz_bookmarks b
2353         JOIN moz_bookmarks p ON b.parent = p.id
2354         WHERE p.guid = :folderGuid
2355         UNION ALL
2356         SELECT id FROM moz_bookmarks
2357         JOIN descendants ON parent = did
2358       )
2359       SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
2360              b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
2361              b.lastModified, IFNULL(b.title, "") AS title,
2362              p.parent AS _grandParentId, NULL AS _childCount,
2363              b.syncStatus AS _syncStatus
2364       FROM descendants
2365       /* The usage of CROSS JOIN is not random, it tells the optimizer
2366          to retain the original rows order, so the hierarchy is respected */
2367       CROSS JOIN moz_bookmarks b ON did = b.id
2368       JOIN moz_bookmarks p ON p.id = b.parent
2369       LEFT JOIN moz_places h ON b.fk = h.id`, { folderGuid });
2370
2371    itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
2372
2373    await db.executeCached(
2374      `WITH RECURSIVE
2375       descendants(did) AS (
2376         SELECT b.id FROM moz_bookmarks b
2377         JOIN moz_bookmarks p ON b.parent = p.id
2378         WHERE p.guid = :folderGuid
2379         UNION ALL
2380         SELECT id FROM moz_bookmarks
2381         JOIN descendants ON parent = did
2382       )
2383       DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid });
2384  }
2385
2386  // Write tombstones for removed items.
2387  await insertTombstones(db, itemsRemoved, syncChangeDelta);
2388
2389  // Bump the change counter for all tagged bookmarks when removing tag
2390  // folders.
2391  await addSyncChangesForRemovedTagFolders(db, itemsRemoved, syncChangeDelta);
2392
2393  // Cleanup orphans.
2394  await removeOrphanAnnotations(db);
2395
2396  // TODO (Bug 1087576): this may leave orphan tags behind.
2397
2398  // Send onItemRemoved notifications to listeners.
2399  // TODO (Bug 1087580): for the case of eraseEverything, this should send a
2400  // single clear bookmarks notification rather than notifying for each
2401  // bookmark.
2402
2403  // Notify listeners in reverse order to serve children before parents.
2404  let { source = Bookmarks.SOURCES.DEFAULT } = options;
2405  let observers = PlacesUtils.bookmarks.getObservers();
2406  for (let item of itemsRemoved.reverse()) {
2407    let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
2408    notify(observers, "onItemRemoved", [ item._id, item._parentId,
2409                                         item.index, item.type, uri,
2410                                         item.guid, item.parentGuid,
2411                                         source ],
2412                                       // Notify observers that this item is being
2413                                       // removed as a descendent.
2414                                       {
2415                                         isDescendantRemoval: true,
2416                                         parentGuid: item.parentGuid
2417                                       });
2418
2419    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
2420    if (isUntagging) {
2421      for (let entry of (await fetchBookmarksByURL(item, true))) {
2422        notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
2423                                             PlacesUtils.toPRTime(entry.lastModified),
2424                                             entry.type, entry._parentId,
2425                                             entry.guid, entry.parentGuid,
2426                                             "", source ]);
2427      }
2428    }
2429  }
2430  return itemsRemoved.filter(item => "url" in item).map(item => item.url);
2431};
2432
2433/**
2434 * Tries to insert a new place if it doesn't exist yet.
2435 * @param url
2436 *        A valid URL object.
2437 * @return {Promise} resolved when the operation is complete.
2438 */
2439async function maybeInsertPlace(db, url) {
2440  // The IGNORE conflict can trigger on `guid`.
2441  await db.executeCached(
2442    `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid)
2443     VALUES (:url, hash(:url), :rev_host, 0, :frecency,
2444             IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
2445                    GENERATE_GUID()))
2446    `, { url: url.href,
2447         rev_host: PlacesUtils.getReversedHost(url),
2448         frecency: url.protocol == "place:" ? 0 : -1 });
2449  await db.executeCached("DELETE FROM moz_updatehostsinsert_temp");
2450}
2451
2452/**
2453 * Tries to insert a new place if it doesn't exist yet.
2454 * @param db
2455 *        The database to use
2456 * @param urls
2457 *        An array with all the url objects to insert.
2458 * @return {Promise} resolved when the operation is complete.
2459 */
2460async function maybeInsertManyPlaces(db, urls) {
2461  await db.executeCached(
2462    `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid) VALUES
2463     (:url, hash(:url), :rev_host, 0, :frecency,
2464     IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url), :maybeguid))`,
2465     urls.map(url => ({
2466       url: url.href,
2467       rev_host: PlacesUtils.getReversedHost(url),
2468       frecency: url.protocol == "place:" ? 0 : -1,
2469       maybeguid: PlacesUtils.history.makeGuid(),
2470     })));
2471  await db.executeCached("DELETE FROM moz_updatehostsinsert_temp");
2472}
2473
2474// Indicates whether we should write a tombstone for an item that has been
2475// uploaded to the server. We ignore "NEW" and "UNKNOWN" items: "NEW" items
2476// haven't been uploaded yet, and "UNKNOWN" items need a full reconciliation
2477// with the server.
2478function needsTombstone(item) {
2479  return item._syncStatus == Bookmarks.SYNC_STATUS.NORMAL;
2480}
2481
2482// Inserts tombstones for removed synced items.
2483function insertTombstones(db, itemsRemoved, syncChangeDelta) {
2484  if (!syncChangeDelta) {
2485    return Promise.resolve();
2486  }
2487  let syncedItems = itemsRemoved.filter(needsTombstone);
2488  if (!syncedItems.length) {
2489    return Promise.resolve();
2490  }
2491  let dateRemoved = PlacesUtils.toPRTime(Date.now());
2492  let valuesTable = syncedItems.map(item => `(
2493    ${JSON.stringify(item.guid)},
2494    ${dateRemoved}
2495  )`).join(",");
2496  return db.execute(`
2497    INSERT INTO moz_bookmarks_deleted (guid, dateRemoved)
2498    VALUES ${valuesTable}`
2499  );
2500}
2501
2502// Bumps the change counter for all bookmarks with URLs referenced in removed
2503// tag folders.
2504var addSyncChangesForRemovedTagFolders = async function(db, itemsRemoved, syncChangeDelta) {
2505  if (!syncChangeDelta) {
2506    return;
2507  }
2508  for (let item of itemsRemoved) {
2509    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
2510    if (isUntagging) {
2511      await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
2512        db, item.url, syncChangeDelta);
2513    }
2514  }
2515};
2516
2517// Bumps the change counter for all bookmarked URLs within `folders`.
2518// This is used to update tagged bookmarks when changing a tag folder.
2519function addSyncChangesForBookmarksInFolder(db, folder, syncChangeDelta) {
2520  if (!syncChangeDelta) {
2521    return Promise.resolve();
2522  }
2523  return db.execute(`
2524    UPDATE moz_bookmarks SET
2525      syncChangeCounter = syncChangeCounter + :syncChangeDelta
2526    WHERE type = :type AND
2527          fk = (SELECT fk FROM moz_bookmarks WHERE parent = :parent)
2528    `,
2529    { syncChangeDelta, type: Bookmarks.TYPE_BOOKMARK, parent: folder._id });
2530}
2531
2532function adjustSeparatorsSyncCounter(db, parentId, startIndex, syncChangeDelta) {
2533  if (!syncChangeDelta) {
2534    return Promise.resolve();
2535  }
2536
2537  return db.executeCached(`
2538    UPDATE moz_bookmarks
2539    SET syncChangeCounter = syncChangeCounter + :delta
2540    WHERE parent = :parent AND position >= :start_index
2541      AND type = :item_type
2542    `,
2543    {
2544      delta: syncChangeDelta,
2545      parent: parentId,
2546      start_index: startIndex,
2547      item_type: Bookmarks.TYPE_SEPARATOR
2548    });
2549}
2550
2551function* chunkArray(array, chunkLength) {
2552  if (array.length <= chunkLength) {
2553    yield array;
2554    return;
2555  }
2556  let startIndex = 0;
2557  while (startIndex < array.length) {
2558    yield array.slice(startIndex, startIndex += chunkLength);
2559  }
2560}
2561
2562/**
2563 * Convert a list of strings or numbers to its SQL
2564 * representation as a string.
2565 */
2566function sqlList(list) {
2567  return list.map(JSON.stringify).join();
2568}
2569