Bug 1274108 - Extract internal bookmarks helpers into a `BookmarkUtils` module. draft
authorKit Cambridge <kcambridge@mozilla.com>
Fri, 03 Jun 2016 10:39:33 -0700
changeset 375300 5d3982be429442a41785a5d8a0bfe555d4b36efa
parent 375299 4384b8de2b3aa506150ea72cdcb15dff7a829edd
child 375301 96e1d70f073fea59f744a7853151215b4a0169e3
child 375311 28db65c232a702bcce7905ec243586afab63a775
push id20219
push userkcambridge@mozilla.com
push dateFri, 03 Jun 2016 21:20:40 +0000
bugs1274108
milestone49.0a1
Bug 1274108 - Extract internal bookmarks helpers into a `BookmarkUtils` module. MozReview-Commit-ID: HAf9UBaSDjB
toolkit/components/places/BookmarkUtils.jsm
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/moz.build
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/BookmarkUtils.jsm
@@ -0,0 +1,912 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "BookmarkUtils", "BookmarkValidators" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
+                                  "resource://gre/modules/Bookmarks.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+
+// Imposed to limit database size.
+const DB_URL_LENGTH_MAX = 65536;
+const DB_TITLE_LENGTH_MAX = 4096;
+
+const MATCH_ANYWHERE_UNMODIFIED = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
+const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+
+var BookmarkValidators = {
+  validateBookmarkObject(input, behavior) {
+    return validateBookmarkProperties(VALIDATORS, input, behavior);
+  },
+
+  isValidGuid(v) {
+    return typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v);
+  },
+};
+
+var BookmarkUtils = {
+  generateGuid(db) {
+    return db.executeCached("SELECT GENERATE_GUID() AS guid").then(rows => rows[0].getResultByName("guid"));
+  },
+
+  ////////////////////////////////////////////////////////////////////////////////
+  // Insert implementation.
+
+  insertBookmark(item, parent, { preProcess, postProcess } = {}) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmark",
+      Task.async(function*(db) {
+
+      // If a guid was not provided, generate one, so we won't need to fetch the
+      // bookmark just after having created it.
+      if (!item.hasOwnProperty("guid"))
+        item.guid = yield BookmarkUtils.generateGuid(db);
+
+      yield db.executeTransaction(function* transaction() {
+        if (preProcess) {
+          item = yield preProcess(db, item);
+        }
+
+        yield BookmarkUtils.insertBookmarkInto(db, item, parent);
+
+        if (postProcess) {
+          item = yield postProcess(db, item);
+        }
+      });
+
+      // If not a tag recalculate frecency...
+      let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
+      if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
+        // ...though we don't wait for the calculation.
+        BookmarkUtils.updateFrecency(db, [item.url]).then(null, Cu.reportError);
+      }
+
+      // Don't return an empty title to the caller.
+      if (item.hasOwnProperty("title") && item.title === null)
+        delete item.title;
+
+      return item;
+    }));
+  },
+
+  insertBookmarkInto: Task.async(function* (db, item, parent) {
+    if (item.type == Bookmarks.TYPE_BOOKMARK) {
+      // Ensure a page exists in moz_places for this URL.
+      yield db.executeCached(
+        `INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
+         VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
+        `, { url: item.url.href, rev_host: PlacesUtils.getReversedHost(item.url),
+             frecency: item.url.protocol == "place:" ? 0 : -1 });
+    }
+
+    // Adjust indices.
+    yield db.executeCached(
+      `UPDATE moz_bookmarks SET position = position + 1
+       WHERE parent = :parent
+       AND position >= :index
+      `, { parent: parent._id, index: item.index });
+
+    // Insert the bookmark into the database.
+    yield db.executeCached(
+      `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
+                                  dateAdded, lastModified, guid)
+       VALUES ((SELECT id FROM moz_places WHERE url = :url), :type, :parent,
+               :index, :title, :date_added, :last_modified, :guid)
+      `, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
+           type: item.type, parent: parent._id, index: item.index,
+           title: item.title, date_added: BookmarkUtils.toPRTime(item.dateAdded),
+           last_modified: BookmarkUtils.toPRTime(item.lastModified), guid: item.guid });
+
+    yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
+  }),
+
+  ////////////////////////////////////////////////////////////////////////////////
+  // Update implementation.
+
+  updateBookmark(info, item, newParent, { preProcess, postProcess } = {}) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
+      Task.async(function*(db) {
+
+      let tuples = new Map();
+      if (info.hasOwnProperty("lastModified"))
+        tuples.set("lastModified", { value: BookmarkUtils.toPRTime(info.lastModified) });
+      if (info.hasOwnProperty("title"))
+        tuples.set("title", { value: info.title });
+
+      yield db.executeTransaction(function* () {
+        if (info.hasOwnProperty("url")) {
+          // Ensure a page exists in moz_places for this URL.
+          yield db.executeCached(
+            `INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
+             VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
+            `, { url: info.url ? info.url.href : null,
+                 rev_host: PlacesUtils.getReversedHost(info.url),
+                 frecency: info.url.protocol == "place:" ? 0 : -1 });
+          tuples.set("url", { value: info.url.href
+                            , fragment: "fk = (SELECT id FROM moz_places WHERE url = :url)" });
+        }
+
+        if (newParent) {
+          // For simplicity, update the index regardless.
+          let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
+          tuples.set("position", { value: newIndex });
+
+          if (newParent.guid == item.parentGuid) {
+            // Moving inside the original container.
+            // When moving "up", add 1 to each index in the interval.
+            // Otherwise when moving down, we subtract 1.
+            let sign = newIndex < item.index ? +1 : -1;
+            yield db.executeCached(
+              `UPDATE moz_bookmarks SET position = position + :sign
+               WHERE parent = :newParentId
+                 AND position BETWEEN :lowIndex AND :highIndex
+              `, { sign: sign, newParentId: newParent._id,
+                   lowIndex: Math.min(item.index, newIndex),
+                   highIndex: Math.max(item.index, newIndex) });
+          } else {
+            // Moving across different containers.
+            tuples.set("parent", { value: newParent._id} );
+            yield db.executeCached(
+              `UPDATE moz_bookmarks SET position = position + :sign
+               WHERE parent = :oldParentId
+                 AND position >= :oldIndex
+              `, { sign: -1, oldParentId: item._parentId, oldIndex: item.index });
+            yield db.executeCached(
+              `UPDATE moz_bookmarks SET position = position + :sign
+               WHERE parent = :newParentId
+                 AND position >= :newIndex
+              `, { sign: +1, newParentId: newParent._id, newIndex: newIndex });
+
+            yield setAncestorsLastModified(db, item.parentGuid, info.lastModified);
+          }
+          yield setAncestorsLastModified(db, newParent.guid, info.lastModified);
+        }
+
+        yield db.executeCached(
+          `UPDATE moz_bookmarks
+           SET ${Array.from(tuples.keys()).map(v => tuples.get(v).fragment || `${v} = :${v}`).join(", ")}
+           WHERE guid = :guid
+          `, Object.assign({ guid: info.guid },
+                           [...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
+      });
+
+      // If the parent changed, update related non-enumerable properties.
+      let additionalParentInfo = {};
+      if (newParent) {
+        Object.defineProperty(additionalParentInfo, "_parentId",
+                              { value: newParent._id, enumerable: false });
+        Object.defineProperty(additionalParentInfo, "_grandParentId",
+                              { value: newParent._parentId, enumerable: false });
+      }
+
+      let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
+
+      // Don't return an empty title to the caller.
+      if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
+        delete updatedItem.title;
+
+      return updatedItem;
+    }));
+  },
+
+  ////////////////////////////////////////////////////////////////////////////////
+  // Remove implementation.
+
+  removeBookmark(item, options) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
+      Task.async(function*(db) {
+
+      let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
+
+      yield db.executeTransaction(function* transaction() {
+        // If it's a folder, remove its contents first.
+        if (item.type == Bookmarks.TYPE_FOLDER) {
+          if (options.preventRemovalOfNonEmptyFolders && item._childCount > 0) {
+            throw new Error("Cannot remove a non-empty folder.");
+          }
+          yield BookmarkUtils.removeFoldersContents(db, [item.guid]);
+        }
+
+        // Remove annotations first.  If it's a tag, we can avoid paying that cost.
+        if (!isUntagging) {
+          // We don't go through the annotations service for this cause otherwise
+          // we'd get a pointless onItemChanged notification and it would also
+          // set lastModified to an unexpected value.
+          yield removeAnnotationsForItem(db, item._id);
+        }
+
+        // Remove the bookmark from the database.
+        yield db.executeCached(
+          `DELETE FROM moz_bookmarks WHERE guid = :guid`, { guid: item.guid });
+
+        // Fix indices in the parent.
+        yield db.executeCached(
+          `UPDATE moz_bookmarks SET position = position - 1 WHERE
+           parent = :parentId AND position > :index
+          `, { parentId: item._parentId, index: item.index });
+
+        yield setAncestorsLastModified(db, item.parentGuid, new Date());
+      });
+
+      // If not a tag recalculate frecency...
+      if (item.type == Bookmarks.TYPE_BOOKMARK && !isUntagging) {
+        // ...though we don't wait for the calculation.
+        BookmarkUtils.updateFrecency(db, [item.url]).then(null, Cu.reportError);
+      }
+
+      return item;
+    }));
+  },
+
+  ////////////////////////////////////////////////////////////////////////////////
+  // Reorder implementation.
+
+  reorderChildren(parent, orderedChildrenGuids) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
+      db => db.executeTransaction(function* () {
+        // Select all of the direct children for the given parent.
+        let children = yield BookmarkUtils.fetchBookmarksByParent({ parentGuid: parent.guid });
+        if (!children.length)
+          return undefined;
+
+        // Reorder the children array according to the specified order, provided
+        // GUIDs come first, others are appended in somehow random order.
+        children.sort((a, b) => {
+          let i = orderedChildrenGuids.indexOf(a.guid);
+          let j = orderedChildrenGuids.indexOf(b.guid);
+          // This works provided fetchBookmarksByParent returns sorted children.
+          if (i == -1 && j == -1)
+            return 0;
+          return (i != -1 && j != -1 && i < j) || (i != -1 && j == -1) ? -1 : 1;
+         });
+
+        // Update the bookmarks position now.  If any unknown guid have been
+        // inserted meanwhile, its position will be set to -position, and we'll
+        // handle it later.
+        // To do the update in a single step, we build a VALUES (guid, position)
+        // table.  We then use count() in the sorting table to avoid skipping values
+        // when no more existing GUIDs have been provided.
+        let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
+                                  .join();
+        yield db.execute(
+          `WITH sorting(g, p) AS (
+             VALUES ${valuesTable}
+           )
+           UPDATE moz_bookmarks SET position = (
+             SELECT CASE count(a.g) WHEN 0 THEN -position
+                                    ELSE count(a.g) - 1
+                    END
+             FROM sorting a
+             JOIN sorting b ON b.p <= a.p
+             WHERE a.g = guid
+               AND parent = :parentId
+          )`, { parentId: parent._id});
+
+        // Update position of items that could have been inserted in the meanwhile.
+        // Since this can happen rarely and it's only done for schema coherence
+        // resonds, we won't notify about these changes.
+        yield db.executeCached(
+          `CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
+             AFTER UPDATE OF position ON moz_bookmarks
+             WHEN NEW.position = -1
+           BEGIN
+             UPDATE moz_bookmarks
+             SET position = (SELECT MAX(position) FROM moz_bookmarks
+                             WHERE parent = NEW.parent) +
+                            (SELECT count(*) FROM moz_bookmarks
+                             WHERE parent = NEW.parent
+                               AND position BETWEEN OLD.position AND -1)
+             WHERE guid = NEW.guid;
+           END
+          `);
+
+        yield db.executeCached(
+          `UPDATE moz_bookmarks SET position = -1 WHERE position < 0`);
+
+        yield db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
+
+        return children;
+      }.bind(this))
+    );
+  },
+
+  ////////////////////////////////////////////////////////////////////////////////
+  // Query implementation.
+
+  queryBookmarks(info) {
+    let queryParams = {tags_folder: PlacesUtils.tagsFolderId};
+    // we're searching for bookmarks, so exclude tags
+    let queryString = "WHERE p.parent <> :tags_folder";
+
+    if (info.title) {
+      queryString += " AND b.title = :title";
+      queryParams.title = info.title;
+    }
+
+    if (info.url) {
+      queryString += " AND h.url = :url";
+      queryParams.url = info.url;
+    }
+
+    if (info.query) {
+      queryString += " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior) ";
+      queryParams.query = info.query;
+      queryParams.matchBehavior = MATCH_ANYWHERE_UNMODIFIED;
+      queryParams.searchBehavior = BEHAVIOR_BOOKMARK;
+    }
+
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks",
+      Task.async(function*(db) {
+
+      // _id, _childCount, _grandParentId and _parentId fields
+      // are required to be in the result by the converting function
+      // hence setting them to NULL
+      let rows = yield db.executeCached(
+        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+                b.dateAdded, b.lastModified, b.type, b.title,
+                h.url AS url, b.parent, p.parent,
+                NULL AS _id,
+                NULL AS _childCount,
+                NULL AS _grandParentId,
+                NULL AS _parentId
+         FROM moz_bookmarks b
+         LEFT JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON h.id = b.fk
+         ${queryString}
+        `, queryParams);
+
+      return BookmarkUtils.rowsToItemsArray(rows);
+    }));
+  },
+
+  ////////////////////////////////////////////////////////////////////////////////
+  // Fetch implementation.
+
+  fetchBookmark(info) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmark",
+      Task.async(function*(db) {
+
+      let rows = yield db.executeCached(
+        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+                b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+                b.id AS _id, b.parent AS _parentId,
+                (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+                p.parent AS _grandParentId
+         FROM moz_bookmarks b
+         LEFT JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON h.id = b.fk
+         WHERE b.guid = :guid
+        `, { guid: info.guid });
+
+      return rows.length ? BookmarkUtils.rowsToItemsArray(rows)[0] : null;
+    }));
+  },
+
+  fetchBookmarkByPosition(info) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarkByPosition",
+      Task.async(function*(db) {
+      let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
+
+      let rows = yield db.executeCached(
+        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+                b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+                b.id AS _id, b.parent AS _parentId,
+                (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+                p.parent AS _grandParentId
+         FROM moz_bookmarks b
+         LEFT JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON h.id = b.fk
+         WHERE p.guid = :parentGuid
+         AND b.position = IFNULL(:index, (SELECT count(*) - 1
+                                          FROM moz_bookmarks
+                                          WHERE parent = p.id))
+        `, { parentGuid: info.parentGuid, index });
+
+      return rows.length ? BookmarkUtils.rowsToItemsArray(rows)[0] : null;
+    }));
+  },
+
+  fetchBookmarksByURL(info) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByURL",
+      Task.async(function*(db) {
+
+      let rows = yield db.executeCached(
+        `/* do not warn (bug no): not worth to add an index */
+         SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+                b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+                b.id AS _id, b.parent AS _parentId,
+                (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+                p.parent AS _grandParentId
+         FROM moz_bookmarks b
+         LEFT JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON h.id = b.fk
+         WHERE h.url = :url
+         AND _grandParentId <> :tags_folder
+         ORDER BY b.lastModified DESC
+        `, { url: info.url.href,
+             tags_folder: PlacesUtils.tagsFolderId });
+
+      return rows.length ? BookmarkUtils.rowsToItemsArray(rows) : null;
+    }));
+  },
+
+  fetchRecentBookmarks(numberOfItems) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchRecentBookmarks",
+      Task.async(function*(db) {
+
+      let rows = yield db.executeCached(
+        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+                b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+                NULL AS _id, NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId
+         FROM moz_bookmarks b
+         LEFT JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON h.id = b.fk
+         WHERE p.parent <> :tags_folder
+         ORDER BY b.dateAdded DESC, b.ROWID DESC
+         LIMIT :numberOfItems
+        `, { tags_folder: PlacesUtils.tagsFolderId, numberOfItems });
+
+      return rows.length ? BookmarkUtils.rowsToItemsArray(rows) : [];
+    }));
+  },
+
+  fetchBookmarksByParent(info) {
+    return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByParent",
+      Task.async(function*(db) {
+
+      let rows = yield db.executeCached(
+        `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+                b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+                b.id AS _id, b.parent AS _parentId,
+                (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+                p.parent AS _grandParentId
+         FROM moz_bookmarks b
+         LEFT JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON h.id = b.fk
+         WHERE p.guid = :parentGuid
+         ORDER BY b.position ASC
+        `, { parentGuid: info.parentGuid });
+
+      return BookmarkUtils.rowsToItemsArray(rows);
+    }));
+  },
+
+  /**
+   * Sends a bookmarks notification through the given observers.
+   *
+   * @param observers
+   *        array of nsINavBookmarkObserver objects.
+   * @param notification
+   *        the notification name.
+   * @param args
+   *        array of arguments to pass to the notification.
+   */
+  notify(observers, notification, args) {
+    for (let observer of observers) {
+      try {
+        observer[notification](...args);
+      } catch (ex) {}
+    }
+  },
+
+  /**
+   * Converts an URL object to an nsIURI.
+   *
+   * @param url
+   *        the URL object to convert.
+   * @return nsIURI for the given URL.
+   */
+  toURI(url) {
+    return NetUtil.newURI(url.href);
+  },
+
+  /**
+   * Convert a Date object to a PRTime (microseconds).
+   *
+   * @param date
+   *        the Date object to convert.
+   * @return microseconds from the epoch.
+   */
+  toPRTime(date) {
+    return date * 1000;
+  },
+
+  /**
+   * Convert a PRTime to a Date object.
+   *
+   * @param time
+   *        microseconds from the epoch.
+   * @return a Date object.
+   */
+  toDate(time) {
+    return new Date(parseInt(time / 1000));
+  },
+
+  /**
+   * Convert an array of mozIStorageRow objects to an array of bookmark objects.
+   *
+   * @param rows
+   *        the array of mozIStorageRow objects.
+   * @return an array of bookmark objects.
+   */
+  rowsToItemsArray(rows) {
+    return rows.map(row => {
+      let item = {};
+      for (let prop of ["guid", "index", "type"]) {
+        item[prop] = row.getResultByName(prop);
+      }
+      for (let prop of ["dateAdded", "lastModified"]) {
+        item[prop] = BookmarkUtils.toDate(row.getResultByName(prop));
+      }
+      for (let prop of ["title", "parentGuid", "url" ]) {
+        let val = row.getResultByName(prop);
+        if (val)
+          item[prop] = prop === "url" ? new URL(val) : val;
+      }
+      for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId"]) {
+        let val = row.getResultByName(prop);
+        if (val !== null) {
+          // These properties should not be returned to the API consumer, thus
+          // they are non-enumerable and removed through Object.assign just before
+          // the object is returned.
+          // Configurable is set to support mergeIntoNewObject overwrites.
+          Object.defineProperty(item, prop, { value: val, enumerable: false,
+                                                          configurable: true });
+        }
+      }
+
+      return item;
+    });
+  },
+
+  /**
+   * Updates frecency for a list of URLs.
+   *
+   * @param db
+   *        the Sqlite.jsm connection handle.
+   * @param urls
+   *        the array of URLs to update.
+   */
+  updateFrecency: Task.async(function* (db, urls) {
+    yield db.execute(
+      `UPDATE moz_places
+       SET frecency = NOTIFY_FRECENCY(
+         CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date
+       ) WHERE url IN ( ${urls.map(url => JSON.stringify(url.href)).join(", ")} )
+      `);
+
+    yield db.execute(
+      `UPDATE moz_places
+       SET hidden = 0
+       WHERE url IN ( ${urls.map(url => JSON.stringify(url.href)).join(", ")} )
+         AND frecency <> 0
+      `);
+  }),
+
+  /**
+   * Remove all descendants of one or more bookmark folders.
+   *
+   * @param db
+   *        the Sqlite.jsm connection handle.
+   * @param folderGuids
+   *        array of folder guids.
+   */
+  removeFoldersContents: Task.async(function* (db, folderGuids) {
+    let itemsRemoved = [];
+    for (let folderGuid of folderGuids) {
+      let rows = yield db.executeCached(
+        `WITH RECURSIVE
+         descendants(did) AS (
+           SELECT b.id FROM moz_bookmarks b
+           JOIN moz_bookmarks p ON b.parent = p.id
+           WHERE p.guid = :folderGuid
+           UNION ALL
+           SELECT id FROM moz_bookmarks
+           JOIN descendants ON parent = did
+         )
+         SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
+                b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
+                b.lastModified, b.title, p.parent AS _grandParentId,
+                NULL AS _childCount
+         FROM moz_bookmarks b
+         JOIN moz_bookmarks p ON p.id = b.parent
+         LEFT JOIN moz_places h ON b.fk = h.id
+         WHERE b.id IN descendants`, { folderGuid });
+
+      itemsRemoved = itemsRemoved.concat(BookmarkUtils.rowsToItemsArray(rows));
+
+      yield db.executeCached(
+        `WITH RECURSIVE
+         descendants(did) AS (
+           SELECT b.id FROM moz_bookmarks b
+           JOIN moz_bookmarks p ON b.parent = p.id
+           WHERE p.guid = :folderGuid
+           UNION ALL
+           SELECT id FROM moz_bookmarks
+           JOIN descendants ON parent = did
+         )
+         DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid });
+    }
+
+    // Cleanup orphans.
+    yield removeOrphanAnnotations(db);
+
+    // TODO (Bug 1087576): this may leave orphan tags behind.
+
+    let urls = itemsRemoved.filter(item => "url" in item).map(item => item.url);
+    BookmarkUtils.updateFrecency(db, urls).then(null, Cu.reportError);
+
+    // Send onItemRemoved notifications to listeners.
+    // TODO (Bug 1087580): for the case of eraseEverything, this should send a
+    // single clear bookmarks notification rather than notifying for each
+    // bookmark.
+
+    // Notify listeners in reverse order to serve children before parents.
+    let observers = PlacesUtils.bookmarks.getObservers();
+    for (let item of itemsRemoved.reverse()) {
+      let uri = item.hasOwnProperty("url") ? BookmarkUtils.toURI(item.url) : null;
+      BookmarkUtils.notify(observers, "onItemRemoved", [ item._id, item._parentId,
+                                           item.index, item.type, uri,
+                                           item.guid, item.parentGuid ]);
+
+      let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
+      if (isUntagging) {
+        for (let entry of (yield BookmarkUtils.fetchBookmarksByURL(item))) {
+          BookmarkUtils.notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+                                               BookmarkUtils.toPRTime(entry.lastModified),
+                                               entry.type, entry._parentId,
+                                               entry.guid, entry.parentGuid,
+                                               "" ]);
+        }
+      }
+    }
+  }),
+
+  /**
+   * Remove properties that have the same value across two bookmark objects.
+   *
+   * @param dest
+   *        destination bookmark object.
+   * @param src
+   *        source bookmark object.
+   * @return a cleaned up bookmark object.
+   * @note "guid" is never removed.
+   */
+  removeSameValueProperties(dest, src) {
+    for (let prop in dest) {
+      let remove = false;
+      switch (prop) {
+        case "lastModified":
+        case "dateAdded":
+          remove = src.hasOwnProperty(prop) && dest[prop].getTime() == src[prop].getTime();
+          break;
+        case "url":
+          remove = src.hasOwnProperty(prop) && dest[prop].href == src[prop].href;
+          break;
+        default:
+          remove = dest[prop] == src[prop];
+      }
+      if (remove && prop != "guid")
+        delete dest[prop];
+    }
+  },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Helpers.
+
+/**
+ * Merges objects into a new object, included non-enumerable properties.
+ *
+ * @param sources
+ *        source objects to merge.
+ * @return a new object including all properties from the source objects.
+ */
+function mergeIntoNewObject(...sources) {
+  let dest = {};
+  for (let src of sources) {
+    for (let prop of Object.getOwnPropertyNames(src)) {
+      Object.defineProperty(dest, prop, Object.getOwnPropertyDescriptor(src, prop));
+    }
+  }
+  return dest;
+}
+
+/**
+ * Executes a boolean validate function, throwing if it returns false.
+ *
+ * @param boolValidateFn
+ *        A boolean validate function.
+ * @return the input value.
+ * @throws if input doesn't pass the validate function.
+ */
+function simpleValidateFunc(boolValidateFn) {
+  return (v, input) => {
+    if (!boolValidateFn(v, input))
+      throw new Error("Invalid value");
+    return v;
+  };
+}
+
+/**
+ * List of validators, one per each known property.
+ * Validators must throw if the property value is invalid and return a fixed up
+ * version of the value, if needed.
+ */
+const VALIDATORS = Object.freeze({
+  guid: simpleValidateFunc(v => BookmarkValidators.isValidGuid(v)),
+  parentGuid: simpleValidateFunc(v => typeof(v) == "string" &&
+                                      /^[a-zA-Z0-9\-_]{12}$/.test(v)),
+  index: simpleValidateFunc(v => Number.isInteger(v) &&
+                                 v >= Bookmarks.DEFAULT_INDEX),
+  dateAdded: simpleValidateFunc(v => v.constructor.name == "Date"),
+  lastModified: simpleValidateFunc(v => v.constructor.name == "Date"),
+  type: simpleValidateFunc(v => Number.isInteger(v) &&
+                                [ Bookmarks.TYPE_BOOKMARK
+                                , Bookmarks.TYPE_FOLDER
+                                , Bookmarks.TYPE_SEPARATOR ].includes(v)),
+  title: v => {
+    simpleValidateFunc(val => val === null || typeof(val) == "string").call(this, v);
+    if (!v)
+      return null;
+    return v.slice(0, DB_TITLE_LENGTH_MAX);
+  },
+  url: v => {
+    simpleValidateFunc(val => (typeof(val) == "string" && val.length <= DB_URL_LENGTH_MAX) ||
+                              (val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
+                              (val instanceof URL && val.href.length <= DB_URL_LENGTH_MAX)
+                      ).call(this, v);
+    if (typeof(v) === "string")
+      return new URL(v);
+    if (v instanceof Ci.nsIURI)
+      return new URL(v.spec);
+    return v;
+  }
+});
+
+/**
+ * Checks validity of a bookmark object, filling up default values for optional
+ * properties.
+ *
+ * @param validators (object)
+          An object containing input validators. Keys should be field names;
+          values should be validation functions.
+ * @param input (object)
+ *        The bookmark object to validate.
+ * @param behavior (object) [optional]
+ *        Object defining special behavior for some of the properties.
+ *        The following behaviors may be optionally set:
+ *         - requiredIf: if the provided condition is satisfied, then this
+ *                       property is required.
+ *         - validIf: if the provided condition is not satisfied, then this
+ *                    property is invalid.
+ *         - defaultValue: an undefined property should default to this value.
+ *
+ * @return a validated and normalized bookmark-item.
+ * @throws if the object contains invalid data.
+ * @note any unknown properties are pass-through.
+ */
+function validateBookmarkProperties(validators, input, behavior={}) {
+  if (!input)
+    throw new Error("Input should be a valid object");
+  let normalizedInput = {};
+  let required = new Set();
+  for (let prop in behavior) {
+    if (behavior[prop].hasOwnProperty("required") && behavior[prop].required) {
+      required.add(prop);
+    }
+    if (behavior[prop].hasOwnProperty("requiredIf") && behavior[prop].requiredIf(input)) {
+      required.add(prop);
+    }
+    if (behavior[prop].hasOwnProperty("validIf") && input[prop] !== undefined &&
+        !behavior[prop].validIf(input)) {
+      throw new Error(`Invalid value for property '${prop}': ${input[prop]}`);
+    }
+    if (behavior[prop].hasOwnProperty("defaultValue") && input[prop] === undefined) {
+      input[prop] = behavior[prop].defaultValue;
+    }
+  }
+
+  for (let prop in input) {
+    if (required.has(prop)) {
+      required.delete(prop);
+    } else if (input[prop] === undefined) {
+      // Skip undefined properties that are not required.
+      continue;
+    }
+    if (validators.hasOwnProperty(prop)) {
+      try {
+        normalizedInput[prop] = validators[prop](input[prop], input);
+      } catch(ex) {
+        throw new Error(`Invalid value for property '${prop}': ${input[prop]}`);
+      }
+    }
+  }
+  if (required.size > 0)
+    throw new Error(`The following properties were expected: ${[...required].join(", ")}`);
+  return normalizedInput;
+}
+
+/**
+ * Removes any orphan annotation entries.
+ *
+ * @param db
+ *        the Sqlite.jsm connection handle.
+ */
+var removeOrphanAnnotations = Task.async(function* (db) {
+  yield db.executeCached(
+    `DELETE FROM moz_items_annos
+     WHERE id IN (SELECT a.id from moz_items_annos a
+                  LEFT JOIN moz_bookmarks b ON a.item_id = b.id
+                  WHERE b.id ISNULL)
+    `);
+  yield db.executeCached(
+    `DELETE FROM moz_anno_attributes
+     WHERE id IN (SELECT n.id from moz_anno_attributes n
+                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
+                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
+                  WHERE a1.id ISNULL AND a2.id ISNULL)
+    `);
+});
+
+/**
+ * Removes annotations for a given item.
+ *
+ * @param db
+ *        the Sqlite.jsm connection handle.
+ * @param itemId
+ *        internal id of the item for which to remove annotations.
+ */
+var removeAnnotationsForItem = Task.async(function* (db, itemId) {
+  yield db.executeCached(
+    `DELETE FROM moz_items_annos
+     WHERE item_id = :id
+    `, { id: itemId });
+  yield db.executeCached(
+    `DELETE FROM moz_anno_attributes
+     WHERE id IN (SELECT n.id from moz_anno_attributes n
+                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
+                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
+                  WHERE a1.id ISNULL AND a2.id ISNULL)
+    `);
+});
+
+/**
+ * Updates lastModified for all the ancestors of a given folder GUID.
+ *
+ * @param db
+ *        the Sqlite.jsm connection handle.
+ * @param folderGuid
+ *        the GUID of the folder whose ancestors should be updated.
+ * @param time
+ *        a Date object to use for the update.
+ *
+ * @note the folder itself is also updated.
+ */
+var setAncestorsLastModified = Task.async(function* (db, folderGuid, time) {
+  yield db.executeCached(
+    `WITH RECURSIVE
+     ancestors(aid) AS (
+       SELECT id FROM moz_bookmarks WHERE guid = :guid
+       UNION ALL
+       SELECT parent FROM moz_bookmarks
+       JOIN ancestors ON id = aid
+       WHERE type = :type
+     )
+     UPDATE moz_bookmarks SET lastModified = :time
+     WHERE id IN ancestors
+    `, { guid: folderGuid, type: Bookmarks.TYPE_FOLDER,
+         time: BookmarkUtils.toPRTime(time) });
+});
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -60,36 +60,25 @@
 
 this.EXPORTED_SYMBOLS = [ "Bookmarks" ];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 Cu.importGlobalProperties(["URL"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-                                  "resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-                                  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkUtils",
+                                  "resource://gre/modules/BookmarkUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkValidators",
+                                  "resource://gre/modules/BookmarkUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
-                                  "resource://gre/modules/Sqlite.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 
-// Imposed to limit database size.
-const DB_URL_LENGTH_MAX = 65536;
-const DB_TITLE_LENGTH_MAX = 4096;
-
-const MATCH_ANYWHERE_UNMODIFIED = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
-const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
-
 var Bookmarks = Object.freeze({
   /**
    * Item's type constants.
    * These should stay consistent with nsINavBookmarksService.idl
    */
   TYPE_BOOKMARK: 1,
   TYPE_FOLDER: 2,
   TYPE_SEPARATOR: 3,
@@ -137,17 +126,17 @@ var Bookmarks = Object.freeze({
    * @resolves to an object representing the created bookmark.
    * @rejects if it's not possible to create the requested bookmark.
    * @throws if the arguments are invalid.
    */
   insert(info) {
     // Ensure to use the same date for dateAdded and lastModified, even if
     // dateAdded may be imposed by the caller.
     let time = (info && info.dateAdded) || new Date();
-    let insertInfo = validateBookmarkObject(info,
+    let insertInfo = BookmarkValidators.validateBookmarkObject(info,
       { type: { defaultValue: this.TYPE_BOOKMARK }
       , index: { defaultValue: this.DEFAULT_INDEX }
       , url: { requiredIf: b => b.type == this.TYPE_BOOKMARK
              , validIf: b => b.type == this.TYPE_BOOKMARK }
       , parentGuid: { required: true }
       , title: { validIf: b => [ this.TYPE_BOOKMARK
                                , this.TYPE_FOLDER ].includes(b.type) }
       , dateAdded: { defaultValue: time
@@ -155,45 +144,45 @@ var Bookmarks = Object.freeze({
                                     b.dateAdded <= b.lastModified }
       , lastModified: { defaultValue: time,
                         validIf: b => (!b.dateAdded && b.lastModified >= time) ||
                                       (b.dateAdded && b.lastModified >= b.dateAdded) }
       });
 
     return Task.spawn(function* () {
       // Ensure the parent exists.
-      let parent = yield fetchBookmark({ guid: insertInfo.parentGuid });
+      let parent = yield BookmarkUtils.fetchBookmark({ guid: insertInfo.parentGuid });
       if (!parent)
         throw new Error("parentGuid must be valid");
 
       // Set index in the appending case.
       if (insertInfo.index == this.DEFAULT_INDEX ||
           insertInfo.index > parent._childCount) {
         insertInfo.index = parent._childCount;
       }
 
-      let item = yield insertBookmark(insertInfo, parent);
+      let item = yield BookmarkUtils.insertBookmark(insertInfo, parent);
 
       // Notify onItemAdded to listeners.
       let observers = PlacesUtils.bookmarks.getObservers();
       // We need the itemId to notify, though once the switch to guids is
       // complete we may stop using it.
-      let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
+      let uri = item.hasOwnProperty("url") ? BookmarkUtils.toURI(item.url) : null;
       let itemId = yield PlacesUtils.promiseItemId(item.guid);
-      notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
+      BookmarkUtils.notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
                                          item.type, uri, item.title || null,
-                                         toPRTime(item.dateAdded), item.guid,
+                                         BookmarkUtils.toPRTime(item.dateAdded), item.guid,
                                          item.parentGuid ]);
 
       // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
       let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
       if (isTagging) {
-        for (let entry of (yield fetchBookmarksByURL(item))) {
-          notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
-                                               toPRTime(entry.lastModified),
+        for (let entry of (yield BookmarkUtils.fetchBookmarksByURL(item))) {
+          BookmarkUtils.notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+                                               BookmarkUtils.toPRTime(entry.lastModified),
                                                entry.type, entry._parentId,
                                                entry.guid, entry.parentGuid,
                                                "" ]);
         }
       }
 
       // Remove non-enumerable properties.
       return Object.assign({}, item);
@@ -218,47 +207,47 @@ var Bookmarks = Object.freeze({
    * @resolves to an object representing the updated bookmark.
    * @rejects if it's not possible to update the given bookmark.
    * @throws if the arguments are invalid.
    */
   update(info) {
     // The info object is first validated here to ensure it's consistent, then
     // it's compared to the existing item to remove any properties that don't
     // need to be updated.
-    let updateInfo = validateBookmarkObject(info,
+    let updateInfo = BookmarkValidators.validateBookmarkObject(info,
       { guid: { required: true }
       , index: { requiredIf: b => b.hasOwnProperty("parentGuid")
                , validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX }
       });
 
     // There should be at last one more property in addition to guid.
     if (Object.keys(updateInfo).length < 2)
       throw new Error("Not enough properties to update");
 
     return Task.spawn(function* () {
       // Ensure the item exists.
-      let item = yield fetchBookmark(updateInfo);
+      let item = yield BookmarkUtils.fetchBookmark(updateInfo);
       if (!item)
         throw new Error("No bookmarks found for the provided GUID");
       if (updateInfo.hasOwnProperty("type") && updateInfo.type != item.type)
         throw new Error("The bookmark type cannot be changed");
       if (updateInfo.hasOwnProperty("dateAdded") &&
           updateInfo.dateAdded.getTime() != item.dateAdded.getTime())
         throw new Error("The bookmark dateAdded cannot be changed");
 
       // Remove any property that will stay the same.
-      removeSameValueProperties(updateInfo, item);
+      BookmarkUtils.removeSameValueProperties(updateInfo, item);
       // Check if anything should still be updated.
       if (Object.keys(updateInfo).length < 2) {
         // Remove non-enumerable properties.
         return Object.assign({}, item);
       }
 
       let time = (updateInfo && updateInfo.dateAdded) || new Date();
-      updateInfo = validateBookmarkObject(updateInfo,
+      updateInfo = BookmarkValidators.validateBookmarkObject(updateInfo,
         { url: { validIf: () => item.type == this.TYPE_BOOKMARK }
         , title: { validIf: () => [ this.TYPE_BOOKMARK
                                   , this.TYPE_FOLDER ].includes(item.type) }
         , lastModified: { defaultValue: new Date()
                         , validIf: b => b.lastModified >= item.dateAdded }
         });
 
       return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: update",
@@ -279,85 +268,85 @@ var Bookmarks = Object.freeze({
                )
                SELECT guid FROM moz_bookmarks
                WHERE id IN descendants
               `, { id: item._id, type: this.TYPE_FOLDER });
             if (rows.map(r => r.getResultByName("guid")).includes(updateInfo.parentGuid))
               throw new Error("Cannot insert a folder into itself or one of its descendants");
           }
 
-          parent = yield fetchBookmark({ guid: updateInfo.parentGuid });
+          parent = yield BookmarkUtils.fetchBookmark({ guid: updateInfo.parentGuid });
           if (!parent)
             throw new Error("No bookmarks found for the provided parentGuid");
         }
 
         if (updateInfo.hasOwnProperty("index")) {
           // If at this point we don't have a parent yet, we are moving into
           // the same container.  Thus we know it exists.
           if (!parent)
-            parent = yield fetchBookmark({ guid: item.parentGuid });
+            parent = yield BookmarkUtils.fetchBookmark({ guid: item.parentGuid });
 
           if (updateInfo.index >= parent._childCount ||
               updateInfo.index == this.DEFAULT_INDEX) {
              updateInfo.index = parent._childCount;
 
             // Fix the index when moving within the same container.
             if (parent.guid == item.parentGuid)
                updateInfo.index--;
           }
         }
 
-        let updatedItem = yield updateBookmark(updateInfo, item, parent);
+        let updatedItem = yield BookmarkUtils.updateBookmark(updateInfo, item, parent);
 
         if (item.type == this.TYPE_BOOKMARK &&
             item.url.href != updatedItem.url.href) {
           // ...though we don't wait for the calculation.
-          updateFrecency(db, [item.url]).then(null, Cu.reportError);
-          updateFrecency(db, [updatedItem.url]).then(null, Cu.reportError);
+          BookmarkUtils.updateFrecency(db, [item.url]).then(null, Cu.reportError);
+          BookmarkUtils.updateFrecency(db, [updatedItem.url]).then(null, Cu.reportError);
         }
 
         // Notify onItemChanged to listeners.
         let observers = PlacesUtils.bookmarks.getObservers();
         // For lastModified, we only care about the original input, since we
         // should not notify implciit lastModified changes.
         if (info.hasOwnProperty("lastModified") &&
             updateInfo.hasOwnProperty("lastModified") &&
             item.lastModified != updatedItem.lastModified) {
-          notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
+          BookmarkUtils.notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
                                                false,
-                                               `${toPRTime(updatedItem.lastModified)}`,
-                                               toPRTime(updatedItem.lastModified),
+                                               `${BookmarkUtils.toPRTime(updatedItem.lastModified)}`,
+                                               BookmarkUtils.toPRTime(updatedItem.lastModified),
                                                updatedItem.type,
                                                updatedItem._parentId,
                                                updatedItem.guid,
                                                updatedItem.parentGuid, "" ]);
         }
         if (updateInfo.hasOwnProperty("title")) {
-          notify(observers, "onItemChanged", [ updatedItem._id, "title",
+          BookmarkUtils.notify(observers, "onItemChanged", [ updatedItem._id, "title",
                                                false, updatedItem.title,
-                                               toPRTime(updatedItem.lastModified),
+                                               BookmarkUtils.toPRTime(updatedItem.lastModified),
                                                updatedItem.type,
                                                updatedItem._parentId,
                                                updatedItem.guid,
                                                updatedItem.parentGuid, "" ]);
         }
         if (updateInfo.hasOwnProperty("url")) {
-          notify(observers, "onItemChanged", [ updatedItem._id, "uri",
+          BookmarkUtils.notify(observers, "onItemChanged", [ updatedItem._id, "uri",
                                                false, updatedItem.url.href,
-                                               toPRTime(updatedItem.lastModified),
+                                               BookmarkUtils.toPRTime(updatedItem.lastModified),
                                                updatedItem.type,
                                                updatedItem._parentId,
                                                updatedItem.guid,
                                                updatedItem.parentGuid,
                                                item.url.href ]);
         }
         // If the item was moved, notify onItemMoved.
         if (item.parentGuid != updatedItem.parentGuid ||
             item.index != updatedItem.index) {
-          notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
+          BookmarkUtils.notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
                                              item.index, updatedItem._parentId,
                                              updatedItem.index, updatedItem.type,
                                              updatedItem.guid, item.parentGuid,
                                              updatedItem.parentGuid ]);
         }
 
         // Remove non-enumerable properties.
         return Object.assign({}, updatedItem);
@@ -392,37 +381,37 @@ var Bookmarks = Object.freeze({
     // Disallow removing the root folders.
     if ([this.rootGuid, this.menuGuid, this.toolbarGuid, this.unfiledGuid,
          this.tagsGuid].includes(info.guid)) {
       throw new Error("It's not possible to remove Places root folders.");
     }
 
     // Even if we ignore any other unneeded property, we still validate any
     // known property to reduce likelihood of hidden bugs.
-    let removeInfo = validateBookmarkObject(info);
+    let removeInfo = BookmarkValidators.validateBookmarkObject(info);
 
     return Task.spawn(function* () {
-      let item = yield fetchBookmark(removeInfo);
+      let item = yield BookmarkUtils.fetchBookmark(removeInfo);
       if (!item)
         throw new Error("No bookmarks found for the provided GUID.");
 
-      item = yield removeBookmark(item, options);
+      item = yield BookmarkUtils.removeBookmark(item, options);
 
       // Notify onItemRemoved to listeners.
       let observers = PlacesUtils.bookmarks.getObservers();
-      let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
-      notify(observers, "onItemRemoved", [ item._id, item._parentId, item.index,
+      let uri = item.hasOwnProperty("url") ? BookmarkUtils.toURI(item.url) : null;
+      BookmarkUtils.notify(observers, "onItemRemoved", [ item._id, item._parentId, item.index,
                                            item.type, uri, item.guid,
                                            item.parentGuid ]);
 
       let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
       if (isUntagging) {
-        for (let entry of (yield fetchBookmarksByURL(item))) {
-          notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
-                                               toPRTime(entry.lastModified),
+        for (let entry of (yield BookmarkUtils.fetchBookmarksByURL(item))) {
+          BookmarkUtils.notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+                                               BookmarkUtils.toPRTime(entry.lastModified),
                                                entry.type, entry._parentId,
                                                entry.guid, entry.parentGuid,
                                                "" ]);
         }
       }
 
       // Remove non-enumerable properties.
       return Object.assign({}, item);
@@ -436,18 +425,18 @@ var Bookmarks = Object.freeze({
    *
    * @return {Promise} resolved when the removal is complete.
    * @resolves once the removal is complete.
    */
   eraseEverything: function() {
     return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
       db => db.executeTransaction(function* () {
         const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid];
-        yield removeFoldersContents(db, folderGuids);
-        const time = toPRTime(new Date());
+        yield BookmarkUtils.removeFoldersContents(db, folderGuids);
+        const time = BookmarkUtils.toPRTime(new Date());
         for (let folderGuid of folderGuids) {
           yield db.executeCached(
             `UPDATE moz_bookmarks SET lastModified = :time
              WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
             `, { folderGuid, time });
         }
       }.bind(this))
     );
@@ -493,17 +482,17 @@ var Bookmarks = Object.freeze({
       } else if (query.url instanceof Ci.nsIURI) {
         query.url = query.url.spec;
       } else {
         throw new Error("Url option must be a string or a URL object");
       }
     }
 
     return Task.spawn(function* () {
-      let results = yield queryBookmarks(query);
+      let results = yield BookmarkUtils.queryBookmarks(query);
 
       return results;
     });
   },
 
   /**
    * Returns a list of recently bookmarked items.
    *
@@ -521,17 +510,17 @@ var Bookmarks = Object.freeze({
     if (!typeof numberOfItems === 'number' || (numberOfItems % 1) !== 0) {
       throw new Error("numberOfItems argument must be an integer");
     }
     if (numberOfItems <= 0) {
       throw new Error("numberOfItems argument must be greater than zero");
     }
 
     return Task.spawn(function* () {
-      return yield fetchRecentBookmarks(numberOfItems);
+      return yield BookmarkUtils.fetchRecentBookmarks(numberOfItems);
     });
   },
 
   /**
    * Fetches information about a bookmark-item.
    *
    * REMARK: any successful call to this method resolves to a single
    *         bookmark-item (or null), even when multiple bookmarks may exist
@@ -580,31 +569,31 @@ var Bookmarks = Object.freeze({
       v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
       v => v.hasOwnProperty("url")
     ].reduce((old, fn) => old + fn(info)|0, 0);
     if (conditionsCount != 1)
       throw new Error(`Unexpected number of conditions provided: ${conditionsCount}`);
 
     // Even if we ignore any other unneeded property, we still validate any
     // known property to reduce likelihood of hidden bugs.
-    let fetchInfo = validateBookmarkObject(info,
+    let fetchInfo = BookmarkValidators.validateBookmarkObject(info,
       { parentGuid: { requiredIf: b => b.hasOwnProperty("index") }
       , index: { requiredIf: b => b.hasOwnProperty("parentGuid")
                , validIf: b => typeof(b.index) == "number" &&
                                b.index >= 0 || b.index == this.DEFAULT_INDEX }
       });
 
     return Task.spawn(function* () {
       let results;
       if (fetchInfo.hasOwnProperty("guid"))
-        results = yield fetchBookmark(fetchInfo);
+        results = yield BookmarkUtils.fetchBookmark(fetchInfo);
       else if (fetchInfo.hasOwnProperty("parentGuid") && fetchInfo.hasOwnProperty("index"))
-        results = yield fetchBookmarkByPosition(fetchInfo);
+        results = yield BookmarkUtils.fetchBookmarkByPosition(fetchInfo);
       else if (fetchInfo.hasOwnProperty("url"))
-        results = yield fetchBookmarksByURL(fetchInfo);
+        results = yield BookmarkUtils.fetchBookmarksByURL(fetchInfo);
 
       if (!results)
         return null;
 
       if (!Array.isArray(results))
         results = [results];
       // Remove non-enumerable properties.
       results = results.map(r => Object.assign({}, r));
@@ -703,897 +692,35 @@ var Bookmarks = Object.freeze({
    *        incomplete, missing entries will be appended.
    *
    * @return {Promise} resolved when reordering is complete.
    * @rejects if an error happens while reordering.
    * @throws if the arguments are invalid.
    */
   reorder(parentGuid, orderedChildrenGuids) {
     let info = { guid: parentGuid };
-    info = validateBookmarkObject(info, { guid: { required: true } });
+    info = BookmarkValidators.validateBookmarkObject(info, { guid: { required: true } });
 
     if (!Array.isArray(orderedChildrenGuids) || !orderedChildrenGuids.length)
       throw new Error("Must provide a sorted array of children GUIDs.");
-    try {
-      orderedChildrenGuids.forEach(VALIDATORS.guid);
-    } catch (ex) {
+    if (!orderedChildrenGuids.every(BookmarkValidators.isValidGuid))
       throw new Error("Invalid GUID found in the sorted children array.");
-    }
 
     return Task.spawn(function* () {
-      let parent = yield fetchBookmark(info);
+      let parent = yield BookmarkUtils.fetchBookmark(info);
       if (!parent || parent.type != this.TYPE_FOLDER)
         throw new Error("No folder found for the provided GUID.");
 
-      let sortedChildren = yield reorderChildren(parent, orderedChildrenGuids);
+      let sortedChildren = yield BookmarkUtils.reorderChildren(parent, orderedChildrenGuids);
 
       let observers = PlacesUtils.bookmarks.getObservers();
       // Note that child.index is the old index.
       for (let i = 0; i < sortedChildren.length; ++i) {
         let child = sortedChildren[i];
-        notify(observers, "onItemMoved", [ child._id, child._parentId,
+        BookmarkUtils.notify(observers, "onItemMoved", [ child._id, child._parentId,
                                            child.index, child._parentId,
                                            i, child.type,
                                            child.guid, child.parentGuid,
                                            child.parentGuid ]);
       }
     }.bind(this));
   }
 });
-
-////////////////////////////////////////////////////////////////////////////////
-// Globals.
-
-/**
- * Sends a bookmarks notification through the given observers.
- *
- * @param observers
- *        array of nsINavBookmarkObserver objects.
- * @param notification
- *        the notification name.
- * @param args
- *        array of arguments to pass to the notification.
- */
-function notify(observers, notification, args) {
-  for (let observer of observers) {
-    try {
-      observer[notification](...args);
-    } catch (ex) {}
-  }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Update implementation.
-
-function updateBookmark(info, item, newParent) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
-    Task.async(function*(db) {
-
-    let tuples = new Map();
-    if (info.hasOwnProperty("lastModified"))
-      tuples.set("lastModified", { value: toPRTime(info.lastModified) });
-    if (info.hasOwnProperty("title"))
-      tuples.set("title", { value: info.title });
-
-    yield db.executeTransaction(function* () {
-      if (info.hasOwnProperty("url")) {
-        // Ensure a page exists in moz_places for this URL.
-        yield db.executeCached(
-          `INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
-           VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
-          `, { url: info.url ? info.url.href : null,
-               rev_host: PlacesUtils.getReversedHost(info.url),
-               frecency: info.url.protocol == "place:" ? 0 : -1 });
-        tuples.set("url", { value: info.url.href
-                          , fragment: "fk = (SELECT id FROM moz_places WHERE url = :url)" });
-      }
-
-      if (newParent) {
-        // For simplicity, update the index regardless.
-        let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
-        tuples.set("position", { value: newIndex });
-
-        if (newParent.guid == item.parentGuid) {
-          // Moving inside the original container.
-          // When moving "up", add 1 to each index in the interval.
-          // Otherwise when moving down, we subtract 1.
-          let sign = newIndex < item.index ? +1 : -1;
-          yield db.executeCached(
-            `UPDATE moz_bookmarks SET position = position + :sign
-             WHERE parent = :newParentId
-               AND position BETWEEN :lowIndex AND :highIndex
-            `, { sign: sign, newParentId: newParent._id,
-                 lowIndex: Math.min(item.index, newIndex),
-                 highIndex: Math.max(item.index, newIndex) });
-        } else {
-          // Moving across different containers.
-          tuples.set("parent", { value: newParent._id} );
-          yield db.executeCached(
-            `UPDATE moz_bookmarks SET position = position + :sign
-             WHERE parent = :oldParentId
-               AND position >= :oldIndex
-            `, { sign: -1, oldParentId: item._parentId, oldIndex: item.index });
-          yield db.executeCached(
-            `UPDATE moz_bookmarks SET position = position + :sign
-             WHERE parent = :newParentId
-               AND position >= :newIndex
-            `, { sign: +1, newParentId: newParent._id, newIndex: newIndex });
-
-          yield setAncestorsLastModified(db, item.parentGuid, info.lastModified);
-        }
-        yield setAncestorsLastModified(db, newParent.guid, info.lastModified);
-      }
-
-      yield db.executeCached(
-        `UPDATE moz_bookmarks
-         SET ${Array.from(tuples.keys()).map(v => tuples.get(v).fragment || `${v} = :${v}`).join(", ")}
-         WHERE guid = :guid
-        `, Object.assign({ guid: info.guid },
-                         [...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
-    });
-
-    // If the parent changed, update related non-enumerable properties.
-    let additionalParentInfo = {};
-    if (newParent) {
-      Object.defineProperty(additionalParentInfo, "_parentId",
-                            { value: newParent._id, enumerable: false });
-      Object.defineProperty(additionalParentInfo, "_grandParentId",
-                            { value: newParent._parentId, enumerable: false });
-    }
-
-    let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
-
-    // Don't return an empty title to the caller.
-    if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
-      delete updatedItem.title;
-
-    return updatedItem;
-  }));
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Insert implementation.
-
-function insertBookmark(item, parent) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmark",
-    Task.async(function*(db) {
-
-    // If a guid was not provided, generate one, so we won't need to fetch the
-    // bookmark just after having created it.
-    if (!item.hasOwnProperty("guid"))
-      item.guid = (yield db.executeCached("SELECT GENERATE_GUID() AS guid"))[0].getResultByName("guid");
-
-    yield db.executeTransaction(function* transaction() {
-      if (item.type == Bookmarks.TYPE_BOOKMARK) {
-        // Ensure a page exists in moz_places for this URL.
-        yield db.executeCached(
-          `INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
-           VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
-          `, { url: item.url.href, rev_host: PlacesUtils.getReversedHost(item.url),
-               frecency: item.url.protocol == "place:" ? 0 : -1 });
-      }
-
-      // Adjust indices.
-      yield db.executeCached(
-        `UPDATE moz_bookmarks SET position = position + 1
-         WHERE parent = :parent
-         AND position >= :index
-        `, { parent: parent._id, index: item.index });
-
-      // Insert the bookmark into the database.
-      yield db.executeCached(
-        `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
-                                    dateAdded, lastModified, guid)
-         VALUES ((SELECT id FROM moz_places WHERE url = :url), :type, :parent,
-                 :index, :title, :date_added, :last_modified, :guid)
-        `, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
-             type: item.type, parent: parent._id, index: item.index,
-             title: item.title, date_added: toPRTime(item.dateAdded),
-             last_modified: toPRTime(item.lastModified), guid: item.guid });
-
-      yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
-    });
-
-    // If not a tag recalculate frecency...
-    let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
-    if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
-      // ...though we don't wait for the calculation.
-      updateFrecency(db, [item.url]).then(null, Cu.reportError);
-    }
-
-    // Don't return an empty title to the caller.
-    if (item.hasOwnProperty("title") && item.title === null)
-      delete item.title;
-
-    return item;
-  }));
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Query implementation.
-
-function queryBookmarks(info) {
-  let queryParams = {tags_folder: PlacesUtils.tagsFolderId};
-  // we're searching for bookmarks, so exclude tags
-  let queryString = "WHERE p.parent <> :tags_folder";
-
-  if (info.title) {
-    queryString += " AND b.title = :title";
-    queryParams.title = info.title;
-  }
-
-  if (info.url) {
-    queryString += " AND h.url = :url";
-    queryParams.url = info.url;
-  }
-
-  if (info.query) {
-    queryString += " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior) ";
-    queryParams.query = info.query;
-    queryParams.matchBehavior = MATCH_ANYWHERE_UNMODIFIED;
-    queryParams.searchBehavior = BEHAVIOR_BOOKMARK;
-  }
-
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks",
-    Task.async(function*(db) {
-
-    // _id, _childCount, _grandParentId and _parentId fields
-    // are required to be in the result by the converting function
-    // hence setting them to NULL
-    let rows = yield db.executeCached(
-      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title,
-              h.url AS url, b.parent, p.parent,
-              NULL AS _id,
-              NULL AS _childCount,
-              NULL AS _grandParentId,
-              NULL AS _parentId
-       FROM moz_bookmarks b
-       LEFT JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON h.id = b.fk
-       ${queryString}
-      `, queryParams);
-
-    return rowsToItemsArray(rows);
-  }));
-}
-
-
-////////////////////////////////////////////////////////////////////////////////
-// Fetch implementation.
-
-function fetchBookmark(info) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmark",
-    Task.async(function*(db) {
-
-    let rows = yield db.executeCached(
-      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
-              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
-       FROM moz_bookmarks b
-       LEFT JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON h.id = b.fk
-       WHERE b.guid = :guid
-      `, { guid: info.guid });
-
-    return rows.length ? rowsToItemsArray(rows)[0] : null;
-  }));
-}
-
-function fetchBookmarkByPosition(info) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarkByPosition",
-    Task.async(function*(db) {
-    let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
-
-    let rows = yield db.executeCached(
-      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
-              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
-       FROM moz_bookmarks b
-       LEFT JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON h.id = b.fk
-       WHERE p.guid = :parentGuid
-       AND b.position = IFNULL(:index, (SELECT count(*) - 1
-                                        FROM moz_bookmarks
-                                        WHERE parent = p.id))
-      `, { parentGuid: info.parentGuid, index });
-
-    return rows.length ? rowsToItemsArray(rows)[0] : null;
-  }));
-}
-
-function fetchBookmarksByURL(info) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByURL",
-    Task.async(function*(db) {
-
-    let rows = yield db.executeCached(
-      `/* do not warn (bug no): not worth to add an index */
-       SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
-              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
-       FROM moz_bookmarks b
-       LEFT JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON h.id = b.fk
-       WHERE h.url = :url
-       AND _grandParentId <> :tags_folder
-       ORDER BY b.lastModified DESC
-      `, { url: info.url.href,
-           tags_folder: PlacesUtils.tagsFolderId });
-
-    return rows.length ? rowsToItemsArray(rows) : null;
-  }));
-}
-
-function fetchRecentBookmarks(numberOfItems) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchRecentBookmarks",
-    Task.async(function*(db) {
-
-    let rows = yield db.executeCached(
-      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              NULL AS _id, NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId
-       FROM moz_bookmarks b
-       LEFT JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON h.id = b.fk
-       WHERE p.parent <> :tags_folder
-       ORDER BY b.dateAdded DESC, b.ROWID DESC
-       LIMIT :numberOfItems
-      `, { tags_folder: PlacesUtils.tagsFolderId, numberOfItems });
-
-    return rows.length ? rowsToItemsArray(rows) : [];
-  }));
-}
-
-function fetchBookmarksByParent(info) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByParent",
-    Task.async(function*(db) {
-
-    let rows = yield db.executeCached(
-      `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
-              (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
-       FROM moz_bookmarks b
-       LEFT JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON h.id = b.fk
-       WHERE p.guid = :parentGuid
-       ORDER BY b.position ASC
-      `, { parentGuid: info.parentGuid });
-
-    return rowsToItemsArray(rows);
-  }));
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Remove implementation.
-
-function removeBookmark(item, options) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
-    Task.async(function*(db) {
-
-    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
-
-    yield db.executeTransaction(function* transaction() {
-      // If it's a folder, remove its contents first.
-      if (item.type == Bookmarks.TYPE_FOLDER) {
-        if (options.preventRemovalOfNonEmptyFolders && item._childCount > 0) {
-          throw new Error("Cannot remove a non-empty folder.");
-        }
-        yield removeFoldersContents(db, [item.guid]);
-      }
-
-      // Remove annotations first.  If it's a tag, we can avoid paying that cost.
-      if (!isUntagging) {
-        // We don't go through the annotations service for this cause otherwise
-        // we'd get a pointless onItemChanged notification and it would also
-        // set lastModified to an unexpected value.
-        yield removeAnnotationsForItem(db, item._id);
-      }
-
-      // Remove the bookmark from the database.
-      yield db.executeCached(
-        `DELETE FROM moz_bookmarks WHERE guid = :guid`, { guid: item.guid });
-
-      // Fix indices in the parent.
-      yield db.executeCached(
-        `UPDATE moz_bookmarks SET position = position - 1 WHERE
-         parent = :parentId AND position > :index
-        `, { parentId: item._parentId, index: item.index });
-
-      yield setAncestorsLastModified(db, item.parentGuid, new Date());
-    });
-
-    // If not a tag recalculate frecency...
-    if (item.type == Bookmarks.TYPE_BOOKMARK && !isUntagging) {
-      // ...though we don't wait for the calculation.
-      updateFrecency(db, [item.url]).then(null, Cu.reportError);
-    }
-
-    return item;
-  }));
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Reorder implementation.
-
-function reorderChildren(parent, orderedChildrenGuids) {
-  return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
-    db => db.executeTransaction(function* () {
-      // Select all of the direct children for the given parent.
-      let children = yield fetchBookmarksByParent({ parentGuid: parent.guid });
-      if (!children.length)
-        return undefined;
-
-      // Reorder the children array according to the specified order, provided
-      // GUIDs come first, others are appended in somehow random order.
-      children.sort((a, b) => {
-        let i = orderedChildrenGuids.indexOf(a.guid);
-        let j = orderedChildrenGuids.indexOf(b.guid);
-        // This works provided fetchBookmarksByParent returns sorted children.
-        if (i == -1 && j == -1)
-          return 0;
-        return (i != -1 && j != -1 && i < j) || (i != -1 && j == -1) ? -1 : 1;
-       });
-
-      // Update the bookmarks position now.  If any unknown guid have been
-      // inserted meanwhile, its position will be set to -position, and we'll
-      // handle it later.
-      // To do the update in a single step, we build a VALUES (guid, position)
-      // table.  We then use count() in the sorting table to avoid skipping values
-      // when no more existing GUIDs have been provided.
-      let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
-                                .join();
-      yield db.execute(
-        `WITH sorting(g, p) AS (
-           VALUES ${valuesTable}
-         )
-         UPDATE moz_bookmarks SET position = (
-           SELECT CASE count(a.g) WHEN 0 THEN -position
-                                  ELSE count(a.g) - 1
-                  END
-           FROM sorting a
-           JOIN sorting b ON b.p <= a.p
-           WHERE a.g = guid
-             AND parent = :parentId
-        )`, { parentId: parent._id});
-
-      // Update position of items that could have been inserted in the meanwhile.
-      // Since this can happen rarely and it's only done for schema coherence
-      // resonds, we won't notify about these changes.
-      yield db.executeCached(
-        `CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
-           AFTER UPDATE OF position ON moz_bookmarks
-           WHEN NEW.position = -1
-         BEGIN
-           UPDATE moz_bookmarks
-           SET position = (SELECT MAX(position) FROM moz_bookmarks
-                           WHERE parent = NEW.parent) +
-                          (SELECT count(*) FROM moz_bookmarks
-                           WHERE parent = NEW.parent
-                             AND position BETWEEN OLD.position AND -1)
-           WHERE guid = NEW.guid;
-         END
-        `);
-
-      yield db.executeCached(
-        `UPDATE moz_bookmarks SET position = -1 WHERE position < 0`);
-
-      yield db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
-
-      return children;
-    }.bind(this))
-  );
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Helpers.
-
-/**
- * Merges objects into a new object, included non-enumerable properties.
- *
- * @param sources
- *        source objects to merge.
- * @return a new object including all properties from the source objects.
- */
-function mergeIntoNewObject(...sources) {
-  let dest = {};
-  for (let src of sources) {
-    for (let prop of Object.getOwnPropertyNames(src)) {
-      Object.defineProperty(dest, prop, Object.getOwnPropertyDescriptor(src, prop));
-    }
-  }
-  return dest;
-}
-
-/**
- * Remove properties that have the same value across two bookmark objects.
- *
- * @param dest
- *        destination bookmark object.
- * @param src
- *        source bookmark object.
- * @return a cleaned up bookmark object.
- * @note "guid" is never removed.
- */
-function removeSameValueProperties(dest, src) {
-  for (let prop in dest) {
-    let remove = false;
-    switch (prop) {
-      case "lastModified":
-      case "dateAdded":
-        remove = src.hasOwnProperty(prop) && dest[prop].getTime() == src[prop].getTime();
-        break;
-      case "url":
-        remove = src.hasOwnProperty(prop) && dest[prop].href == src[prop].href;
-        break;
-      default:
-        remove = dest[prop] == src[prop];
-    }
-    if (remove && prop != "guid")
-      delete dest[prop];
-  }
-}
-
-/**
- * Converts an URL object to an nsIURI.
- *
- * @param url
- *        the URL object to convert.
- * @return nsIURI for the given URL.
- */
-function toURI(url) {
-  return NetUtil.newURI(url.href);
-}
-
-/**
- * Convert a Date object to a PRTime (microseconds).
- *
- * @param date
- *        the Date object to convert.
- * @return microseconds from the epoch.
- */
-function toPRTime(date) {
-  return date * 1000;
-}
-
-/**
- * Convert a PRTime to a Date object.
- *
- * @param time
- *        microseconds from the epoch.
- * @return a Date object.
- */
-function toDate(time) {
-  return new Date(parseInt(time / 1000));
-}
-
-/**
- * Convert an array of mozIStorageRow objects to an array of bookmark objects.
- *
- * @param rows
- *        the array of mozIStorageRow objects.
- * @return an array of bookmark objects.
- */
-function rowsToItemsArray(rows) {
-  return rows.map(row => {
-    let item = {};
-    for (let prop of ["guid", "index", "type"]) {
-      item[prop] = row.getResultByName(prop);
-    }
-    for (let prop of ["dateAdded", "lastModified"]) {
-      item[prop] = toDate(row.getResultByName(prop));
-    }
-    for (let prop of ["title", "parentGuid", "url" ]) {
-      let val = row.getResultByName(prop);
-      if (val)
-        item[prop] = prop === "url" ? new URL(val) : val;
-    }
-    for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId"]) {
-      let val = row.getResultByName(prop);
-      if (val !== null) {
-        // These properties should not be returned to the API consumer, thus
-        // they are non-enumerable and removed through Object.assign just before
-        // the object is returned.
-        // Configurable is set to support mergeIntoNewObject overwrites.
-        Object.defineProperty(item, prop, { value: val, enumerable: false,
-                                                        configurable: true });
-      }
-    }
-
-    return item;
-  });
-}
-
-/**
- * Executes a boolean validate function, throwing if it returns false.
- *
- * @param boolValidateFn
- *        A boolean validate function.
- * @return the input value.
- * @throws if input doesn't pass the validate function.
- */
-function simpleValidateFunc(boolValidateFn) {
-  return (v, input) => {
-    if (!boolValidateFn(v, input))
-      throw new Error("Invalid value");
-    return v;
-  };
-}
-
-/**
- * List of validators, one per each known property.
- * Validators must throw if the property value is invalid and return a fixed up
- * version of the value, if needed.
- */
-const VALIDATORS = Object.freeze({
-  guid: simpleValidateFunc(v => typeof(v) == "string" &&
-                                /^[a-zA-Z0-9\-_]{12}$/.test(v)),
-  parentGuid: simpleValidateFunc(v => typeof(v) == "string" &&
-                                      /^[a-zA-Z0-9\-_]{12}$/.test(v)),
-  index: simpleValidateFunc(v => Number.isInteger(v) &&
-                                 v >= Bookmarks.DEFAULT_INDEX),
-  dateAdded: simpleValidateFunc(v => v.constructor.name == "Date"),
-  lastModified: simpleValidateFunc(v => v.constructor.name == "Date"),
-  type: simpleValidateFunc(v => Number.isInteger(v) &&
-                                [ Bookmarks.TYPE_BOOKMARK
-                                , Bookmarks.TYPE_FOLDER
-                                , Bookmarks.TYPE_SEPARATOR ].includes(v)),
-  title: v => {
-    simpleValidateFunc(val => val === null || typeof(val) == "string").call(this, v);
-    if (!v)
-      return null;
-    return v.slice(0, DB_TITLE_LENGTH_MAX);
-  },
-  url: v => {
-    simpleValidateFunc(val => (typeof(val) == "string" && val.length <= DB_URL_LENGTH_MAX) ||
-                              (val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
-                              (val instanceof URL && val.href.length <= DB_URL_LENGTH_MAX)
-                      ).call(this, v);
-    if (typeof(v) === "string")
-      return new URL(v);
-    if (v instanceof Ci.nsIURI)
-      return new URL(v.spec);
-    return v;
-  }
-});
-
-/**
- * Checks validity of a bookmark object, filling up default values for optional
- * properties.
- *
- * @param input (object)
- *        The bookmark object to validate.
- * @param behavior (object) [optional]
- *        Object defining special behavior for some of the properties.
- *        The following behaviors may be optionally set:
- *         - requiredIf: if the provided condition is satisfied, then this
- *                       property is required.
- *         - validIf: if the provided condition is not satisfied, then this
- *                    property is invalid.
- *         - defaultValue: an undefined property should default to this value.
- *
- * @return a validated and normalized bookmark-item.
- * @throws if the object contains invalid data.
- * @note any unknown properties are pass-through.
- */
-function validateBookmarkObject(input, behavior={}) {
-  if (!input)
-    throw new Error("Input should be a valid object");
-  let normalizedInput = {};
-  let required = new Set();
-  for (let prop in behavior) {
-    if (behavior[prop].hasOwnProperty("required") && behavior[prop].required) {
-      required.add(prop);
-    }
-    if (behavior[prop].hasOwnProperty("requiredIf") && behavior[prop].requiredIf(input)) {
-      required.add(prop);
-    }
-    if (behavior[prop].hasOwnProperty("validIf") && input[prop] !== undefined &&
-        !behavior[prop].validIf(input)) {
-      throw new Error(`Invalid value for property '${prop}': ${input[prop]}`);
-    }
-    if (behavior[prop].hasOwnProperty("defaultValue") && input[prop] === undefined) {
-      input[prop] = behavior[prop].defaultValue;
-    }
-  }
-
-  for (let prop in input) {
-    if (required.has(prop)) {
-      required.delete(prop);
-    } else if (input[prop] === undefined) {
-      // Skip undefined properties that are not required.
-      continue;
-    }
-    if (VALIDATORS.hasOwnProperty(prop)) {
-      try {
-        normalizedInput[prop] = VALIDATORS[prop](input[prop], input);
-      } catch(ex) {
-        throw new Error(`Invalid value for property '${prop}': ${input[prop]}`);
-      }
-    }
-  }
-  if (required.size > 0)
-    throw new Error(`The following properties were expected: ${[...required].join(", ")}`);
-  return normalizedInput;
-}
-
-/**
- * Updates frecency for a list of URLs.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- * @param urls
- *        the array of URLs to update.
- */
-var updateFrecency = Task.async(function* (db, urls) {
-  yield db.execute(
-    `UPDATE moz_places
-     SET frecency = NOTIFY_FRECENCY(
-       CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date
-     ) WHERE url IN ( ${urls.map(url => JSON.stringify(url.href)).join(", ")} )
-    `);
-
-  yield db.execute(
-    `UPDATE moz_places
-     SET hidden = 0
-     WHERE url IN ( ${urls.map(url => JSON.stringify(url.href)).join(", ")} )
-       AND frecency <> 0
-    `);
-});
-
-/**
- * Removes any orphan annotation entries.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- */
-var removeOrphanAnnotations = Task.async(function* (db) {
-  yield db.executeCached(
-    `DELETE FROM moz_items_annos
-     WHERE id IN (SELECT a.id from moz_items_annos a
-                  LEFT JOIN moz_bookmarks b ON a.item_id = b.id
-                  WHERE b.id ISNULL)
-    `);
-  yield db.executeCached(
-    `DELETE FROM moz_anno_attributes
-     WHERE id IN (SELECT n.id from moz_anno_attributes n
-                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
-                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
-                  WHERE a1.id ISNULL AND a2.id ISNULL)
-    `);
-});
-
-/**
- * Removes annotations for a given item.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- * @param itemId
- *        internal id of the item for which to remove annotations.
- */
-var removeAnnotationsForItem = Task.async(function* (db, itemId) {
-  yield db.executeCached(
-    `DELETE FROM moz_items_annos
-     WHERE item_id = :id
-    `, { id: itemId });
-  yield db.executeCached(
-    `DELETE FROM moz_anno_attributes
-     WHERE id IN (SELECT n.id from moz_anno_attributes n
-                  LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
-                  LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
-                  WHERE a1.id ISNULL AND a2.id ISNULL)
-    `);
-});
-
-/**
- * Updates lastModified for all the ancestors of a given folder GUID.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- * @param folderGuid
- *        the GUID of the folder whose ancestors should be updated.
- * @param time
- *        a Date object to use for the update.
- *
- * @note the folder itself is also updated.
- */
-var setAncestorsLastModified = Task.async(function* (db, folderGuid, time) {
-  yield db.executeCached(
-    `WITH RECURSIVE
-     ancestors(aid) AS (
-       SELECT id FROM moz_bookmarks WHERE guid = :guid
-       UNION ALL
-       SELECT parent FROM moz_bookmarks
-       JOIN ancestors ON id = aid
-       WHERE type = :type
-     )
-     UPDATE moz_bookmarks SET lastModified = :time
-     WHERE id IN ancestors
-    `, { guid: folderGuid, type: Bookmarks.TYPE_FOLDER,
-         time: toPRTime(time) });
-});
-
-/**
- * Remove all descendants of one or more bookmark folders.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- * @param folderGuids
- *        array of folder guids.
- */
-var removeFoldersContents =
-Task.async(function* (db, folderGuids) {
-  let itemsRemoved = [];
-  for (let folderGuid of folderGuids) {
-    let rows = yield db.executeCached(
-      `WITH RECURSIVE
-       descendants(did) AS (
-         SELECT b.id FROM moz_bookmarks b
-         JOIN moz_bookmarks p ON b.parent = p.id
-         WHERE p.guid = :folderGuid
-         UNION ALL
-         SELECT id FROM moz_bookmarks
-         JOIN descendants ON parent = did
-       )
-       SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
-              b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
-              b.lastModified, b.title, p.parent AS _grandParentId,
-              NULL AS _childCount
-       FROM moz_bookmarks b
-       JOIN moz_bookmarks p ON p.id = b.parent
-       LEFT JOIN moz_places h ON b.fk = h.id
-       WHERE b.id IN descendants`, { folderGuid });
-
-    itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
-
-    yield db.executeCached(
-      `WITH RECURSIVE
-       descendants(did) AS (
-         SELECT b.id FROM moz_bookmarks b
-         JOIN moz_bookmarks p ON b.parent = p.id
-         WHERE p.guid = :folderGuid
-         UNION ALL
-         SELECT id FROM moz_bookmarks
-         JOIN descendants ON parent = did
-       )
-       DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid });
-  }
-
-  // Cleanup orphans.
-  yield removeOrphanAnnotations(db);
-
-  // TODO (Bug 1087576): this may leave orphan tags behind.
-
-  let urls = itemsRemoved.filter(item => "url" in item).map(item => item.url);
-  updateFrecency(db, urls).then(null, Cu.reportError);
-
-  // Send onItemRemoved notifications to listeners.
-  // TODO (Bug 1087580): for the case of eraseEverything, this should send a
-  // single clear bookmarks notification rather than notifying for each
-  // bookmark.
-
-  // Notify listeners in reverse order to serve children before parents.
-  let observers = PlacesUtils.bookmarks.getObservers();
-  for (let item of itemsRemoved.reverse()) {
-    let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
-    notify(observers, "onItemRemoved", [ item._id, item._parentId,
-                                         item.index, item.type, uri,
-                                         item.guid, item.parentGuid ]);
-
-    let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
-    if (isUntagging) {
-      for (let entry of (yield fetchBookmarksByURL(item))) {
-        notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
-                                             toPRTime(entry.lastModified),
-                                             entry.type, entry._parentId,
-                                             entry.guid, entry.parentGuid,
-                                             "" ]);
-      }
-    }
-  }
-});
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -56,16 +56,17 @@ if CONFIG['MOZ_PLACES']:
     LOCAL_INCLUDES += [
         '../build',
     ]
 
     EXTRA_JS_MODULES += [
         'BookmarkHTMLUtils.jsm',
         'BookmarkJSONUtils.jsm',
         'Bookmarks.jsm',
+        'BookmarkUtils.jsm',
         'ClusterLib.js',
         'ColorAnalyzer_worker.js',
         'ColorConversion.js',
         'History.jsm',
         'Livemark.jsm',
         'PlacesBackups.jsm',
         'PlacesDBUtils.jsm',
         'PlacesRemoteTabsAutocompleteProvider.jsm',