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 bookmark-moved 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
62const { XPCOMUtils } = ChromeUtils.import(
63  "resource://gre/modules/XPCOMUtils.jsm"
64);
65
66XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
67
68ChromeUtils.defineModuleGetter(
69  this,
70  "NetUtil",
71  "resource://gre/modules/NetUtil.jsm"
72);
73ChromeUtils.defineModuleGetter(
74  this,
75  "PlacesUtils",
76  "resource://gre/modules/PlacesUtils.jsm"
77);
78ChromeUtils.defineModuleGetter(
79  this,
80  "PlacesSyncUtils",
81  "resource://gre/modules/PlacesSyncUtils.jsm"
82);
83
84// This is an helper to temporarily cover the need to know the tags folder
85// itemId until bug 424160 is fixed.  This exists so that startup paths won't
86// pay the price to initialize the bookmarks service just to fetch this value.
87// If the method is already initing the bookmarks service for other reasons
88// (most of the writing methods will invoke getObservers() already) it can
89// directly use the PlacesUtils.tagsFolderId property.
90var gTagsFolderId;
91async function promiseTagsFolderId() {
92  if (gTagsFolderId) {
93    return gTagsFolderId;
94  }
95  let db = await PlacesUtils.promiseDBConnection();
96  let rows = await db.execute(
97    "SELECT id FROM moz_bookmarks WHERE guid = :guid",
98    { guid: Bookmarks.tagsGuid }
99  );
100  return (gTagsFolderId = rows[0].getResultByName("id"));
101}
102
103const MATCH_ANYWHERE_UNMODIFIED =
104  Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
105const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
106
107var Bookmarks = Object.freeze({
108  /**
109   * Item's type constants.
110   * These should stay consistent with nsINavBookmarksService.idl
111   */
112  TYPE_BOOKMARK: 1,
113  TYPE_FOLDER: 2,
114  TYPE_SEPARATOR: 3,
115
116  /**
117   * Sync status constants, stored for each item.
118   */
119  SYNC_STATUS: {
120    UNKNOWN: Ci.nsINavBookmarksService.SYNC_STATUS_UNKNOWN,
121    NEW: Ci.nsINavBookmarksService.SYNC_STATUS_NEW,
122    NORMAL: Ci.nsINavBookmarksService.SYNC_STATUS_NORMAL,
123  },
124
125  /**
126   * Default index used to append a bookmark-item at the end of a folder.
127   * This should stay consistent with nsINavBookmarksService.idl
128   */
129  DEFAULT_INDEX: -1,
130
131  /**
132   * Maximum length of a tag.
133   * Any tag above this length is rejected.
134   */
135  MAX_TAG_LENGTH: 100,
136
137  /**
138   * Bookmark change source constants, passed as optional properties and
139   * forwarded to observers. See nsINavBookmarksService.idl for an explanation.
140   */
141  SOURCES: {
142    DEFAULT: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
143    SYNC: Ci.nsINavBookmarksService.SOURCE_SYNC,
144    IMPORT: Ci.nsINavBookmarksService.SOURCE_IMPORT,
145    SYNC_REPARENT_REMOVED_FOLDER_CHILDREN:
146      Ci.nsINavBookmarksService.SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
147    RESTORE: Ci.nsINavBookmarksService.SOURCE_RESTORE,
148    RESTORE_ON_STARTUP: Ci.nsINavBookmarksService.SOURCE_RESTORE_ON_STARTUP,
149  },
150
151  /**
152   * Special GUIDs associated with bookmark roots.
153   * It's guaranteed that the roots will always have these guids.
154   */
155  rootGuid: "root________",
156  menuGuid: "menu________",
157  toolbarGuid: "toolbar_____",
158  unfiledGuid: "unfiled_____",
159  mobileGuid: "mobile______",
160
161  // With bug 424160, tags will stop being bookmarks, thus this root will
162  // be removed.  Do not rely on this, rather use the tagging service API.
163  tagsGuid: "tags________",
164
165  /**
166   * The GUIDs of the user content root folders that we support, for easy access
167   * as a set.
168   */
169  userContentRoots: [
170    "toolbar_____",
171    "menu________",
172    "unfiled_____",
173    "mobile______",
174  ],
175
176  /**
177   * GUIDs associated with virtual queries that are used for displaying bookmark
178   * folders in the left pane.
179   */
180  virtualMenuGuid: "menu_______v",
181  virtualToolbarGuid: "toolbar____v",
182  virtualUnfiledGuid: "unfiled____v",
183  virtualMobileGuid: "mobile_____v",
184
185  /**
186   * Checks if a guid is a virtual root.
187   *
188   * @param {String} guid The guid of the item to look for.
189   * @returns {Boolean} true if guid is a virtual root, false otherwise.
190   */
191  isVirtualRootItem(guid) {
192    return (
193      guid == PlacesUtils.bookmarks.virtualMenuGuid ||
194      guid == PlacesUtils.bookmarks.virtualToolbarGuid ||
195      guid == PlacesUtils.bookmarks.virtualUnfiledGuid ||
196      guid == PlacesUtils.bookmarks.virtualMobileGuid
197    );
198  },
199
200  /**
201   * Returns the title to use on the UI for a bookmark item. Root folders
202   * in the database don't store fully localised versions of the title. To
203   * get those this function should be called.
204   *
205   * Hence, this function should only be called if a root folder object is
206   * likely to be displayed to the user.
207   *
208   * @param {Object} info An object representing a bookmark-item.
209   * @returns {String} The correct string.
210   * @throws {Error} If the guid in PlacesUtils.bookmarks.userContentRoots is
211   *                 not supported.
212   */
213  getLocalizedTitle(info) {
214    if (!PlacesUtils.bookmarks.userContentRoots.includes(info.guid)) {
215      return info.title;
216    }
217
218    switch (info.guid) {
219      case PlacesUtils.bookmarks.toolbarGuid:
220        return PlacesUtils.getString("BookmarksToolbarFolderTitle");
221      case PlacesUtils.bookmarks.menuGuid:
222        return PlacesUtils.getString("BookmarksMenuFolderTitle");
223      case PlacesUtils.bookmarks.unfiledGuid:
224        return PlacesUtils.getString("OtherBookmarksFolderTitle");
225      case PlacesUtils.bookmarks.mobileGuid:
226        return PlacesUtils.getString("MobileBookmarksFolderTitle");
227      default:
228        throw new Error(
229          `Unsupported guid ${info.guid} passed to getLocalizedTitle!`
230        );
231    }
232  },
233
234  /**
235   * Inserts a bookmark-item into the bookmarks tree.
236   *
237   * For creating a bookmark, the following set of properties is required:
238   *  - type
239   *  - parentGuid
240   *  - url, only for bookmarked URLs
241   *
242   * If an index is not specified, it defaults to appending.
243   * It's also possible to pass a non-existent GUID to force creation of an
244   * item with the given GUID, but unless you have a very sound reason, such as
245   * an undo manager implementation or synchronization, don't do that.
246   *
247   * Note that any known properties that don't apply to the specific item type
248   * cause an exception.
249   *
250   * @param info
251   *        object representing a bookmark-item.
252   *
253   * @return {Promise} resolved when the creation is complete.
254   * @resolves to an object representing the created bookmark.
255   * @rejects if it's not possible to create the requested bookmark.
256   * @throws if the arguments are invalid.
257   */
258  insert(info) {
259    let now = new Date();
260    let addedTime = (info && info.dateAdded) || now;
261    let modTime = addedTime;
262    if (addedTime > now) {
263      modTime = now;
264    }
265    let insertInfo = validateBookmarkObject("Bookmarks.jsm: insert", info, {
266      type: { defaultValue: this.TYPE_BOOKMARK },
267      index: { defaultValue: this.DEFAULT_INDEX },
268      url: {
269        requiredIf: b => b.type == this.TYPE_BOOKMARK,
270        validIf: b => b.type == this.TYPE_BOOKMARK,
271      },
272      parentGuid: {
273        required: true,
274        // Inserting into the root folder is not allowed.
275        validIf: b => b.parentGuid != this.rootGuid,
276      },
277      title: {
278        defaultValue: "",
279        validIf: b =>
280          b.type == this.TYPE_BOOKMARK ||
281          b.type == this.TYPE_FOLDER ||
282          b.title === "",
283      },
284      dateAdded: { defaultValue: addedTime },
285      lastModified: {
286        defaultValue: modTime,
287        validIf: b =>
288          b.lastModified >= now ||
289          (b.dateAdded && b.lastModified >= b.dateAdded),
290      },
291      source: { defaultValue: this.SOURCES.DEFAULT },
292    });
293
294    return (async () => {
295      // Ensure the parent exists.
296      let parent = await fetchBookmark({ guid: insertInfo.parentGuid });
297      if (!parent) {
298        throw new Error("parentGuid must be valid");
299      }
300
301      // Set index in the appending case.
302      if (
303        insertInfo.index == this.DEFAULT_INDEX ||
304        insertInfo.index > parent._childCount
305      ) {
306        insertInfo.index = parent._childCount;
307      }
308
309      let item = await insertBookmark(insertInfo, parent);
310
311      // We need the itemId to notify, though once the switch to guids is
312      // complete we may stop using it.
313      let itemId = await PlacesUtils.promiseItemId(item.guid);
314
315      // Pass tagging information for the observers to skip over these notifications when needed.
316      let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
317      let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
318      let url = "";
319      if (item.type == Bookmarks.TYPE_BOOKMARK) {
320        url = item.url.href;
321      }
322
323      const notifications = [
324        new PlacesBookmarkAddition({
325          id: itemId,
326          url,
327          itemType: item.type,
328          parentId: parent._id,
329          index: item.index,
330          title: item.title,
331          dateAdded: item.dateAdded,
332          guid: item.guid,
333          parentGuid: item.parentGuid,
334          source: item.source,
335          isTagging: isTagging || isTagsFolder,
336        }),
337      ];
338
339      // If it's a tag, notify bookmark-tags-changed event to all bookmarks for this URL.
340      if (isTagging) {
341        const tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(url));
342
343        for (let entry of await fetchBookmarksByURL(item, {
344          concurrent: true,
345        })) {
346          notifications.push(
347            new PlacesBookmarkTags({
348              id: entry._id,
349              itemType: entry.type,
350              url,
351              guid: entry.guid,
352              parentGuid: entry.parentGuid,
353              tags,
354              lastModified: entry.lastModified,
355              source: item.source,
356              isTagging: false,
357            })
358          );
359        }
360      }
361
362      PlacesObservers.notifyListeners(notifications);
363
364      // Remove non-enumerable properties.
365      delete item.source;
366      return Object.assign({}, item);
367    })();
368  },
369
370  /**
371   * Inserts a bookmark-tree into the existing bookmarks tree.
372   *
373   * All the specified folders and bookmarks will be inserted as new, even
374   * if duplicates. There's no merge support at this time.
375   *
376   * The input should be of the form:
377   * {
378   *   guid: "<some-existing-guid-to-use-as-parent>",
379   *   source: "<some valid source>", (optional)
380   *   children: [
381   *     ... valid bookmark objects.
382   *   ]
383   * }
384   *
385   * Children will be appended to any existing children of the parent
386   * that is specified. The source specified on the root of the tree
387   * will be used for all the items inserted. Any indices or custom parentGuids
388   * set on children will be ignored and overwritten.
389   *
390   * @param {Object} tree
391   *        object representing a tree of bookmark items to insert.
392   * @param {Object} options [optional]
393   *        object with properties representing options.  Current options are:
394   *         - fixupOrSkipInvalidEntries: makes the insert more lenient to
395   *           mistakes in the input tree.  Properties of an entry that are
396   *           fixable will be corrected, otherwise the entry will be skipped.
397   *           This is particularly convenient for import/restore operations,
398   *           but should not be abused for common inserts, since it may hide
399   *           bugs in the calling code.
400   *
401   * @return {Promise} resolved when the creation is complete.
402   * @resolves to an array of objects representing the created bookmark(s).
403   * @rejects if it's not possible to create the requested bookmark.
404   * @throws if the arguments are invalid.
405   */
406  insertTree(tree, options) {
407    if (!tree || typeof tree != "object") {
408      throw new Error("Should be provided a valid tree object.");
409    }
410    if (!Array.isArray(tree.children) || !tree.children.length) {
411      throw new Error("Should have a non-zero number of children to insert.");
412    }
413    if (!PlacesUtils.isValidGuid(tree.guid)) {
414      throw new Error(
415        `The parent guid is not valid (${tree.guid} ${tree.title}).`
416      );
417    }
418    if (tree.guid == this.rootGuid) {
419      throw new Error("Can't insert into the root.");
420    }
421    if (tree.guid == this.tagsGuid) {
422      throw new Error("Can't use insertTree to insert tags.");
423    }
424    if (
425      tree.hasOwnProperty("source") &&
426      !Object.values(this.SOURCES).includes(tree.source)
427    ) {
428      throw new Error("Can't use source value " + tree.source);
429    }
430    if (options && typeof options != "object") {
431      throw new Error("Options should be a valid object");
432    }
433    let fixupOrSkipInvalidEntries =
434      options && !!options.fixupOrSkipInvalidEntries;
435
436    // Serialize the tree into an array of items to insert into the db.
437    let insertInfos = [];
438    let urlsThatMightNeedPlaces = [];
439
440    // We want to use the same 'last added' time for all the entries
441    // we import (so they won't differ by a few ms based on where
442    // they are in the tree, and so we don't needlessly construct
443    // multiple dates).
444    let fallbackLastAdded = new Date();
445
446    const { TYPE_BOOKMARK, TYPE_FOLDER, SOURCES } = this;
447
448    // Reuse the 'source' property for all the entries.
449    let source = tree.source || SOURCES.DEFAULT;
450
451    // This is recursive.
452    function appendInsertionInfoForInfoArray(infos, indexToUse, parentGuid) {
453      // We want to keep the index of items that will be inserted into the root
454      // NULL, and then use a subquery to select the right index, to avoid
455      // races where other consumers might add items between when we determine
456      // the index and when we insert. However, the validator does not allow
457      // NULL values in in the index, so we fake it while validating and then
458      // correct later. Keep track of whether we're doing this:
459      let shouldUseNullIndices = false;
460      if (indexToUse === null) {
461        shouldUseNullIndices = true;
462        indexToUse = 0;
463      }
464
465      // When a folder gets an item added, its last modified date is updated
466      // to be equal to the date we added the item (if that date is newer).
467      // Because we're inserting a tree, we keep track of this date for the
468      // loop, updating it for inserted items as well as from any subfolders
469      // we insert.
470      let lastAddedForParent = new Date(0);
471      for (let info of infos) {
472        // Ensure to use the same date for dateAdded and lastModified, even if
473        // dateAdded may be imposed by the caller.
474        let time = (info && info.dateAdded) || fallbackLastAdded;
475        let insertInfo = {
476          guid: { defaultValue: PlacesUtils.history.makeGuid() },
477          type: { defaultValue: TYPE_BOOKMARK },
478          url: {
479            requiredIf: b => b.type == TYPE_BOOKMARK,
480            validIf: b => b.type == TYPE_BOOKMARK,
481          },
482          parentGuid: { replaceWith: parentGuid }, // Set the correct parent guid.
483          title: {
484            defaultValue: "",
485            validIf: b =>
486              b.type == TYPE_BOOKMARK ||
487              b.type == TYPE_FOLDER ||
488              b.title === "",
489          },
490          dateAdded: {
491            defaultValue: time,
492            validIf: b => !b.lastModified || b.dateAdded <= b.lastModified,
493          },
494          lastModified: {
495            defaultValue: time,
496            validIf: b =>
497              (!b.dateAdded && b.lastModified >= time) ||
498              (b.dateAdded && b.lastModified >= b.dateAdded),
499          },
500          index: { replaceWith: indexToUse++ },
501          source: { replaceWith: source },
502          keyword: { validIf: b => b.type == TYPE_BOOKMARK },
503          charset: { validIf: b => b.type == TYPE_BOOKMARK },
504          postData: { validIf: b => b.type == TYPE_BOOKMARK },
505          tags: { validIf: b => b.type == TYPE_BOOKMARK },
506          children: {
507            validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children),
508          },
509        };
510        if (fixupOrSkipInvalidEntries) {
511          insertInfo.guid.fixup = b =>
512            (b.guid = PlacesUtils.history.makeGuid());
513          insertInfo.dateAdded.fixup = insertInfo.lastModified.fixup = b =>
514            (b.lastModified = b.dateAdded = fallbackLastAdded);
515        }
516        try {
517          insertInfo = validateBookmarkObject(
518            "Bookmarks.jsm: insertTree",
519            info,
520            insertInfo
521          );
522        } catch (ex) {
523          if (fixupOrSkipInvalidEntries) {
524            indexToUse--;
525            continue;
526          } else {
527            throw ex;
528          }
529        }
530
531        if (shouldUseNullIndices) {
532          insertInfo.index = null;
533        }
534        // Store the URL if this is a bookmark, so we can ensure we create an
535        // entry in moz_places for it.
536        if (insertInfo.type == Bookmarks.TYPE_BOOKMARK) {
537          urlsThatMightNeedPlaces.push(insertInfo.url);
538        }
539
540        insertInfos.push(insertInfo);
541        // Process any children. We have to use info.children here rather than
542        // insertInfo.children because validateBookmarkObject doesn't copy over
543        // the children ref, as the default bookmark validators object doesn't
544        // know about children.
545        if (info.children) {
546          // start children of this item off at index 0.
547          let childrenLastAdded = appendInsertionInfoForInfoArray(
548            info.children,
549            0,
550            insertInfo.guid
551          );
552          if (childrenLastAdded > insertInfo.lastModified) {
553            insertInfo.lastModified = childrenLastAdded;
554          }
555          if (childrenLastAdded > lastAddedForParent) {
556            lastAddedForParent = childrenLastAdded;
557          }
558        }
559
560        // Ensure we track what time to update the parent to.
561        if (insertInfo.dateAdded > lastAddedForParent) {
562          lastAddedForParent = insertInfo.dateAdded;
563        }
564      }
565      return lastAddedForParent;
566    }
567
568    // We want to validate synchronously, but we can't know the index at which
569    // we're inserting into the parent. We just use NULL instead,
570    // and the SQL query with which we insert will update it as necessary.
571    let lastAddedForParent = appendInsertionInfoForInfoArray(
572      tree.children,
573      null,
574      tree.guid
575    );
576
577    // appendInsertionInfoForInfoArray will remove invalid items and may leave
578    // us with nothing to insert, if so, just return early.
579    if (!insertInfos.length) {
580      return [];
581    }
582
583    return (async function() {
584      let treeParent = await fetchBookmark({ guid: tree.guid });
585      if (!treeParent) {
586        throw new Error("The parent you specified doesn't exist.");
587      }
588
589      if (treeParent._parentId == PlacesUtils.tagsFolderId) {
590        throw new Error("Can't use insertTree to insert tags.");
591      }
592
593      await insertBookmarkTree(
594        insertInfos,
595        source,
596        treeParent,
597        urlsThatMightNeedPlaces,
598        lastAddedForParent
599      );
600
601      // Now update the indices of root items in the objects we return.
602      // These may be wrong if someone else modified the table between
603      // when we fetched the parent and inserted our items, but the actual
604      // inserts will have been correct, and we don't want to query the DB
605      // again if we don't have to. bug 1347230 covers improving this.
606      let rootIndex = treeParent._childCount;
607      for (let insertInfo of insertInfos) {
608        if (insertInfo.parentGuid == tree.guid) {
609          insertInfo.index += rootIndex++;
610        }
611      }
612      // We need the itemIds to notify, though once the switch to guids is
613      // complete we may stop using them.
614      let itemIdMap = await PlacesUtils.promiseManyItemIds(
615        insertInfos.map(info => info.guid)
616      );
617
618      let notifications = [];
619      for (let i = 0; i < insertInfos.length; i++) {
620        let item = insertInfos[i];
621        let itemId = itemIdMap.get(item.guid);
622        // For sub-folders, we need to make sure their children have the correct parent ids.
623        let parentId;
624        if (item.parentGuid === treeParent.guid) {
625          // This is a direct child of the tree parent, so we can use the
626          // existing parent's id.
627          parentId = treeParent._id;
628        } else {
629          // This is a parent folder that's been updated, so we need to
630          // use the new item id.
631          parentId = itemIdMap.get(item.parentGuid);
632        }
633
634        let url = "";
635        if (item.type == Bookmarks.TYPE_BOOKMARK) {
636          url = item.url instanceof URL ? item.url.href : item.url;
637        }
638
639        notifications.push(
640          new PlacesBookmarkAddition({
641            id: itemId,
642            url,
643            itemType: item.type,
644            parentId,
645            index: item.index,
646            title: item.title,
647            dateAdded: item.dateAdded,
648            guid: item.guid,
649            parentGuid: item.parentGuid,
650            source: item.source,
651            isTagging: false,
652          })
653        );
654
655        try {
656          await handleBookmarkItemSpecialData(itemId, item);
657        } catch (ex) {
658          // This is not critical, regardless the bookmark has been created
659          // and we should continue notifying the next ones.
660          Cu.reportError(
661            `An error occured while handling special bookmark data: ${ex}`
662          );
663        }
664
665        // Remove non-enumerable properties.
666        delete item.source;
667
668        insertInfos[i] = Object.assign({}, item);
669      }
670
671      if (notifications.length) {
672        PlacesObservers.notifyListeners(notifications);
673      }
674
675      return insertInfos;
676    })();
677  },
678
679  /**
680   * Updates a bookmark-item.
681   *
682   * Only set the properties which should be changed (undefined properties
683   * won't be taken into account).
684   * Moreover, the item's type or dateAdded cannot be changed, since they are
685   * immutable after creation.  Trying to change them will reject.
686   *
687   * Note that any known properties that don't apply to the specific item type
688   * cause an exception.
689   *
690   * @param info
691   *        object representing a bookmark-item, as defined above.
692   *
693   * @return {Promise} resolved when the update is complete.
694   * @resolves to an object representing the updated bookmark.
695   * @rejects if it's not possible to update the given bookmark.
696   * @throws if the arguments are invalid.
697   */
698  update(info) {
699    // The info object is first validated here to ensure it's consistent, then
700    // it's compared to the existing item to remove any properties that don't
701    // need to be updated.
702    let updateInfo = validateBookmarkObject("Bookmarks.jsm: update", info, {
703      guid: { required: true },
704      index: {
705        requiredIf: b => b.hasOwnProperty("parentGuid"),
706        validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX,
707      },
708      parentGuid: { validIf: b => b.parentGuid != this.rootGuid },
709      source: { defaultValue: this.SOURCES.DEFAULT },
710    });
711
712    // There should be at last one more property in addition to guid and source.
713    if (Object.keys(updateInfo).length < 3) {
714      throw new Error("Not enough properties to update");
715    }
716
717    return (async () => {
718      // Ensure the item exists.
719      let item = await fetchBookmark(updateInfo);
720      if (!item) {
721        throw new Error("No bookmarks found for the provided GUID");
722      }
723      if (updateInfo.hasOwnProperty("type") && updateInfo.type != item.type) {
724        throw new Error("The bookmark type cannot be changed");
725      }
726
727      // Remove any property that will stay the same.
728      removeSameValueProperties(updateInfo, item);
729      // Check if anything should still be updated.
730      if (Object.keys(updateInfo).length < 3) {
731        // Remove non-enumerable properties.
732        return Object.assign({}, item);
733      }
734      const now = new Date();
735      let lastModifiedDefault = now;
736      // In the case where `dateAdded` is specified, but `lastModified` is not,
737      // we only update `lastModified` if it is older than the new `dateAdded`.
738      if (!("lastModified" in updateInfo) && "dateAdded" in updateInfo) {
739        lastModifiedDefault = new Date(
740          Math.max(item.lastModified, updateInfo.dateAdded)
741        );
742      }
743      updateInfo = validateBookmarkObject("Bookmarks.jsm: update", updateInfo, {
744        url: { validIf: () => item.type == this.TYPE_BOOKMARK },
745        title: {
746          validIf: () =>
747            [this.TYPE_BOOKMARK, this.TYPE_FOLDER].includes(item.type),
748        },
749        lastModified: {
750          defaultValue: lastModifiedDefault,
751          validIf: b =>
752            b.lastModified >= now ||
753            b.lastModified >= (b.dateAdded || item.dateAdded),
754        },
755        dateAdded: { defaultValue: item.dateAdded },
756      });
757
758      return PlacesUtils.withConnectionWrapper(
759        "Bookmarks.jsm: update",
760        async db => {
761          let parent;
762          if (updateInfo.hasOwnProperty("parentGuid")) {
763            if (PlacesUtils.isRootItem(item.guid)) {
764              throw new Error("It's not possible to move Places root folders.");
765            }
766            if (item.type == this.TYPE_FOLDER) {
767              // Make sure we are not moving a folder into itself or one of its
768              // descendants.
769              let rows = await db.executeCached(
770                `WITH RECURSIVE
771               descendants(did) AS (
772                 VALUES(:id)
773                 UNION ALL
774                 SELECT id FROM moz_bookmarks
775                 JOIN descendants ON parent = did
776                 WHERE type = :type
777               )
778               SELECT guid FROM moz_bookmarks
779               WHERE id IN descendants
780              `,
781                { id: item._id, type: this.TYPE_FOLDER }
782              );
783              if (
784                rows
785                  .map(r => r.getResultByName("guid"))
786                  .includes(updateInfo.parentGuid)
787              ) {
788                throw new Error(
789                  "Cannot insert a folder into itself or one of its descendants"
790                );
791              }
792            }
793
794            parent = await fetchBookmark({ guid: updateInfo.parentGuid });
795            if (!parent) {
796              throw new Error("No bookmarks found for the provided parentGuid");
797            }
798          }
799
800          if (updateInfo.hasOwnProperty("index")) {
801            if (PlacesUtils.isRootItem(item.guid)) {
802              throw new Error("It's not possible to move Places root folders.");
803            }
804            // If at this point we don't have a parent yet, we are moving into
805            // the same container.  Thus we know it exists.
806            if (!parent) {
807              parent = await fetchBookmark({ guid: item.parentGuid });
808            }
809
810            if (
811              updateInfo.index >= parent._childCount ||
812              updateInfo.index == this.DEFAULT_INDEX
813            ) {
814              updateInfo.index = parent._childCount;
815
816              // Fix the index when moving within the same container.
817              if (parent.guid == item.parentGuid) {
818                updateInfo.index--;
819              }
820            }
821          }
822
823          let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
824            info.source
825          );
826
827          let updatedItem = await db.executeTransaction(async function() {
828            let updatedItem = await updateBookmark(
829              db,
830              updateInfo,
831              item,
832              item.index,
833              parent,
834              syncChangeDelta
835            );
836            if (parent) {
837              await setAncestorsLastModified(
838                db,
839                parent.guid,
840                updatedItem.lastModified,
841                syncChangeDelta
842              );
843            }
844            return updatedItem;
845          });
846
847          if (
848            item.type == this.TYPE_BOOKMARK &&
849            item.url.href != updatedItem.url.href
850          ) {
851            // ...though we don't wait for the calculation.
852            updateFrecency(db, [item.url, updatedItem.url]).catch(
853              Cu.reportError
854            );
855          }
856
857          const notifications = [];
858
859          // For lastModified, we only care about the original input, since we
860          // should not notify implicit lastModified changes.
861          if (
862            (info.hasOwnProperty("lastModified") &&
863              updateInfo.hasOwnProperty("lastModified") &&
864              item.lastModified != updatedItem.lastModified) ||
865            (info.hasOwnProperty("dateAdded") &&
866              updateInfo.hasOwnProperty("dateAdded") &&
867              item.dateAdded != updatedItem.dateAdded)
868          ) {
869            let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
870            if (!isTagging) {
871              if (!parent) {
872                parent = await fetchBookmark({ guid: updatedItem.parentGuid });
873              }
874              isTagging = parent.parentGuid === Bookmarks.tagsGuid;
875            }
876
877            notifications.push(
878              new PlacesBookmarkTime({
879                id: updatedItem._id,
880                itemType: updatedItem.type,
881                url: updatedItem.url?.href,
882                guid: updatedItem.guid,
883                parentGuid: updatedItem.parentGuid,
884                dateAdded: updatedItem.dateAdded,
885                lastModified: updatedItem.lastModified,
886                source: updatedItem.source,
887                isTagging,
888              })
889            );
890          }
891
892          if (updateInfo.hasOwnProperty("title")) {
893            let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
894            if (!isTagging) {
895              if (!parent) {
896                parent = await fetchBookmark({ guid: updatedItem.parentGuid });
897              }
898              isTagging = parent.parentGuid === Bookmarks.tagsGuid;
899            }
900
901            notifications.push(
902              new PlacesBookmarkTitle({
903                id: updatedItem._id,
904                itemType: updatedItem.type,
905                url: updatedItem.url?.href,
906                guid: updatedItem.guid,
907                parentGuid: updatedItem.parentGuid,
908                title: updatedItem.title,
909                lastModified: updatedItem.lastModified,
910                source: updatedItem.source,
911                isTagging,
912              })
913            );
914
915            // If we're updating a tag, we must notify all the tagged bookmarks
916            // about the change.
917            if (isTagging) {
918              for (let entry of await fetchBookmarksByTags(
919                { tags: [updatedItem.title] },
920                { concurrent: true }
921              )) {
922                const tags = PlacesUtils.tagging.getTagsForURI(
923                  NetUtil.newURI(entry.url)
924                );
925                notifications.push(
926                  new PlacesBookmarkTags({
927                    id: entry._id,
928                    itemType: entry.type,
929                    url: entry.url,
930                    guid: entry.guid,
931                    parentGuid: entry.parentGuid,
932                    tags,
933                    lastModified: entry.lastModified,
934                    source: updatedItem.source,
935                    isTagging: false,
936                  })
937                );
938              }
939            }
940          }
941          if (updateInfo.hasOwnProperty("url")) {
942            await PlacesUtils.keywords.reassign(
943              item.url,
944              updatedItem.url,
945              updatedItem.source
946            );
947
948            let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
949            if (!isTagging) {
950              if (!parent) {
951                parent = await fetchBookmark({ guid: updatedItem.parentGuid });
952              }
953              isTagging = parent.parentGuid === Bookmarks.tagsGuid;
954            }
955
956            notifications.push(
957              new PlacesBookmarkUrl({
958                id: updatedItem._id,
959                itemType: updatedItem.type,
960                url: updatedItem.url.href,
961                guid: updatedItem.guid,
962                parentGuid: updatedItem.parentGuid,
963                source: updatedItem.source,
964                isTagging,
965                lastModified: updatedItem.lastModified,
966              })
967            );
968          }
969          // If the item was moved, notify bookmark-moved.
970          if (
971            item.parentGuid != updatedItem.parentGuid ||
972            item.index != updatedItem.index
973          ) {
974            notifications.push(
975              new PlacesBookmarkMoved({
976                id: updatedItem._id,
977                itemType: updatedItem.type,
978                url: updatedItem.url && updatedItem.url.href,
979                guid: updatedItem.guid,
980                parentGuid: updatedItem.parentGuid,
981                source: updatedItem.source,
982                index: updatedItem.index,
983                oldParentGuid: item.parentGuid,
984                oldIndex: item.index,
985                isTagging:
986                  updatedItem.parentGuid === Bookmarks.tagsGuid ||
987                  parent.parentGuid === Bookmarks.tagsGuid,
988              })
989            );
990          }
991
992          if (notifications.length) {
993            PlacesObservers.notifyListeners(notifications);
994          }
995
996          // Remove non-enumerable properties.
997          delete updatedItem.source;
998          return Object.assign({}, updatedItem);
999        }
1000      );
1001    })();
1002  },
1003
1004  /**
1005   * Moves multiple bookmark-items to a specific folder.
1006   *
1007   * If you are only updating/moving a single bookmark, use update() instead.
1008   *
1009   * @param {Array} guids
1010   *        An array of GUIDs representing the bookmarks to move.
1011   * @param {String} parentGuid
1012   *        Optional, the parent GUID to move the bookmarks to.
1013   * @param {Integer} index
1014   *        The index to move the bookmarks to. If this is -1, the bookmarks
1015   *        will be appended to the folder.
1016   * @param {Integer} source
1017   *        One of the Bookmarks.SOURCES.* options, representing the source of
1018   *        this change.
1019   *
1020   * @return {Promise} resolved when the move is complete.
1021   * @resolves to an array of objects representing the moved bookmarks.
1022   * @rejects if it's not possible to move the given bookmark(s).
1023   * @throws if the arguments are invalid.
1024   */
1025  moveToFolder(guids, parentGuid, index, source) {
1026    if (!Array.isArray(guids) || guids.length < 1) {
1027      throw new Error("guids should be an array of at least one item");
1028    }
1029    if (!guids.every(guid => PlacesUtils.isValidGuid(guid))) {
1030      throw new Error("Expected only valid GUIDs to be passed.");
1031    }
1032    if (parentGuid && !PlacesUtils.isValidGuid(parentGuid)) {
1033      throw new Error("parentGuid should be a valid GUID");
1034    }
1035    if (parentGuid == PlacesUtils.bookmarks.rootGuid) {
1036      throw new Error("Cannot move bookmarks into root.");
1037    }
1038    if (typeof index != "number" || index < this.DEFAULT_INDEX) {
1039      throw new Error(
1040        `index should be a number greater than ${this.DEFAULT_INDEX}`
1041      );
1042    }
1043
1044    if (!source) {
1045      source = this.SOURCES.DEFAULT;
1046    }
1047
1048    return (async () => {
1049      let updateInfos = [];
1050      let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
1051        source
1052      );
1053
1054      await PlacesUtils.withConnectionWrapper(
1055        "Bookmarks.jsm: moveToFolder",
1056        async db => {
1057          const lastModified = new Date();
1058
1059          let targetParentGuid = parentGuid || undefined;
1060
1061          for (let guid of guids) {
1062            // Ensure the item exists.
1063            let existingItem = await fetchBookmark({ guid }, { db });
1064            if (!existingItem) {
1065              throw new Error("No bookmarks found for the provided GUID");
1066            }
1067
1068            if (parentGuid) {
1069              // We're moving to a different folder.
1070              if (existingItem.type == this.TYPE_FOLDER) {
1071                // Make sure we are not moving a folder into itself or one of its
1072                // descendants.
1073                let rows = await db.executeCached(
1074                  `WITH RECURSIVE
1075                 descendants(did) AS (
1076                   VALUES(:id)
1077                   UNION ALL
1078                   SELECT id FROM moz_bookmarks
1079                   JOIN descendants ON parent = did
1080                   WHERE type = :type
1081                 )
1082                 SELECT guid FROM moz_bookmarks
1083                 WHERE id IN descendants
1084                `,
1085                  { id: existingItem._id, type: this.TYPE_FOLDER }
1086                );
1087                if (
1088                  rows.map(r => r.getResultByName("guid")).includes(parentGuid)
1089                ) {
1090                  throw new Error(
1091                    "Cannot insert a folder into itself or one of its descendants"
1092                  );
1093                }
1094              }
1095            } else if (!targetParentGuid) {
1096              targetParentGuid = existingItem.parentGuid;
1097            } else if (existingItem.parentGuid != targetParentGuid) {
1098              throw new Error(
1099                "All bookmarks should be in the same folder if no parent is specified"
1100              );
1101            }
1102
1103            updateInfos.push({ existingItem, currIndex: existingItem.index });
1104          }
1105
1106          let newParent = await fetchBookmark(
1107            { guid: targetParentGuid },
1108            { db }
1109          );
1110
1111          if (newParent._grandParentId == PlacesUtils.tagsFolderId) {
1112            throw new Error("Can't move to a tags folder");
1113          }
1114
1115          let newParentChildCount = newParent._childCount;
1116
1117          await db.executeTransaction(async () => {
1118            // Now that we have all the existing items, we can do the actual updates.
1119            for (let i = 0; i < updateInfos.length; i++) {
1120              let info = updateInfos[i];
1121              if (index != this.DEFAULT_INDEX) {
1122                // If we're dropping on the same folder, then we may need to adjust
1123                // the index to insert at the correct place.
1124                if (info.existingItem.parentGuid == newParent.guid) {
1125                  if (index > info.existingItem.index) {
1126                    // If we're dragging down, we need to go one lower to insert at
1127                    // the real point as moving the element changes the index of
1128                    // everything below by 1.
1129                    index--;
1130                  } else if (index == info.existingItem.index) {
1131                    // This isn't moving so we skip it, but copy the data so we have
1132                    // an easy way for the notifications to check.
1133                    info.updatedItem = { ...info.existingItem };
1134                    continue;
1135                  }
1136                }
1137              }
1138
1139              // Never let the index go higher than the max count of the folder.
1140              if (index == this.DEFAULT_INDEX || index >= newParentChildCount) {
1141                index = newParentChildCount;
1142
1143                // If this is moving within the same folder, then we need to drop the
1144                // index by one to compensate for "removing" it, then re-inserting.
1145                if (info.existingItem.parentGuid == newParent.guid) {
1146                  index--;
1147                }
1148              }
1149
1150              info.updatedItem = await updateBookmark(
1151                db,
1152                { lastModified, index },
1153                info.existingItem,
1154                info.currIndex,
1155                newParent,
1156                syncChangeDelta
1157              );
1158              info.newParent = newParent;
1159
1160              // For items moving within the same folder, we have to keep track
1161              // of their indexes. Otherwise we run the risk of not correctly
1162              // updating the indexes of other items in the folder.
1163              // This section simulates the database write in moveBookmark, which
1164              // allows us to avoid re-reading the database.
1165              if (info.existingItem.parentGuid == newParent.guid) {
1166                let sign = index < info.currIndex ? 1 : -1;
1167                for (let j = 0; j < updateInfos.length; j++) {
1168                  if (j == i) {
1169                    continue;
1170                  }
1171                  if (
1172                    updateInfos[j].currIndex >=
1173                      Math.min(info.currIndex, index) &&
1174                    updateInfos[j].currIndex <= Math.max(info.currIndex, index)
1175                  ) {
1176                    updateInfos[j].currIndex += sign;
1177                  }
1178                }
1179              }
1180              info.currIndex = index;
1181
1182              // We only bump the parent count if we're moving from a different folder.
1183              if (info.existingItem.parentGuid != newParent.guid) {
1184                newParentChildCount++;
1185              }
1186              index++;
1187            }
1188
1189            await setAncestorsLastModified(
1190              db,
1191              newParent.guid,
1192              lastModified,
1193              syncChangeDelta
1194            );
1195          });
1196        }
1197      );
1198
1199      const notifications = [];
1200
1201      // Updates complete, time to notify everyone.
1202      for (let { updatedItem, existingItem, newParent } of updateInfos) {
1203        // If the item was moved, notify bookmark-moved.
1204        // We use the updatedItem.index here, rather than currIndex, as the views
1205        // need to know where we inserted the item as opposed to where it ended
1206        // up.
1207        if (
1208          existingItem.parentGuid != updatedItem.parentGuid ||
1209          existingItem.index != updatedItem.index
1210        ) {
1211          notifications.push(
1212            new PlacesBookmarkMoved({
1213              id: updatedItem._id,
1214              itemType: updatedItem.type,
1215              url: existingItem.url,
1216              guid: updatedItem.guid,
1217              parentGuid: updatedItem.parentGuid,
1218              source,
1219              index: updatedItem.index,
1220              oldParentGuid: existingItem.parentGuid,
1221              oldIndex: existingItem.index,
1222              isTagging:
1223                updatedItem.parentGuid === Bookmarks.tagsGuid ||
1224                newParent.parentGuid === Bookmarks.tagsGuid,
1225            })
1226          );
1227        }
1228        // Remove non-enumerable properties.
1229        delete updatedItem.source;
1230      }
1231
1232      if (notifications.length) {
1233        PlacesObservers.notifyListeners(notifications);
1234      }
1235
1236      return updateInfos.map(updateInfo =>
1237        Object.assign({}, updateInfo.updatedItem)
1238      );
1239    })();
1240  },
1241
1242  /**
1243   * Removes one or more bookmark-items.
1244   *
1245   * @param guidOrInfo This may be:
1246   *        - The globally unique identifier of the item to remove
1247   *        - an object representing the item, as defined above
1248   *        - an array of objects representing the items to be removed
1249   * @param {Object} [options={}]
1250   *        Additional options that can be passed to the function.
1251   *        Currently supports the following properties:
1252   *         - preventRemovalOfNonEmptyFolders: Causes an exception to be
1253   *           thrown when attempting to remove a folder that is not empty.
1254   *         - source: The change source, forwarded to all bookmark observers.
1255   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1256   *
1257   * @return {Promise}
1258   * @resolves when the removal is complete
1259   * @rejects if the provided guid doesn't match any existing bookmark.
1260   * @throws if the arguments are invalid.
1261   */
1262  remove(guidOrInfo, options = {}) {
1263    let infos = guidOrInfo;
1264    if (!infos) {
1265      throw new Error("Input should be a valid object");
1266    }
1267    if (!Array.isArray(guidOrInfo)) {
1268      if (typeof guidOrInfo != "object") {
1269        infos = [{ guid: guidOrInfo }];
1270      } else {
1271        infos = [guidOrInfo];
1272      }
1273    }
1274
1275    if (!("source" in options)) {
1276      options.source = Bookmarks.SOURCES.DEFAULT;
1277    }
1278
1279    let removeInfos = [];
1280    for (let info of infos) {
1281      // Disallow removing the root folders.
1282      if (
1283        [
1284          Bookmarks.rootGuid,
1285          Bookmarks.menuGuid,
1286          Bookmarks.toolbarGuid,
1287          Bookmarks.unfiledGuid,
1288          Bookmarks.tagsGuid,
1289          Bookmarks.mobileGuid,
1290        ].includes(info.guid)
1291      ) {
1292        throw new Error("It's not possible to remove Places root folders.");
1293      }
1294
1295      // Even if we ignore any other unneeded property, we still validate any
1296      // known property to reduce likelihood of hidden bugs.
1297      let removeInfo = validateBookmarkObject("Bookmarks.jsm: remove", info);
1298      removeInfos.push(removeInfo);
1299    }
1300
1301    return (async function() {
1302      let removeItems = [];
1303      for (let info of removeInfos) {
1304        // We must be able to remove a bookmark even if it has an invalid url.
1305        // In that case the item won't have a url property.
1306        let item = await fetchBookmark(info, { ignoreInvalidURLs: true });
1307        if (!item) {
1308          throw new Error("No bookmarks found for the provided GUID.");
1309        }
1310
1311        removeItems.push(item);
1312      }
1313
1314      await removeBookmarks(removeItems, options);
1315
1316      // Notify bookmark-removed to listeners.
1317      let notifications = [];
1318
1319      for (let item of removeItems) {
1320        let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
1321        let url = "";
1322        if (item.type == Bookmarks.TYPE_BOOKMARK) {
1323          url = item.hasOwnProperty("url") ? item.url.href : null;
1324        }
1325
1326        notifications.push(
1327          new PlacesBookmarkRemoved({
1328            id: item._id,
1329            url,
1330            itemType: item.type,
1331            parentId: item._parentId,
1332            index: item.index,
1333            guid: item.guid,
1334            parentGuid: item.parentGuid,
1335            source: options.source,
1336            isTagging: isUntagging,
1337            isDescendantRemoval: false,
1338          })
1339        );
1340
1341        if (isUntagging) {
1342          const tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(url));
1343          for (let entry of await fetchBookmarksByURL(item, {
1344            concurrent: true,
1345          })) {
1346            notifications.push(
1347              new PlacesBookmarkTags({
1348                id: entry._id,
1349                itemType: entry.type,
1350                url,
1351                guid: entry.guid,
1352                parentGuid: entry.parentGuid,
1353                tags,
1354                lastModified: entry.lastModified,
1355                source: options.source,
1356                isTagging: false,
1357              })
1358            );
1359          }
1360        }
1361      }
1362
1363      PlacesObservers.notifyListeners(notifications);
1364    })();
1365  },
1366
1367  /**
1368   * Removes ALL bookmarks, resetting the bookmarks storage to an empty tree.
1369   *
1370   * Note that roots are preserved, only their children will be removed.
1371   *
1372   * @param {Object} [options={}]
1373   *        Additional options. Currently supports the following properties:
1374   *         - source: The change source, forwarded to all bookmark observers.
1375   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1376   *
1377   * @return {Promise} resolved when the removal is complete.
1378   * @resolves once the removal is complete.
1379   */
1380  eraseEverything(options = {}) {
1381    if (!options.source) {
1382      options.source = Bookmarks.SOURCES.DEFAULT;
1383    }
1384
1385    return PlacesUtils.withConnectionWrapper(
1386      "Bookmarks.jsm: eraseEverything",
1387      async function(db) {
1388        let urls;
1389
1390        await db.executeTransaction(async function() {
1391          urls = await removeFoldersContents(
1392            db,
1393            Bookmarks.userContentRoots,
1394            options
1395          );
1396          const time = PlacesUtils.toPRTime(new Date());
1397          const syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
1398            options.source
1399          );
1400          for (let folderGuid of Bookmarks.userContentRoots) {
1401            await db.executeCached(
1402              `UPDATE moz_bookmarks SET lastModified = :time,
1403                                        syncChangeCounter = syncChangeCounter + :syncChangeDelta
1404               WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
1405              `,
1406              { folderGuid, time, syncChangeDelta }
1407            );
1408          }
1409
1410          await PlacesSyncUtils.bookmarks.resetSyncMetadata(db, options.source);
1411        });
1412
1413        // We don't wait for the frecency calculation.
1414        if (urls && urls.length) {
1415          await PlacesUtils.keywords.eraseEverything();
1416          updateFrecency(db, urls).catch(Cu.reportError);
1417        }
1418      }
1419    );
1420  },
1421
1422  /**
1423   * Returns a list of recently bookmarked items.
1424   * Only includes actual bookmarks. Excludes folders, separators and queries.
1425   *
1426   * @param {integer} numberOfItems
1427   *        The maximum number of bookmark items to return.
1428   *
1429   * @return {Promise} resolved when the listing is complete.
1430   * @resolves to an array of recent bookmark-items.
1431   * @rejects if an error happens while querying.
1432   */
1433  getRecent(numberOfItems) {
1434    if (numberOfItems === undefined) {
1435      throw new Error("numberOfItems argument is required");
1436    }
1437    if (!typeof numberOfItems === "number" || numberOfItems % 1 !== 0) {
1438      throw new Error("numberOfItems argument must be an integer");
1439    }
1440    if (numberOfItems <= 0) {
1441      throw new Error("numberOfItems argument must be greater than zero");
1442    }
1443
1444    return fetchRecentBookmarks(numberOfItems);
1445  },
1446
1447  /**
1448   * Fetches information about a bookmark-item.
1449   *
1450   * REMARK: any successful call to this method resolves to a single
1451   *         bookmark-item (or null), even when multiple bookmarks may exist
1452   *         (e.g. fetching by url).  If you wish to retrieve all of the
1453   *         bookmarks for a given match, use the callback instead.
1454   *
1455   * Input can be either a guid or an object with one, and only one, of these
1456   * filtering properties set:
1457   *  - guid
1458   *      retrieves the item with the specified guid.
1459   *  - parentGuid and index
1460   *      retrieves the item by its position.
1461   *  - url
1462   *      retrieves the most recent bookmark having the given URL.
1463   *      To retrieve ALL of the bookmarks for that URL, you must pass in an
1464   *      onResult callback, that will be invoked once for each found bookmark.
1465   *  - guidPrefix
1466   *      retrieves the most recent item with the specified guid prefix.
1467   *      To retrieve ALL of the bookmarks for that guid prefix, you must pass
1468   *      in an onResult callback, that will be invoked once for each bookmark.
1469   *  - tags
1470   *      Retrieves the most recent item with all the specified tags.
1471   *      The tags are matched in a case-insensitive way.
1472   *      To retrieve ALL of the bookmarks having these tags, pass in an
1473   *      onResult callback, that will be invoked once for each bookmark.
1474   *      Note, there can be multiple bookmarks for the same url, if you need
1475   *      unique tagged urls you can filter duplicates by accumulating in a Set.
1476   *
1477   * @param guidOrInfo
1478   *        The globally unique identifier of the item to fetch, or an
1479   *        object representing it, as defined above.
1480   * @param onResult [optional]
1481   *        Callback invoked for each found bookmark.
1482   * @param options [optional]
1483   *        an optional object whose properties describe options for the fetch:
1484   *         - concurrent: fetches concurrently to any writes, returning results
1485   *                       faster. On the negative side, it may return stale
1486   *                       information missing the currently ongoing write.
1487   *         - includePath: additionally fetches the path for the bookmarks.
1488   *                        This is a potentially expensive operation.  When
1489   *                        set to true, the path property is set on results
1490   *                        containing an array of {title, guid} objects
1491   *                        ordered from root to leaf.
1492   *
1493   * @return {Promise} resolved when the fetch is complete.
1494   * @resolves to an object representing the found item, as described above, or
1495   *           an array of such objects.  if no item is found, the returned
1496   *           promise is resolved to null.
1497   * @rejects if an error happens while fetching.
1498   * @throws if the arguments are invalid.
1499   *
1500   * @note Any unknown property in the info object is ignored.  Known properties
1501   *       may be overwritten.
1502   */
1503  fetch(guidOrInfo, onResult = null, options = {}) {
1504    if (onResult && typeof onResult != "function") {
1505      throw new Error("onResult callback must be a valid function");
1506    }
1507    let info = guidOrInfo;
1508    if (!info) {
1509      throw new Error("Input should be a valid object");
1510    }
1511    if (typeof info != "object") {
1512      info = { guid: guidOrInfo };
1513    } else if (Object.keys(info).length == 1) {
1514      // Just a faster code path.
1515      if (
1516        !["url", "guid", "parentGuid", "index", "guidPrefix", "tags"].includes(
1517          Object.keys(info)[0]
1518        )
1519      ) {
1520        throw new Error(`Unexpected number of conditions provided: 0`);
1521      }
1522    } else {
1523      // Only one condition at a time can be provided.
1524      let conditionsCount = [
1525        v => v.hasOwnProperty("guid"),
1526        v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
1527        v => v.hasOwnProperty("url"),
1528        v => v.hasOwnProperty("guidPrefix"),
1529        v => v.hasOwnProperty("tags"),
1530      ].reduce((old, fn) => (old + fn(info)) | 0, 0);
1531      if (conditionsCount != 1) {
1532        throw new Error(
1533          `Unexpected number of conditions provided: ${conditionsCount}`
1534        );
1535      }
1536    }
1537
1538    // Create a new options object with just the support properties, because
1539    // we may augment it and hand it down to other methods.
1540    options = {
1541      concurrent: !!options.concurrent,
1542      includePath: !!options.includePath,
1543    };
1544
1545    let behavior = {};
1546    if (info.hasOwnProperty("parentGuid") || info.hasOwnProperty("index")) {
1547      behavior = {
1548        parentGuid: { requiredIf: b => b.hasOwnProperty("index") },
1549        index: {
1550          validIf: b =>
1551            (typeof b.index == "number" && b.index >= 0) ||
1552            b.index == this.DEFAULT_INDEX,
1553        },
1554      };
1555    }
1556
1557    // Even if we ignore any other unneeded property, we still validate any
1558    // known property to reduce likelihood of hidden bugs.
1559    let fetchInfo = validateBookmarkObject(
1560      "Bookmarks.jsm: fetch",
1561      info,
1562      behavior
1563    );
1564
1565    return (async function() {
1566      let results;
1567      if (fetchInfo.hasOwnProperty("url")) {
1568        results = await fetchBookmarksByURL(fetchInfo, options);
1569      } else if (fetchInfo.hasOwnProperty("guid")) {
1570        results = await fetchBookmark(fetchInfo, options);
1571      } else if (fetchInfo.hasOwnProperty("parentGuid")) {
1572        if (fetchInfo.hasOwnProperty("index")) {
1573          results = await fetchBookmarkByPosition(fetchInfo, options);
1574        } else {
1575          results = await fetchBookmarksByParentGUID(fetchInfo, options);
1576        }
1577      } else if (fetchInfo.hasOwnProperty("guidPrefix")) {
1578        results = await fetchBookmarksByGUIDPrefix(fetchInfo, options);
1579      } else if (fetchInfo.hasOwnProperty("tags")) {
1580        results = await fetchBookmarksByTags(fetchInfo, options);
1581      }
1582
1583      if (!results) {
1584        return null;
1585      }
1586
1587      if (!Array.isArray(results)) {
1588        results = [results];
1589      }
1590      // Remove non-enumerable properties.
1591      results = results.map(r => Object.assign({}, r));
1592
1593      if (options.includePath) {
1594        for (let result of results) {
1595          let folderPath = await retrieveFullBookmarkPath(result.parentGuid);
1596          if (folderPath) {
1597            result.path = folderPath;
1598          }
1599        }
1600      }
1601
1602      // Ideally this should handle an incremental behavior and thus be invoked
1603      // while we fetch.  Though, the likelihood of 2 or more bookmarks for the
1604      // same match is very low, so it's not worth the added code complication.
1605      if (onResult) {
1606        for (let result of results) {
1607          try {
1608            onResult(result);
1609          } catch (ex) {
1610            Cu.reportError(ex);
1611          }
1612        }
1613      }
1614
1615      return results[0];
1616    })();
1617  },
1618
1619  /**
1620   * Retrieves an object representation of a bookmark-item, along with all of
1621   * its descendants, if any.
1622   *
1623   * Each node in the tree is an object that extends the item representation
1624   * described above with some additional properties:
1625   *
1626   *  - [deprecated] id (number)
1627   *      the item's id.  Defined only if aOptions.includeItemIds is set.
1628   *  - annos (array)
1629   *      the item's annotations.  This is not set if there are no annotations
1630   *      set for the item.
1631   *
1632   * The root object of the tree also has the following properties set:
1633   *  - itemsCount (number, not enumerable)
1634   *      the number of items, including the root item itself, which are
1635   *      represented in the resolved object.
1636   *
1637   * Bookmarked URLs may also have the following properties:
1638   *  - tags (string)
1639   *      csv string of the bookmark's tags, if any.
1640   *  - charset (string)
1641   *      the last known charset of the bookmark, if any.
1642   *  - iconurl (URL)
1643   *      the bookmark's favicon URL, if any.
1644   *
1645   * Folders may also have the following properties:
1646   *  - children (array)
1647   *      the folder's children information, each of them having the same set of
1648   *      properties as above.
1649   *
1650   * @param [optional] guid
1651   *        the topmost item to be queried.  If it's not passed, the Places
1652   *        root folder is queried: that is, you get a representation of the
1653   *        entire bookmarks hierarchy.
1654   * @param [optional] options
1655   *        Options for customizing the query behavior, in the form of an
1656   *        object with any of the following properties:
1657   *         - excludeItemsCallback: a function for excluding items, along with
1658   *           their descendants.  Given an item object (that has everything set
1659   *           apart its potential children data), it should return true if the
1660   *           item should be excluded.  Once an item is excluded, the function
1661   *           isn't called for any of its descendants.  This isn't called for
1662   *           the root item.
1663   *           WARNING: since the function may be called for each item, using
1664   *           this option can slow down the process significantly if the
1665   *           callback does anything that's not relatively trivial.  It is
1666   *           highly recommended to avoid any synchronous I/O or DB queries.
1667   *         - includeItemIds: opt-in to include the deprecated id property.
1668   *           Use it if you must. It'll be removed once the switch to guids is
1669   *           complete.
1670   *
1671   * @return {Promise} resolved when the fetch is complete.
1672   * @resolves to an object that represents either a single item or a
1673   *           bookmarks tree.  if guid points to a non-existent item, the
1674   *           returned promise is resolved to null.
1675   * @rejects if an error happens while fetching.
1676   * @throws if the arguments are invalid.
1677   */
1678  // TODO must implement these methods yet:
1679  // PlacesUtils.promiseBookmarksTree()
1680  fetchTree(guid = "", options = {}) {
1681    throw new Error("Not yet implemented");
1682  },
1683
1684  /**
1685   * Fetch all the existing tags, sorted alphabetically.
1686   * @return {Promise} resolves to an array of objects representing tags, when
1687   *         fetching is complete.
1688   *         Each object looks like {
1689   *           name: the name of the tag,
1690   *           count: number of bookmarks with this tag
1691   *         }
1692   */
1693  async fetchTags() {
1694    // TODO: Once the tagging API is implemented in Bookmarks.jsm, we can cache
1695    // the list of tags, instead of querying every time.
1696    let db = await PlacesUtils.promiseDBConnection();
1697    let rows = await db.executeCached(
1698      `
1699      SELECT b.title AS name, count(*) AS count
1700      FROM moz_bookmarks b
1701      JOIN moz_bookmarks p ON b.parent = p.id
1702      JOIN moz_bookmarks c ON c.parent = b.id
1703      WHERE p.guid = :tagsGuid
1704      GROUP BY name
1705      ORDER BY name COLLATE nocase ASC
1706    `,
1707      { tagsGuid: this.tagsGuid }
1708    );
1709    return rows.map(r => ({
1710      name: r.getResultByName("name"),
1711      count: r.getResultByName("count"),
1712    }));
1713  },
1714
1715  /**
1716   * Reorders contents of a folder based on a provided array of GUIDs.
1717   *
1718   * @param parentGuid
1719   *        The globally unique identifier of the folder whose contents should
1720   *        be reordered.
1721   * @param orderedChildrenGuids
1722   *        Ordered array of the children's GUIDs.  If this list contains
1723   *        non-existing entries they will be ignored.  If the list is
1724   *        incomplete, and the current child list is already in order with
1725   *        respect to orderedChildrenGuids, no change is made. Otherwise, the
1726   *        new items are appended but maintain their current order relative to
1727   *        eachother.
1728   * @param {Object} [options={}]
1729   *        Additional options. Currently supports the following properties:
1730   *         - lastModified: The last modified time to use for the folder and
1731               reordered children. Defaults to the current time.
1732   *         - source: The change source, forwarded to all bookmark observers.
1733   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
1734   *
1735   * @return {Promise} resolved when reordering is complete.
1736   * @rejects if an error happens while reordering.
1737   * @throws if the arguments are invalid.
1738   */
1739  reorder(parentGuid, orderedChildrenGuids, options = {}) {
1740    let info = { guid: parentGuid };
1741    info = validateBookmarkObject("Bookmarks.jsm: reorder", info, {
1742      guid: { required: true },
1743    });
1744
1745    if (!Array.isArray(orderedChildrenGuids) || !orderedChildrenGuids.length) {
1746      throw new Error("Must provide a sorted array of children GUIDs.");
1747    }
1748    try {
1749      orderedChildrenGuids.forEach(PlacesUtils.BOOKMARK_VALIDATORS.guid);
1750    } catch (ex) {
1751      throw new Error("Invalid GUID found in the sorted children array.");
1752    }
1753
1754    options.source =
1755      "source" in options
1756        ? PlacesUtils.BOOKMARK_VALIDATORS.source(options.source)
1757        : Bookmarks.SOURCES.DEFAULT;
1758    options.lastModified =
1759      "lastModified" in options
1760        ? PlacesUtils.BOOKMARK_VALIDATORS.lastModified(options.lastModified)
1761        : new Date();
1762
1763    return (async () => {
1764      let parent = await fetchBookmark(info);
1765      if (!parent || parent.type != this.TYPE_FOLDER) {
1766        throw new Error("No folder found for the provided GUID.");
1767      }
1768
1769      let sortedChildren = await reorderChildren(
1770        parent,
1771        orderedChildrenGuids,
1772        options
1773      );
1774
1775      const notifications = [];
1776
1777      // Note that child.index is the old index.
1778      for (let i = 0; i < sortedChildren.length; ++i) {
1779        let child = sortedChildren[i];
1780        notifications.push(
1781          new PlacesBookmarkMoved({
1782            id: child._id,
1783            itemType: child.type,
1784            url: child.url && child.url.href,
1785            guid: child.guid,
1786            parentGuid: child.parentGuid,
1787            source: options.source,
1788            index: i,
1789            oldParentGuid: child.parentGuid,
1790            oldIndex: child.index,
1791            isTagging:
1792              child.parentGuid === Bookmarks.tagsGuid ||
1793              parent.parentGuid === Bookmarks.tagsGuid,
1794          })
1795        );
1796      }
1797
1798      if (notifications.length) {
1799        PlacesObservers.notifyListeners(notifications);
1800      }
1801    })();
1802  },
1803
1804  /**
1805   * Searches a list of bookmark-items by a search term, url or title.
1806   *
1807   * IMPORTANT:
1808   * This is intended as an interim API for the web-extensions implementation.
1809   * It will be removed as soon as we have a new querying API.
1810   *
1811   * Note also that this used to exclude separators but no longer does so.
1812   *
1813   * If you just want to search bookmarks by URL, use .fetch() instead.
1814   *
1815   * @param query
1816   *        Either a string to use as search term, or an object
1817   *        containing any of these keys: query, title or url with the
1818   *        corresponding string to match as value.
1819   *        The url property can be either a string or an nsIURI.
1820   *
1821   * @return {Promise} resolved when the search is complete.
1822   * @resolves to an array of found bookmark-items.
1823   * @rejects if an error happens while searching.
1824   * @throws if the arguments are invalid.
1825   *
1826   * @note Any unknown property in the query object is ignored.
1827   *       Known properties may be overwritten.
1828   */
1829  search(query) {
1830    if (!query) {
1831      throw new Error("Query object is required");
1832    }
1833    if (typeof query === "string") {
1834      query = { query };
1835    }
1836    if (typeof query !== "object") {
1837      throw new Error("Query must be an object or a string");
1838    }
1839    if (query.query && typeof query.query !== "string") {
1840      throw new Error("Query option must be a string");
1841    }
1842    if (query.title && typeof query.title !== "string") {
1843      throw new Error("Title option must be a string");
1844    }
1845
1846    if (query.url) {
1847      if (typeof query.url === "string" || query.url instanceof URL) {
1848        query.url = new URL(query.url).href;
1849      } else if (query.url instanceof Ci.nsIURI) {
1850        query.url = query.url.spec;
1851      } else {
1852        throw new Error("Url option must be a string or a URL object");
1853      }
1854    }
1855
1856    return queryBookmarks(query);
1857  },
1858});
1859
1860// Globals.
1861
1862// Update implementation.
1863
1864/**
1865 * Updates a single bookmark in the database. This should be called from within
1866 * a transaction.
1867 *
1868 * @param {Object} db The pre-existing database connection.
1869 * @param {Object} info A bookmark-item structure with new properties.
1870 * @param {Object} item A bookmark-item structure representing the existing bookmark.
1871 * @param {Integer} oldIndex The index of the item in the old parent.
1872 * @param {Object} newParent The new parent folder (note: this may be the same as)
1873 *                           the existing folder.
1874 * @param {Integer} syncChangeDelta The change delta to be applied.
1875 */
1876async function updateBookmark(
1877  db,
1878  info,
1879  item,
1880  oldIndex,
1881  newParent,
1882  syncChangeDelta
1883) {
1884  let tuples = new Map();
1885  tuples.set("lastModified", {
1886    value: PlacesUtils.toPRTime(info.lastModified),
1887  });
1888  if (info.hasOwnProperty("title")) {
1889    tuples.set("title", {
1890      value: info.title,
1891      fragment: `title = NULLIF(:title, '')`,
1892    });
1893  }
1894  if (info.hasOwnProperty("dateAdded")) {
1895    tuples.set("dateAdded", { value: PlacesUtils.toPRTime(info.dateAdded) });
1896  }
1897
1898  if (info.hasOwnProperty("url")) {
1899    // Ensure a page exists in moz_places for this URL.
1900    await maybeInsertPlace(db, info.url);
1901    // Update tuples for the update query.
1902    tuples.set("url", {
1903      value: info.url.href,
1904      fragment:
1905        "fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)",
1906    });
1907  }
1908
1909  let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
1910  if (newParent) {
1911    // For simplicity, update the index regardless.
1912    tuples.set("position", { value: newIndex });
1913
1914    // For moving within the same parent, we've already updated the indexes.
1915    if (newParent.guid == item.parentGuid) {
1916      // Moving inside the original container.
1917      // When moving "up", add 1 to each index in the interval.
1918      // Otherwise when moving down, we subtract 1.
1919      // Only the parent needs a sync change, which is handled in
1920      // `setAncestorsLastModified`.
1921      await db.executeCached(
1922        `UPDATE moz_bookmarks
1923           SET position = CASE WHEN :newIndex < :currIndex
1924             THEN position + 1
1925             ELSE position - 1
1926           END
1927           WHERE parent = :newParentId
1928             AND position BETWEEN :lowIndex AND :highIndex
1929          `,
1930        {
1931          newIndex,
1932          currIndex: oldIndex,
1933          newParentId: newParent._id,
1934          lowIndex: Math.min(oldIndex, newIndex),
1935          highIndex: Math.max(oldIndex, newIndex),
1936        }
1937      );
1938    } else {
1939      // Moving across different containers. In this case, both parents and
1940      // the child need sync changes. `setAncestorsLastModified`, below and in
1941      // `update` and `moveToFolder`, handles the parents. The `needsSyncChange`
1942      // check below handles the child.
1943      tuples.set("parent", { value: newParent._id });
1944      await db.executeCached(
1945        `UPDATE moz_bookmarks SET position = position - 1
1946         WHERE parent = :oldParentId
1947           AND position >= :oldIndex
1948        `,
1949        { oldParentId: item._parentId, oldIndex }
1950      );
1951      await db.executeCached(
1952        `UPDATE moz_bookmarks SET position = position + 1
1953         WHERE parent = :newParentId
1954           AND position >= :newIndex
1955        `,
1956        { newParentId: newParent._id, newIndex }
1957      );
1958
1959      await setAncestorsLastModified(
1960        db,
1961        item.parentGuid,
1962        info.lastModified,
1963        syncChangeDelta
1964      );
1965    }
1966  }
1967
1968  if (syncChangeDelta) {
1969    // Sync stores child indices in the parent's record, so we only bump the
1970    // item's counter if we're updating at least one more property in
1971    // addition to the index, last modified time, and dateAdded.
1972    let sizeThreshold = 1;
1973    if (newIndex != oldIndex) {
1974      ++sizeThreshold;
1975    }
1976    if (tuples.has("dateAdded")) {
1977      ++sizeThreshold;
1978    }
1979    let needsSyncChange = tuples.size > sizeThreshold;
1980    if (needsSyncChange) {
1981      tuples.set("syncChangeDelta", {
1982        value: syncChangeDelta,
1983        fragment: "syncChangeCounter = syncChangeCounter + :syncChangeDelta",
1984      });
1985    }
1986  }
1987
1988  let isTagging = item._grandParentId == PlacesUtils.tagsFolderId;
1989  if (isTagging) {
1990    // If we're updating a tag entry, bump the sync change counter for
1991    // bookmarks with the tagged URL.
1992    await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
1993      db,
1994      item.url,
1995      syncChangeDelta
1996    );
1997    if (info.hasOwnProperty("url")) {
1998      // Changing the URL of a tag entry is equivalent to untagging the
1999      // old URL and tagging the new one, so we bump the change counter
2000      // for the new URL here.
2001      await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
2002        db,
2003        info.url,
2004        syncChangeDelta
2005      );
2006    }
2007  }
2008
2009  let isChangingTagFolder = item._parentId == PlacesUtils.tagsFolderId;
2010  if (isChangingTagFolder && syncChangeDelta) {
2011    // If we're updating a tag folder (for example, changing a tag's title),
2012    // bump the change counter for all tagged bookmarks.
2013    await db.executeCached(
2014      `
2015      UPDATE moz_bookmarks SET
2016        syncChangeCounter = syncChangeCounter + :syncChangeDelta
2017      WHERE type = :type AND
2018            fk = (SELECT fk FROM moz_bookmarks WHERE parent = :parent)
2019      `,
2020      { syncChangeDelta, type: Bookmarks.TYPE_BOOKMARK, parent: item._id }
2021    );
2022  }
2023
2024  await db.executeCached(
2025    `UPDATE moz_bookmarks
2026     SET ${Array.from(tuples.keys())
2027       .map(v => tuples.get(v).fragment || `${v} = :${v}`)
2028       .join(", ")}
2029     WHERE guid = :guid
2030    `,
2031    Object.assign(
2032      { guid: item.guid },
2033      [...tuples.entries()].reduce((p, c) => {
2034        p[c[0]] = c[1].value;
2035        return p;
2036      }, {})
2037    )
2038  );
2039
2040  if (newParent) {
2041    if (newParent.guid == item.parentGuid) {
2042      // Mark all affected separators as changed
2043      // Also bumps the change counter if the item itself is a separator
2044      const startIndex = Math.min(newIndex, oldIndex);
2045      await adjustSeparatorsSyncCounter(
2046        db,
2047        newParent._id,
2048        startIndex,
2049        syncChangeDelta
2050      );
2051    } else {
2052      // Mark all affected separators as changed
2053      await adjustSeparatorsSyncCounter(
2054        db,
2055        item._parentId,
2056        oldIndex,
2057        syncChangeDelta
2058      );
2059      await adjustSeparatorsSyncCounter(
2060        db,
2061        newParent._id,
2062        newIndex,
2063        syncChangeDelta
2064      );
2065    }
2066    // Remove the Sync orphan annotation from reparented items. We don't
2067    // notify annotation observers about this because this is a temporary,
2068    // internal anno that's only used by Sync.
2069    await db.executeCached(
2070      `DELETE FROM moz_items_annos
2071       WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes
2072                                  WHERE name = :orphanAnno) AND
2073             item_id = :id`,
2074      { orphanAnno: PlacesSyncUtils.bookmarks.SYNC_PARENT_ANNO, id: item._id }
2075    );
2076  }
2077
2078  // If the parent changed, update related non-enumerable properties.
2079  let additionalParentInfo = {};
2080  if (newParent) {
2081    additionalParentInfo.parentGuid = newParent.guid;
2082    Object.defineProperty(additionalParentInfo, "_parentId", {
2083      value: newParent._id,
2084      enumerable: false,
2085    });
2086    Object.defineProperty(additionalParentInfo, "_grandParentId", {
2087      value: newParent._parentId,
2088      enumerable: false,
2089    });
2090  }
2091
2092  return mergeIntoNewObject(item, info, additionalParentInfo);
2093}
2094
2095// Insert implementation.
2096
2097function insertBookmark(item, parent) {
2098  return PlacesUtils.withConnectionWrapper(
2099    "Bookmarks.jsm: insertBookmark",
2100    async function(db) {
2101      // If a guid was not provided, generate one, so we won't need to fetch the
2102      // bookmark just after having created it.
2103      let hasExistingGuid = item.hasOwnProperty("guid");
2104      if (!hasExistingGuid) {
2105        item.guid = PlacesUtils.history.makeGuid();
2106      }
2107
2108      let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
2109
2110      await db.executeTransaction(async function transaction() {
2111        if (item.type == Bookmarks.TYPE_BOOKMARK) {
2112          // Ensure a page exists in moz_places for this URL.
2113          // The IGNORE conflict can trigger on `guid`.
2114          await maybeInsertPlace(db, item.url);
2115        }
2116
2117        // Adjust indices.
2118        await db.executeCached(
2119          `UPDATE moz_bookmarks SET position = position + 1
2120         WHERE parent = :parent
2121         AND position >= :index
2122        `,
2123          { parent: parent._id, index: item.index }
2124        );
2125
2126        let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
2127          item.source
2128        );
2129        let syncStatus = PlacesSyncUtils.bookmarks.determineInitialSyncStatus(
2130          item.source
2131        );
2132
2133        // Insert the bookmark into the database.
2134        await db.executeCached(
2135          `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
2136                                    dateAdded, lastModified, guid,
2137                                    syncChangeCounter, syncStatus)
2138         VALUES (CASE WHEN :url ISNULL THEN NULL ELSE (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) END,
2139                 :type, :parent, :index, NULLIF(:title, ''), :date_added,
2140                 :last_modified, :guid, :syncChangeCounter, :syncStatus)
2141        `,
2142          {
2143            url: item.hasOwnProperty("url") ? item.url.href : null,
2144            type: item.type,
2145            parent: parent._id,
2146            index: item.index,
2147            title: item.title,
2148            date_added: PlacesUtils.toPRTime(item.dateAdded),
2149            last_modified: PlacesUtils.toPRTime(item.lastModified),
2150            guid: item.guid,
2151            syncChangeCounter: syncChangeDelta,
2152            syncStatus,
2153          }
2154        );
2155
2156        // Mark all affected separators as changed
2157        await adjustSeparatorsSyncCounter(
2158          db,
2159          parent._id,
2160          item.index + 1,
2161          syncChangeDelta
2162        );
2163
2164        if (hasExistingGuid) {
2165          // Remove stale tombstones if we're reinserting an item.
2166          await db.executeCached(
2167            `DELETE FROM moz_bookmarks_deleted WHERE guid = :guid`,
2168            { guid: item.guid }
2169          );
2170        }
2171
2172        if (isTagging) {
2173          // New tag entry; bump the change counter for bookmarks with the
2174          // tagged URL.
2175          await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
2176            db,
2177            item.url,
2178            syncChangeDelta
2179          );
2180        }
2181
2182        await setAncestorsLastModified(
2183          db,
2184          item.parentGuid,
2185          item.dateAdded,
2186          syncChangeDelta
2187        );
2188      });
2189
2190      // If not a tag recalculate frecency...
2191      if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
2192        // ...though we don't wait for the calculation.
2193        updateFrecency(db, [item.url]).catch(Cu.reportError);
2194      }
2195
2196      return item;
2197    }
2198  );
2199}
2200
2201function insertBookmarkTree(items, source, parent, urls, lastAddedForParent) {
2202  return PlacesUtils.withConnectionWrapper(
2203    "Bookmarks.jsm: insertBookmarkTree",
2204    async function(db) {
2205      await db.executeTransaction(async function transaction() {
2206        await maybeInsertManyPlaces(db, urls);
2207
2208        let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
2209          source
2210        );
2211        let syncStatus = PlacesSyncUtils.bookmarks.determineInitialSyncStatus(
2212          source
2213        );
2214
2215        let rootId = parent._id;
2216
2217        items = items.map(item => ({
2218          url: item.url && item.url.href,
2219          type: item.type,
2220          parentGuid: item.parentGuid,
2221          index: item.index,
2222          title: item.title,
2223          date_added: PlacesUtils.toPRTime(item.dateAdded),
2224          last_modified: PlacesUtils.toPRTime(item.lastModified),
2225          guid: item.guid,
2226          syncChangeCounter: syncChangeDelta,
2227          syncStatus,
2228          rootId,
2229        }));
2230        await db.executeCached(
2231          `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
2232                                    dateAdded, lastModified, guid,
2233                                    syncChangeCounter, syncStatus)
2234         VALUES (CASE WHEN :url ISNULL THEN NULL ELSE (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) END, :type,
2235         (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid),
2236         IFNULL(:index, (SELECT count(*) FROM moz_bookmarks WHERE parent = :rootId)),
2237         NULLIF(:title, ''), :date_added, :last_modified, :guid,
2238         :syncChangeCounter, :syncStatus)`,
2239          items
2240        );
2241
2242        // Remove stale tombstones for new items.
2243        for (let chunk of PlacesUtils.chunkArray(items, db.variableLimit)) {
2244          await db.executeCached(
2245            `DELETE FROM moz_bookmarks_deleted
2246             WHERE guid IN (${sqlBindPlaceholders(chunk)})`,
2247            chunk.map(item => item.guid)
2248          );
2249        }
2250
2251        await setAncestorsLastModified(
2252          db,
2253          parent.guid,
2254          lastAddedForParent,
2255          syncChangeDelta
2256        );
2257      });
2258
2259      // We don't wait for the frecency calculation.
2260      updateFrecency(db, urls).catch(Cu.reportError);
2261
2262      return items;
2263    }
2264  );
2265}
2266
2267/**
2268 * Handles special data on a bookmark, e.g. annotations, keywords, tags, charsets,
2269 * inserting the data into the appropriate place.
2270 *
2271 * @param {Integer} itemId The ID of the item within the bookmarks database.
2272 * @param {Object} item The bookmark item with possible special data to be inserted.
2273 */
2274async function handleBookmarkItemSpecialData(itemId, item) {
2275  if ("keyword" in item && item.keyword) {
2276    try {
2277      await PlacesUtils.keywords.insert({
2278        keyword: item.keyword,
2279        url: item.url,
2280        postData: item.postData,
2281        source: item.source,
2282      });
2283    } catch (ex) {
2284      Cu.reportError(
2285        `Failed to insert keyword "${item.keyword} for ${item.url}": ${ex}`
2286      );
2287    }
2288  }
2289  if ("tags" in item) {
2290    try {
2291      PlacesUtils.tagging.tagURI(
2292        NetUtil.newURI(item.url),
2293        item.tags,
2294        item.source
2295      );
2296    } catch (ex) {
2297      // Invalid tag child, skip it.
2298      Cu.reportError(
2299        `Unable to set tags "${item.tags.join(", ")}" for ${item.url}: ${ex}`
2300      );
2301    }
2302  }
2303  if ("charset" in item && item.charset) {
2304    try {
2305      // UTF-8 is the default. If we are passed the value then set it to null,
2306      // to ensure any charset is removed from the database.
2307      let charset = item.charset;
2308      if (item.charset.toLowerCase() == "utf-8") {
2309        charset = null;
2310      }
2311
2312      await PlacesUtils.history.update({
2313        url: item.url,
2314        annotations: new Map([[PlacesUtils.CHARSET_ANNO, charset]]),
2315      });
2316    } catch (ex) {
2317      Cu.reportError(
2318        `Failed to set charset "${item.charset}" for ${item.url}: ${ex}`
2319      );
2320    }
2321  }
2322}
2323
2324// Query implementation.
2325
2326async function queryBookmarks(info) {
2327  let queryParams = {
2328    tags_folder: await promiseTagsFolderId(),
2329  };
2330  // We're searching for bookmarks, so exclude tags.
2331  let queryString = "WHERE b.parent <> :tags_folder";
2332  queryString += " AND p.parent <> :tags_folder";
2333
2334  if (info.title) {
2335    queryString += " AND b.title = :title";
2336    queryParams.title = info.title;
2337  }
2338
2339  if (info.url) {
2340    queryString += " AND h.url_hash = hash(:url) AND h.url = :url";
2341    queryParams.url = info.url;
2342  }
2343
2344  if (info.query) {
2345    queryString +=
2346      " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior, NULL) ";
2347    queryParams.query = info.query;
2348    queryParams.matchBehavior = MATCH_ANYWHERE_UNMODIFIED;
2349    queryParams.searchBehavior = BEHAVIOR_BOOKMARK;
2350  }
2351
2352  return PlacesUtils.withConnectionWrapper(
2353    "Bookmarks.jsm: queryBookmarks",
2354    async function(db) {
2355      // _id, _childCount, _grandParentId and _parentId fields
2356      // are required to be in the result by the converting function
2357      // hence setting them to NULL
2358      let rows = await db.executeCached(
2359        `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2360                b.dateAdded, b.lastModified, b.type,
2361                IFNULL(b.title, '') AS title, h.url AS url, b.parent, p.parent,
2362                NULL AS _id,
2363                NULL AS _childCount,
2364                NULL AS _grandParentId,
2365                NULL AS _parentId,
2366                NULL AS _syncStatus
2367         FROM moz_bookmarks b
2368         LEFT JOIN moz_bookmarks p ON p.id = b.parent
2369         LEFT JOIN moz_places h ON h.id = b.fk
2370         ${queryString}
2371        `,
2372        queryParams
2373      );
2374
2375      return rowsToItemsArray(rows);
2376    }
2377  );
2378}
2379
2380/**
2381 * Internal fetch implementation.
2382 * @param {object} info
2383 *        The bookmark item to remove.
2384 * @param {object} options
2385 *        An options object supporting the following properties:
2386 * @param {object} [options.concurrent]
2387 *        Whether to use the concurrent read-only connection.
2388 * @param {object} [options.db]
2389 *        A specific connection to be used.
2390 * @param {object} [options.ignoreInvalidURLs]
2391 *        Whether invalid URLs should be ignored or throw an exception.
2392 *
2393 */
2394async function fetchBookmark(info, options = {}) {
2395  let query = async function(db) {
2396    let rows = await db.executeCached(
2397      `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2398              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2399              h.url AS url, b.id AS _id, b.parent AS _parentId,
2400              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
2401              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
2402       FROM moz_bookmarks b
2403       LEFT JOIN moz_bookmarks p ON p.id = b.parent
2404       LEFT JOIN moz_places h ON h.id = b.fk
2405       WHERE b.guid = :guid
2406      `,
2407      { guid: info.guid }
2408    );
2409
2410    return rows.length
2411      ? rowsToItemsArray(rows, !!options.ignoreInvalidURLs)[0]
2412      : null;
2413  };
2414  if (options.concurrent) {
2415    let db = await PlacesUtils.promiseDBConnection();
2416    return query(db);
2417  }
2418  if (options.db) {
2419    return query(options.db);
2420  }
2421  return PlacesUtils.withConnectionWrapper(
2422    "Bookmarks.jsm: fetchBookmark",
2423    query
2424  );
2425}
2426
2427async function fetchBookmarkByPosition(info, options = {}) {
2428  let query = async function(db) {
2429    let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
2430    let rows = await db.executeCached(
2431      `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2432              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2433              h.url AS url, b.id AS _id, b.parent AS _parentId,
2434              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
2435              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
2436       FROM moz_bookmarks b
2437       LEFT JOIN moz_bookmarks p ON p.id = b.parent
2438       LEFT JOIN moz_places h ON h.id = b.fk
2439       WHERE p.guid = :parentGuid
2440       AND b.position = IFNULL(:index, (SELECT count(*) - 1
2441                                        FROM moz_bookmarks
2442                                        WHERE parent = p.id))
2443      `,
2444      { parentGuid: info.parentGuid, index }
2445    );
2446
2447    return rows.length ? rowsToItemsArray(rows)[0] : null;
2448  };
2449  if (options.concurrent) {
2450    let db = await PlacesUtils.promiseDBConnection();
2451    return query(db);
2452  }
2453  return PlacesUtils.withConnectionWrapper(
2454    "Bookmarks.jsm: fetchBookmarkByPosition",
2455    query
2456  );
2457}
2458
2459async function fetchBookmarksByTags(info, options = {}) {
2460  let query = async function(db) {
2461    let rows = await db.executeCached(
2462      `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2463              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2464              h.url AS url, b.id AS _id, b.parent AS _parentId,
2465              NULL AS _childCount,
2466              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
2467       FROM moz_bookmarks b
2468       JOIN moz_bookmarks p ON p.id = b.parent
2469       JOIN moz_bookmarks g ON g.id = p.parent
2470       JOIN moz_places h ON h.id = b.fk
2471       WHERE g.guid <> ? AND b.fk IN (
2472          SELECT b2.fk FROM moz_bookmarks b2
2473          JOIN moz_bookmarks p2 ON p2.id = b2.parent
2474          JOIN moz_bookmarks g2 ON g2.id = p2.parent
2475          WHERE g2.guid = ?
2476                AND lower(p2.title) IN (
2477                  ${new Array(info.tags.length).fill("?").join(",")}
2478                )
2479          GROUP BY b2.fk HAVING count(*) = ${info.tags.length}
2480       )
2481       ORDER BY b.lastModified DESC
2482      `,
2483      [Bookmarks.tagsGuid, Bookmarks.tagsGuid].concat(
2484        info.tags.map(t => t.toLowerCase())
2485      )
2486    );
2487
2488    return rows.length ? rowsToItemsArray(rows) : null;
2489  };
2490
2491  if (options.concurrent) {
2492    let db = await PlacesUtils.promiseDBConnection();
2493    return query(db);
2494  }
2495  return PlacesUtils.withConnectionWrapper(
2496    "Bookmarks.jsm: fetchBookmarksByTags",
2497    query
2498  );
2499}
2500
2501async function fetchBookmarksByGUIDPrefix(info, options = {}) {
2502  let query = async function(db) {
2503    let rows = await db.executeCached(
2504      `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2505              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2506              h.url AS url, b.id AS _id, b.parent AS _parentId,
2507              NULL AS _childCount,
2508              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
2509       FROM moz_bookmarks b
2510       LEFT JOIN moz_bookmarks p ON p.id = b.parent
2511       LEFT JOIN moz_places h ON h.id = b.fk
2512       WHERE b.guid LIKE :guidPrefix
2513       ORDER BY b.lastModified DESC
2514      `,
2515      { guidPrefix: info.guidPrefix + "%" }
2516    );
2517
2518    return rows.length ? rowsToItemsArray(rows) : null;
2519  };
2520
2521  if (options.concurrent) {
2522    let db = await PlacesUtils.promiseDBConnection();
2523    return query(db);
2524  }
2525  return PlacesUtils.withConnectionWrapper(
2526    "Bookmarks.jsm: fetchBookmarksByGUIDPrefix",
2527    query
2528  );
2529}
2530
2531async function fetchBookmarksByURL(info, options = {}) {
2532  let query = async function(db) {
2533    let tagsFolderId = await promiseTagsFolderId();
2534    let rows = await db.executeCached(
2535      `/* do not warn (bug no): not worth to add an index */
2536      SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2537              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2538              h.url AS url, b.id AS _id, b.parent AS _parentId,
2539              NULL AS _childCount, /* Unused for now */
2540              p.parent AS _grandParentId, b.syncStatus AS _syncStatus
2541      FROM moz_bookmarks b
2542      JOIN moz_bookmarks p ON p.id = b.parent
2543      JOIN moz_places h ON h.id = b.fk
2544      WHERE h.url_hash = hash(:url) AND h.url = :url
2545      AND _grandParentId <> :tagsFolderId
2546      ORDER BY b.lastModified DESC
2547      `,
2548      { url: info.url.href, tagsFolderId }
2549    );
2550
2551    return rows.length ? rowsToItemsArray(rows) : null;
2552  };
2553
2554  if (options.concurrent) {
2555    let db = await PlacesUtils.promiseDBConnection();
2556    return query(db);
2557  }
2558  return PlacesUtils.withConnectionWrapper(
2559    "Bookmarks.jsm: fetchBookmarksByURL",
2560    query
2561  );
2562}
2563
2564async function fetchBookmarksByParentGUID(info, options = {}) {
2565  let query = async function(db) {
2566    let rows = await db.executeCached(
2567      `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2568              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2569              h.url AS url,
2570              NULL AS _id,
2571              NULL AS _parentId,
2572              NULL AS _childCount,
2573              NULL AS _grandParentId,
2574              NULL AS _syncStatus
2575       FROM moz_bookmarks b
2576       LEFT JOIN moz_bookmarks p ON p.id = b.parent
2577       LEFT JOIN moz_places h ON h.id = b.fk
2578       WHERE p.guid = :parentGuid
2579       ORDER BY b.position ASC
2580      `,
2581      { parentGuid: info.parentGuid }
2582    );
2583
2584    return rows.length ? rowsToItemsArray(rows) : null;
2585  };
2586
2587  if (options.concurrent) {
2588    let db = await PlacesUtils.promiseDBConnection();
2589    return query(db);
2590  }
2591  return PlacesUtils.withConnectionWrapper(
2592    "Bookmarks.jsm: fetchBookmarksByParentGUID",
2593    query
2594  );
2595}
2596
2597function fetchRecentBookmarks(numberOfItems) {
2598  return PlacesUtils.withConnectionWrapper(
2599    "Bookmarks.jsm: fetchRecentBookmarks",
2600    async function(db) {
2601      let tagsFolderId = await promiseTagsFolderId();
2602      let rows = await db.executeCached(
2603        `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2604                b.dateAdded, b.lastModified, b.type,
2605                IFNULL(b.title, '') AS title, h.url AS url, NULL AS _id,
2606                NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId,
2607                NULL AS _syncStatus
2608        FROM moz_bookmarks b
2609        JOIN moz_bookmarks p ON p.id = b.parent
2610        JOIN moz_places h ON h.id = b.fk
2611        WHERE p.parent <> :tagsFolderId
2612        AND b.type = :type
2613        AND url_hash NOT BETWEEN hash("place", "prefix_lo")
2614                              AND hash("place", "prefix_hi")
2615        ORDER BY b.dateAdded DESC, b.ROWID DESC
2616        LIMIT :numberOfItems
2617        `,
2618        {
2619          tagsFolderId,
2620          type: Bookmarks.TYPE_BOOKMARK,
2621          numberOfItems,
2622        }
2623      );
2624
2625      return rows.length ? rowsToItemsArray(rows) : [];
2626    }
2627  );
2628}
2629
2630async function fetchBookmarksByParent(db, info) {
2631  let rows = await db.executeCached(
2632    `SELECT b.guid, IFNULL(p.guid, '') AS parentGuid, b.position AS 'index',
2633            b.dateAdded, b.lastModified, b.type, IFNULL(b.title, '') AS title,
2634            h.url AS url, b.id AS _id, b.parent AS _parentId,
2635            (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
2636            p.parent AS _grandParentId, b.syncStatus AS _syncStatus
2637     FROM moz_bookmarks b
2638     LEFT JOIN moz_bookmarks p ON p.id = b.parent
2639     LEFT JOIN moz_places h ON h.id = b.fk
2640     WHERE p.guid = :parentGuid
2641     ORDER BY b.position ASC
2642    `,
2643    { parentGuid: info.parentGuid }
2644  );
2645
2646  return rowsToItemsArray(rows);
2647}
2648
2649// Remove implementation.
2650
2651function removeBookmarks(items, options) {
2652  return PlacesUtils.withConnectionWrapper(
2653    "Bookmarks.jsm: removeBookmarks",
2654    async function(db) {
2655      let urls = [];
2656
2657      await db.executeTransaction(async function transaction() {
2658        // We use a map for its de-duplication properties.
2659        let parents = new Map();
2660        let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
2661          options.source
2662        );
2663
2664        for (let item of items) {
2665          parents.set(item.parentGuid, item._parentId);
2666
2667          // If it's a folder, remove its contents first.
2668          if (item.type == Bookmarks.TYPE_FOLDER) {
2669            if (
2670              options.preventRemovalOfNonEmptyFolders &&
2671              item._childCount > 0
2672            ) {
2673              throw new Error("Cannot remove a non-empty folder.");
2674            }
2675            urls = urls.concat(
2676              await removeFoldersContents(db, [item.guid], options)
2677            );
2678          }
2679        }
2680
2681        for (let chunk of PlacesUtils.chunkArray(items, db.variableLimit)) {
2682          // We don't go through the annotations service for this cause otherwise
2683          // we'd get a pointless onItemChanged notification and it would also
2684          // set lastModified to an unexpected value.
2685          await removeAnnotationsForItems(db, chunk);
2686
2687          // Remove the bookmarks.
2688          await db.executeCached(
2689            `DELETE FROM moz_bookmarks
2690             WHERE guid IN (${sqlBindPlaceholders(chunk)})`,
2691            chunk.map(item => item.guid)
2692          );
2693        }
2694
2695        for (let [parentGuid, parentId] of parents.entries()) {
2696          // Now recalculate the positions.
2697          await db.executeCached(
2698            `WITH positions(id, pos, seq) AS (
2699            SELECT id, position AS pos,
2700                   (row_number() OVER (ORDER BY position)) - 1 AS seq
2701            FROM moz_bookmarks
2702            WHERE parent = :parentId
2703          )
2704          UPDATE moz_bookmarks
2705            SET position = (SELECT seq FROM positions WHERE positions.id = moz_bookmarks.id)
2706            WHERE id IN (SELECT id FROM positions WHERE seq <> pos)
2707        `,
2708            { parentId }
2709          );
2710
2711          // Mark this parent as changed.
2712          await setAncestorsLastModified(
2713            db,
2714            parentGuid,
2715            new Date(),
2716            syncChangeDelta
2717          );
2718        }
2719
2720        for (let i = 0; i < items.length; i++) {
2721          const item = items[i];
2722          // For the notifications, we may need to adjust indexes if there are more
2723          // than one of the same item in the folder. This makes sure that we notify
2724          // the index of the item when it was removed, rather than the original index.
2725          for (let j = i + 1; j < items.length; j++) {
2726            if (
2727              items[j]._parentId == item._parentId &&
2728              items[j].index > item.index
2729            ) {
2730              items[j].index--;
2731            }
2732          }
2733          if (item._grandParentId == PlacesUtils.tagsFolderId) {
2734            // If we're removing a tag entry, increment the change counter for all
2735            // bookmarks with the tagged URL.
2736            await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
2737              db,
2738              item.url,
2739              syncChangeDelta
2740            );
2741          }
2742
2743          await adjustSeparatorsSyncCounter(
2744            db,
2745            item._parentId,
2746            item.index,
2747            syncChangeDelta
2748          );
2749        }
2750
2751        // Write tombstones for the removed items.
2752        await insertTombstones(db, items, syncChangeDelta);
2753      });
2754
2755      // Update the frecencies outside of the transaction, excluding tags, so that
2756      // the updates can progress in the background.
2757      urls = urls.concat(
2758        items
2759          .filter(item => {
2760            let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
2761            return !isUntagging && "url" in item;
2762          })
2763          .map(item => item.url)
2764      );
2765
2766      if (urls.length) {
2767        await PlacesUtils.keywords.removeFromURLsIfNotBookmarked(urls);
2768        updateFrecency(db, urls).catch(Cu.reportError);
2769      }
2770    }
2771  );
2772}
2773
2774// Reorder implementation.
2775
2776function reorderChildren(parent, orderedChildrenGuids, options) {
2777  return PlacesUtils.withConnectionWrapper(
2778    "Bookmarks.jsm: reorderChildren",
2779    db =>
2780      db.executeTransaction(async function() {
2781        // Select all of the direct children for the given parent.
2782        let children = await fetchBookmarksByParent(db, {
2783          parentGuid: parent.guid,
2784        });
2785        if (!children.length) {
2786          return [];
2787        }
2788
2789        // Maps of GUIDs to indices for fast lookups in the comparator function.
2790        let guidIndices = new Map();
2791        let currentIndices = new Map();
2792        for (let i = 0; i < orderedChildrenGuids.length; ++i) {
2793          let guid = orderedChildrenGuids[i];
2794          guidIndices.set(guid, i);
2795        }
2796
2797        // If we got an incomplete list but everything we have is in the right
2798        // order, we do nothing.
2799        let needReorder = true;
2800        let requestedChildIndices = [];
2801        for (let i = 0; i < children.length; ++i) {
2802          // Take the opportunity to build currentIndices here, since we already
2803          // are iterating over the children array.
2804          currentIndices.set(children[i].guid, i);
2805
2806          if (guidIndices.has(children[i].guid)) {
2807            let index = guidIndices.get(children[i].guid);
2808            requestedChildIndices.push(index);
2809          }
2810        }
2811
2812        if (requestedChildIndices.length) {
2813          needReorder = false;
2814          for (let i = 1; i < requestedChildIndices.length; ++i) {
2815            if (requestedChildIndices[i - 1] > requestedChildIndices[i]) {
2816              needReorder = true;
2817              break;
2818            }
2819          }
2820        }
2821
2822        if (needReorder) {
2823          // Reorder the children array according to the specified order, provided
2824          // GUIDs come first, others are appended in somehow random order.
2825          children.sort((a, b) => {
2826            // This works provided fetchBookmarksByParent returns sorted children.
2827            if (!guidIndices.has(a.guid) && !guidIndices.has(b.guid)) {
2828              return currentIndices.get(a.guid) < currentIndices.get(b.guid)
2829                ? -1
2830                : 1;
2831            }
2832            if (!guidIndices.has(a.guid)) {
2833              return 1;
2834            }
2835            if (!guidIndices.has(b.guid)) {
2836              return -1;
2837            }
2838            return guidIndices.get(a.guid) < guidIndices.get(b.guid) ? -1 : 1;
2839          });
2840
2841          // Update the bookmarks position now.  If any unknown guid have been
2842          // inserted meanwhile, its position will be set to -position, and we'll
2843          // handle it later.
2844          // To do the update in a single step, we build a VALUES (guid, position)
2845          // table.  We then use count() in the sorting table to avoid skipping values
2846          // when no more existing GUIDs have been provided.
2847          let valuesTable = children
2848            .map((child, i) => `("${child.guid}", ${i})`)
2849            .join();
2850          await db.execute(
2851            `WITH sorting(g, p) AS (
2852             VALUES ${valuesTable}
2853           )
2854           UPDATE moz_bookmarks SET
2855             position = (
2856               SELECT CASE count(*) WHEN 0 THEN -position
2857                                           ELSE count(*) - 1
2858                      END
2859               FROM sorting a
2860               JOIN sorting b ON b.p <= a.p
2861               WHERE a.g = guid
2862             ),
2863             lastModified = :lastModified
2864           WHERE parent = :parentId
2865          `,
2866            {
2867              parentId: parent._id,
2868              lastModified: PlacesUtils.toPRTime(options.lastModified),
2869            }
2870          );
2871
2872          let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
2873            options.source
2874          );
2875          await setAncestorsLastModified(
2876            db,
2877            parent.guid,
2878            options.lastModified,
2879            syncChangeDelta
2880          );
2881
2882          // Update position of items that could have been inserted in the meanwhile.
2883          // Since this can happen rarely and it's only done for schema coherence
2884          // resonds, we won't notify about these changes.
2885          await db.executeCached(
2886            `CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
2887             AFTER UPDATE OF position ON moz_bookmarks
2888             WHEN NEW.position = -1
2889           BEGIN
2890             UPDATE moz_bookmarks
2891             SET position = (SELECT MAX(position) FROM moz_bookmarks
2892                             WHERE parent = NEW.parent) +
2893                            (SELECT count(*) FROM moz_bookmarks
2894                             WHERE parent = NEW.parent
2895                               AND position BETWEEN OLD.position AND -1)
2896             WHERE guid = NEW.guid;
2897           END
2898          `
2899          );
2900
2901          await db.executeCached(
2902            `UPDATE moz_bookmarks SET position = -1 WHERE position < 0`
2903          );
2904
2905          await db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
2906        }
2907
2908        // Remove the Sync orphan annotation from the reordered children, so that
2909        // Sync doesn't try to reparent them once it sees the original parents. We
2910        // only do this for explicitly ordered children, to avoid removing orphan
2911        // annos set by Sync.
2912        let possibleOrphanIds = [];
2913        for (let child of children) {
2914          if (guidIndices.has(child.guid)) {
2915            possibleOrphanIds.push(child._id);
2916          }
2917        }
2918        await db.executeCached(
2919          `DELETE FROM moz_items_annos
2920         WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes
2921                                    WHERE name = :orphanAnno) AND
2922               item_id IN (${possibleOrphanIds.join(", ")})`,
2923          { orphanAnno: PlacesSyncUtils.bookmarks.SYNC_PARENT_ANNO }
2924        );
2925
2926        return children;
2927      })
2928  );
2929}
2930
2931// Helpers.
2932
2933/**
2934 * Merges objects into a new object, included non-enumerable properties.
2935 *
2936 * @param sources
2937 *        source objects to merge.
2938 * @return a new object including all properties from the source objects.
2939 */
2940function mergeIntoNewObject(...sources) {
2941  let dest = {};
2942  for (let src of sources) {
2943    for (let prop of Object.getOwnPropertyNames(src)) {
2944      Object.defineProperty(
2945        dest,
2946        prop,
2947        Object.getOwnPropertyDescriptor(src, prop)
2948      );
2949    }
2950  }
2951  return dest;
2952}
2953
2954/**
2955 * Remove properties that have the same value across two bookmark objects.
2956 *
2957 * @param dest
2958 *        destination bookmark object.
2959 * @param src
2960 *        source bookmark object.
2961 * @return a cleaned up bookmark object.
2962 * @note "guid" is never removed.
2963 */
2964function removeSameValueProperties(dest, src) {
2965  for (let prop in dest) {
2966    let remove = false;
2967    switch (prop) {
2968      case "lastModified":
2969      case "dateAdded":
2970        remove =
2971          src.hasOwnProperty(prop) &&
2972          dest[prop].getTime() == src[prop].getTime();
2973        break;
2974      case "url":
2975        remove = src.hasOwnProperty(prop) && dest[prop].href == src[prop].href;
2976        break;
2977      default:
2978        remove = dest[prop] == src[prop];
2979    }
2980    if (remove && prop != "guid") {
2981      delete dest[prop];
2982    }
2983  }
2984}
2985
2986/**
2987 * Convert an array of mozIStorageRow objects to an array of bookmark objects.
2988 *
2989 * @param {Array} rows
2990 *        the array of mozIStorageRow objects.
2991 * @param {Boolean} ignoreInvalidURLs
2992 *        whether to ignore invalid urls (leaving the url property undefined)
2993 *        or throw.
2994 * @return an array of bookmark objects.
2995 */
2996function rowsToItemsArray(rows, ignoreInvalidURLs = false) {
2997  return rows.map(row => {
2998    let item = {};
2999    for (let prop of ["guid", "index", "type", "title"]) {
3000      item[prop] = row.getResultByName(prop);
3001    }
3002    for (let prop of ["dateAdded", "lastModified"]) {
3003      let value = row.getResultByName(prop);
3004      if (value) {
3005        item[prop] = PlacesUtils.toDate(value);
3006      }
3007    }
3008    let parentGuid = row.getResultByName("parentGuid");
3009    if (parentGuid) {
3010      item.parentGuid = parentGuid;
3011    }
3012    let url = row.getResultByName("url");
3013    if (url) {
3014      try {
3015        item.url = new URL(url);
3016      } catch (ex) {
3017        if (!ignoreInvalidURLs) {
3018          throw ex;
3019        }
3020      }
3021    }
3022
3023    for (let prop of [
3024      "_id",
3025      "_parentId",
3026      "_childCount",
3027      "_grandParentId",
3028      "_syncStatus",
3029    ]) {
3030      let val = row.getResultByName(prop);
3031      if (val !== null) {
3032        // These properties should not be returned to the API consumer, thus
3033        // they are non-enumerable and removed through Object.assign just before
3034        // the object is returned.
3035        // Configurable is set to support mergeIntoNewObject overwrites.
3036        Object.defineProperty(item, prop, {
3037          value: val,
3038          enumerable: false,
3039          configurable: true,
3040        });
3041      }
3042    }
3043
3044    return item;
3045  });
3046}
3047
3048function validateBookmarkObject(name, input, behavior) {
3049  return PlacesUtils.validateItemProperties(
3050    name,
3051    PlacesUtils.BOOKMARK_VALIDATORS,
3052    input,
3053    behavior
3054  );
3055}
3056
3057/**
3058 * Updates frecency for a list of URLs.
3059 *
3060 * @param db
3061 *        the Sqlite.jsm connection handle.
3062 * @param urls
3063 *        the array of URLs to update.
3064 */
3065var updateFrecency = async function(db, urls) {
3066  let hrefs = urls.map(url => url.href);
3067  // We just use the hashes, since updating a few additional urls won't hurt.
3068  for (let chunk of PlacesUtils.chunkArray(hrefs, db.variableLimit)) {
3069    await db.execute(
3070      `UPDATE moz_places
3071       SET hidden = (url_hash BETWEEN hash("place", "prefix_lo") AND hash("place", "prefix_hi")),
3072           frecency = CALCULATE_FRECENCY(id)
3073       WHERE url_hash IN (${sqlBindPlaceholders(chunk, "hash(", ")")})`,
3074      chunk
3075    );
3076  }
3077
3078  // Trigger frecency updates for all affected origins.
3079  await db.executeCached(`DELETE FROM moz_updateoriginsupdate_temp`);
3080
3081  PlacesObservers.notifyListeners([new PlacesRanking()]);
3082};
3083
3084/**
3085 * Removes any orphan annotation entries.
3086 *
3087 * @param db
3088 *        the Sqlite.jsm connection handle.
3089 */
3090var removeOrphanAnnotations = async function(db) {
3091  await db.executeCached(
3092    `DELETE FROM moz_items_annos
3093     WHERE id IN (SELECT a.id from moz_items_annos a
3094                  LEFT JOIN moz_bookmarks b ON a.item_id = b.id
3095                  WHERE b.id ISNULL)
3096    `
3097  );
3098  await db.executeCached(
3099    `DELETE FROM moz_anno_attributes
3100     WHERE id IN (SELECT n.id from moz_anno_attributes n
3101                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
3102                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
3103                  WHERE a1.id ISNULL AND a2.id ISNULL)
3104    `
3105  );
3106};
3107
3108/**
3109 * Removes annotations for a given item.
3110 *
3111 * @param db
3112 *        the Sqlite.jsm connection handle.
3113 * @param items
3114 *        The items for which to remove annotations.
3115 */
3116var removeAnnotationsForItems = async function(db, items) {
3117  // Remove the annotations.
3118  let itemIds = items.map(item => item._id);
3119  await db.executeCached(
3120    `DELETE FROM moz_items_annos
3121     WHERE item_id IN (${sqlBindPlaceholders(itemIds)})`,
3122    itemIds
3123  );
3124  await db.executeCached(
3125    `DELETE FROM moz_anno_attributes
3126     WHERE id IN (SELECT n.id from moz_anno_attributes n
3127                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
3128                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
3129                  WHERE a1.id ISNULL AND a2.id ISNULL)
3130    `
3131  );
3132};
3133
3134/**
3135 * Updates lastModified for all the ancestors of a given folder GUID.
3136 *
3137 * @param db
3138 *        the Sqlite.jsm connection handle.
3139 * @param folderGuid
3140 *        the GUID of the folder whose ancestors should be updated.
3141 * @param time
3142 *        a Date object to use for the update.
3143 *
3144 * @note the folder itself is also updated.
3145 */
3146var setAncestorsLastModified = async function(
3147  db,
3148  folderGuid,
3149  time,
3150  syncChangeDelta
3151) {
3152  await db.executeCached(
3153    `WITH RECURSIVE
3154     ancestors(aid) AS (
3155       SELECT id FROM moz_bookmarks WHERE guid = :guid
3156       UNION ALL
3157       SELECT parent FROM moz_bookmarks
3158       JOIN ancestors ON id = aid
3159       WHERE type = :type
3160     )
3161     UPDATE moz_bookmarks SET lastModified = :time
3162     WHERE id IN ancestors
3163    `,
3164    {
3165      guid: folderGuid,
3166      type: Bookmarks.TYPE_FOLDER,
3167      time: PlacesUtils.toPRTime(time),
3168    }
3169  );
3170
3171  if (syncChangeDelta) {
3172    // Flag the folder as having a change.
3173    await db.executeCached(
3174      `
3175      UPDATE moz_bookmarks SET
3176        syncChangeCounter = syncChangeCounter + :syncChangeDelta
3177      WHERE guid = :guid`,
3178      { guid: folderGuid, syncChangeDelta }
3179    );
3180  }
3181};
3182
3183/**
3184 * Remove all descendants of one or more bookmark folders.
3185 *
3186 * @param {Object} db
3187 *        the Sqlite.jsm connection handle.
3188 * @param {Array} folderGuids
3189 *        array of folder guids.
3190 * @return {Array}
3191 *         An array of urls that will need to be updated for frecency. These
3192 *         are returned rather than updated immediately so that the caller
3193 *         can decide when they need to be updated - they do not need to
3194 *         stop this function from completing.
3195 */
3196var removeFoldersContents = async function(db, folderGuids, options) {
3197  let syncChangeDelta = PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
3198    options.source
3199  );
3200
3201  let itemsRemoved = [];
3202  for (let folderGuid of folderGuids) {
3203    let rows = await db.executeCached(
3204      `WITH RECURSIVE
3205       descendants(did) AS (
3206         SELECT b.id FROM moz_bookmarks b
3207         JOIN moz_bookmarks p ON b.parent = p.id
3208         WHERE p.guid = :folderGuid
3209         UNION ALL
3210         SELECT id FROM moz_bookmarks
3211         JOIN descendants ON parent = did
3212       )
3213       SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
3214              b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
3215              b.lastModified, IFNULL(b.title, '') AS title,
3216              p.parent AS _grandParentId, NULL AS _childCount,
3217              b.syncStatus AS _syncStatus
3218       FROM descendants
3219       /* The usage of CROSS JOIN is not random, it tells the optimizer
3220          to retain the original rows order, so the hierarchy is respected */
3221       CROSS JOIN moz_bookmarks b ON did = b.id
3222       JOIN moz_bookmarks p ON p.id = b.parent
3223       LEFT JOIN moz_places h ON b.fk = h.id`,
3224      { folderGuid }
3225    );
3226
3227    itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows, true));
3228
3229    await db.executeCached(
3230      `WITH RECURSIVE
3231       descendants(did) AS (
3232         SELECT b.id FROM moz_bookmarks b
3233         JOIN moz_bookmarks p ON b.parent = p.id
3234         WHERE p.guid = :folderGuid
3235         UNION ALL
3236         SELECT id FROM moz_bookmarks
3237         JOIN descendants ON parent = did
3238       )
3239       DELETE FROM moz_bookmarks WHERE id IN descendants`,
3240      { folderGuid }
3241    );
3242  }
3243
3244  // Write tombstones for removed items.
3245  await insertTombstones(db, itemsRemoved, syncChangeDelta);
3246
3247  // Bump the change counter for all tagged bookmarks when removing tag
3248  // folders.
3249  await addSyncChangesForRemovedTagFolders(db, itemsRemoved, syncChangeDelta);
3250
3251  // Cleanup orphans.
3252  await removeOrphanAnnotations(db);
3253
3254  // TODO (Bug 1087576): this may leave orphan tags behind.
3255
3256  // Send onItemRemoved notifications to listeners.
3257  // TODO (Bug 1087580): for the case of eraseEverything, this should send a
3258  // single clear bookmarks notification rather than notifying for each
3259  // bookmark.
3260
3261  // Notify listeners in reverse order to serve children before parents.
3262  let { source = Bookmarks.SOURCES.DEFAULT } = options;
3263  let notifications = [];
3264  for (let item of itemsRemoved.reverse()) {
3265    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
3266    let url = "";
3267    if (item.type == Bookmarks.TYPE_BOOKMARK) {
3268      url = item.hasOwnProperty("url") ? item.url.href : null;
3269    }
3270    notifications.push(
3271      new PlacesBookmarkRemoved({
3272        id: item._id,
3273        url,
3274        parentId: item._parentId,
3275        index: item.index,
3276        itemType: item.type,
3277        guid: item.guid,
3278        parentGuid: item.parentGuid,
3279        source,
3280        isTagging: isUntagging,
3281        isDescendantRemoval: !PlacesUtils.bookmarks.userContentRoots.includes(
3282          item.parentGuid
3283        ),
3284      })
3285    );
3286
3287    if (isUntagging) {
3288      const tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(url));
3289      for (let entry of await fetchBookmarksByURL(item, true)) {
3290        notifications.push(
3291          new PlacesBookmarkTags({
3292            id: entry._id,
3293            itemType: entry.type,
3294            url,
3295            guid: entry.guid,
3296            parentGuid: entry.parentGuid,
3297            tags,
3298            lastModified: entry.lastModified,
3299            source,
3300            isTagging: false,
3301          })
3302        );
3303      }
3304    }
3305  }
3306
3307  if (notifications.length) {
3308    PlacesObservers.notifyListeners(notifications);
3309  }
3310
3311  return itemsRemoved.filter(item => "url" in item).map(item => item.url);
3312};
3313
3314/**
3315 * Tries to insert a new place if it doesn't exist yet.
3316 * @param url
3317 *        A valid URL object.
3318 * @return {Promise} resolved when the operation is complete.
3319 */
3320async function maybeInsertPlace(db, url) {
3321  // The IGNORE conflict can trigger on `guid`.
3322  await db.executeCached(
3323    `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid)
3324     VALUES (:url, hash(:url), :rev_host, 0, :frecency,
3325             IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
3326                    GENERATE_GUID()))
3327    `,
3328    {
3329      url: url.href,
3330      rev_host: PlacesUtils.getReversedHost(url),
3331      frecency: url.protocol == "place:" ? 0 : -1,
3332    }
3333  );
3334  await db.executeCached("DELETE FROM moz_updateoriginsinsert_temp");
3335}
3336
3337/**
3338 * Tries to insert a new place if it doesn't exist yet.
3339 * @param db
3340 *        The database to use
3341 * @param urls
3342 *        An array with all the url objects to insert.
3343 * @return {Promise} resolved when the operation is complete.
3344 */
3345async function maybeInsertManyPlaces(db, urls) {
3346  await db.executeCached(
3347    `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid) VALUES
3348     (:url, hash(:url), :rev_host, 0, :frecency,
3349     IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url), :maybeguid))`,
3350    urls.map(url => ({
3351      url: url.href,
3352      rev_host: PlacesUtils.getReversedHost(url),
3353      frecency: url.protocol == "place:" ? 0 : -1,
3354      maybeguid: PlacesUtils.history.makeGuid(),
3355    }))
3356  );
3357  await db.executeCached("DELETE FROM moz_updateoriginsinsert_temp");
3358}
3359
3360// Indicates whether we should write a tombstone for an item that has been
3361// uploaded to the server. We ignore "NEW" and "UNKNOWN" items: "NEW" items
3362// haven't been uploaded yet, and "UNKNOWN" items need a full reconciliation
3363// with the server.
3364function needsTombstone(item) {
3365  return item._syncStatus == Bookmarks.SYNC_STATUS.NORMAL;
3366}
3367
3368// Inserts tombstones for removed synced items.
3369function insertTombstones(db, itemsRemoved, syncChangeDelta) {
3370  if (!syncChangeDelta) {
3371    return Promise.resolve();
3372  }
3373  let syncedItems = itemsRemoved.filter(needsTombstone);
3374  if (!syncedItems.length) {
3375    return Promise.resolve();
3376  }
3377  let dateRemoved = PlacesUtils.toPRTime(Date.now());
3378  let valuesTable = syncedItems
3379    .map(
3380      item => `(
3381    ${JSON.stringify(item.guid)},
3382    ${dateRemoved}
3383  )`
3384    )
3385    .join(",");
3386  return db.execute(`
3387    INSERT INTO moz_bookmarks_deleted (guid, dateRemoved)
3388    VALUES ${valuesTable}`);
3389}
3390
3391// Bumps the change counter for all bookmarks with URLs referenced in removed
3392// tag folders.
3393var addSyncChangesForRemovedTagFolders = async function(
3394  db,
3395  itemsRemoved,
3396  syncChangeDelta
3397) {
3398  if (!syncChangeDelta) {
3399    return;
3400  }
3401  for (let item of itemsRemoved) {
3402    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
3403    if (isUntagging) {
3404      await PlacesSyncUtils.bookmarks.addSyncChangesForBookmarksWithURL(
3405        db,
3406        item.url,
3407        syncChangeDelta
3408      );
3409    }
3410  }
3411};
3412
3413function adjustSeparatorsSyncCounter(
3414  db,
3415  parentId,
3416  startIndex,
3417  syncChangeDelta
3418) {
3419  if (!syncChangeDelta) {
3420    return Promise.resolve();
3421  }
3422
3423  return db.executeCached(
3424    `
3425    UPDATE moz_bookmarks
3426    SET syncChangeCounter = syncChangeCounter + :delta
3427    WHERE parent = :parent AND position >= :start_index
3428      AND type = :item_type
3429    `,
3430    {
3431      delta: syncChangeDelta,
3432      parent: parentId,
3433      start_index: startIndex,
3434      item_type: Bookmarks.TYPE_SEPARATOR,
3435    }
3436  );
3437}
3438
3439/**
3440 * Generates a list of "?" SQL bindings based on input array length.
3441 * @param {array} values an array of values.
3442 * @param {string} [prefix] a string to prefix to the placeholder.
3443 * @param {string} [suffix] a string to suffix to the placeholder.
3444 * @returns {string} placeholders is a string made of question marks and commas,
3445 *          one per value.
3446 */
3447function sqlBindPlaceholders(values, prefix = "", suffix = "") {
3448  return new Array(values.length).fill(prefix + "?" + suffix).join(",");
3449}
3450
3451/**
3452 * Return the full path, from parent to root folder, of a bookmark.
3453 *
3454 * @param guid
3455 *        The globally unique identifier of the item to determine the full
3456 *        bookmark path for.
3457 * @param options [optional]
3458 *        an optional object whose properties describe options for the query:
3459 *         - concurrent:  Queries concurrently to any writes, returning results
3460 *                        faster. On the negative side, it may return stale
3461 *                        information missing the currently ongoing write.
3462 *         - db:          A specific connection to be used.
3463 * @return {Promise} resolved when the query is complete.
3464 * @resolves to an array of {guid, title} objects that represent the full path
3465 *           from parent to root for the passed in bookmark.
3466 * @rejects if an error happens while querying.
3467 */
3468async function retrieveFullBookmarkPath(guid, options = {}) {
3469  let query = async function(db) {
3470    let rows = await db.executeCached(
3471      `WITH RECURSIVE parents(guid, _id, _parent, title) AS
3472          (SELECT guid, id AS _id, parent AS _parent,
3473                  IFNULL(title, '') AS title
3474           FROM moz_bookmarks
3475           WHERE guid = :pguid
3476           UNION ALL
3477           SELECT b.guid, b.id AS _id, b.parent AS _parent,
3478                  IFNULL(b.title, '') AS title
3479           FROM moz_bookmarks b
3480           INNER JOIN parents ON b.id=parents._parent)
3481        SELECT * FROM parents WHERE guid != :rootGuid;
3482      `,
3483      { pguid: guid, rootGuid: PlacesUtils.bookmarks.rootGuid }
3484    );
3485
3486    return rows.reverse().map(r => ({
3487      guid: r.getResultByName("guid"),
3488      title: r.getResultByName("title"),
3489    }));
3490  };
3491
3492  if (options.concurrent) {
3493    let db = await PlacesUtils.promiseDBConnection();
3494    return query(db);
3495  }
3496  return PlacesUtils.withConnectionWrapper(
3497    "Bookmarks.jsm: retrieveFullBookmarkPath",
3498    query
3499  );
3500}
3501