Bug 1095426 - Convert JSON backups code to the new async Bookmarks.jsm API. r?mak draft
authorMark Banner <standard8@mozilla.com>
Fri, 12 May 2017 08:04:51 +0100
changeset 597296 1b3a81eb95a7c42f2525d54062c558c881185ff8
parent 597295 72627afac86eb0b3ce27bd9480269b3e1d58a934
child 634201 9c2615960c4378b672808d2aefba4c90c9e613e6
push id64899
push userbmo:standard8@mozilla.com
push dateTue, 20 Jun 2017 11:09:48 +0000
reviewersmak
bugs1095426
milestone56.0a1
Bug 1095426 - Convert JSON backups code to the new async Bookmarks.jsm API. r?mak MozReview-Commit-ID: FNZZGwWVSI2
services/sync/tests/unit/test_bookmark_engine.js
toolkit/components/places/BookmarkJSONUtils.jsm
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/PlacesBackups.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
toolkit/components/places/tests/unit/bookmarks.json
toolkit/components/places/tests/unit/test_384370.js
toolkit/components/places/tests/unit/test_bookmarks_json.js
toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
toolkit/components/places/tests/unit/test_sync_utils.js
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -558,16 +558,20 @@ add_task(async function test_sync_dateAd
   await PlacesSyncUtils.bookmarks.reset();
   let engine = new BookmarksEngine(Service);
   let store  = engine._store;
   let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
+  // TODO: Avoid random orange (bug 1374599), this is only necessary
+  // intermittently - reset the last sync date so that we'll get all bookmarks.
+  engine.lastSync = 1;
+
   Svc.Obs.notify("weave:engine:start-tracking");   // We skip usual startup...
 
   // Just matters that it's in the past, not how far.
   let now = Date.now();
   let oneYearMS = 365 * 24 * 60 * 60 * 1000;
 
   try {
     let item1GUID = "abcdefabcdef";
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -6,21 +6,21 @@ this.EXPORTED_SYMBOLS = [ "BookmarkJSONU
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/PromiseUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
   "resource://gre/modules/PlacesBackups.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
   "resource://gre/modules/Deprecated.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
 XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
 
@@ -238,351 +238,319 @@ BookmarkImporter.prototype = {
       converter.charset = "UTF-8";
       let jsonString = converter.convertFromByteArray(aResult, aResult.length);
       await this.importFromJSON(jsonString);
   },
 
   /**
    * Import bookmarks from a JSON string.
    *
-   * @param aString
-   *        JSON string of serialized bookmark data.
+   * @param {String} aString JSON string of serialized bookmark data.
+   * @return {Promise}
+   * @resolves When the new bookmarks have been created.
+   * @rejects JavaScript exception.
    */
   async importFromJSON(aString) {
-    this._importPromises = [];
-    let deferred = PromiseUtils.defer();
     let nodes =
       PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
 
     if (nodes.length == 0 || !nodes[0].children ||
         nodes[0].children.length == 0) {
-      deferred.resolve(); // Nothing to restore
-    } else {
-      // Ensure tag folder gets processed last
-      nodes[0].children.sort(function sortRoots(aNode, bNode) {
-        if (aNode.root && aNode.root == "tagsFolder")
-          return 1;
-        if (bNode.root && bNode.root == "tagsFolder")
-          return -1;
-        return 0;
-      });
+      return;
+    }
+
+    // Change to nodes[0].children as we don't import the root, and also filter
+    // out any obsolete "tagsFolder" sections.
+    nodes = nodes[0].children.filter(node => !node.root || node.root != "tagsFolder");
+
+    // If we're replacing, then erase existing bookmarks first.
+    if (this._replace) {
+      await PlacesBackups.eraseEverythingIncludingUserRoots({ source: this._source });
+    }
+
+    let folderIdToGuidMap = {};
+    let searchGuids = [];
 
-      let batch = {
-        nodes: nodes[0].children,
-        runBatched: () => {
-          if (this._replace) {
-            // Get roots excluded from the backup, we will not remove them
-            // before restoring.
-            let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
-                                 PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
-            // Delete existing children of the root node, excepting:
-            // 1. special folders: delete the child nodes
-            // 2. tags folder: untag via the tagging api
-            let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
-                                                   false, false).root;
-            let childIds = [];
-            for (let i = 0; i < root.childCount; i++) {
-              let childId = root.getChild(i).itemId;
-              if (!excludeItems.includes(childId) &&
-                  childId != PlacesUtils.tagsFolderId) {
-                childIds.push(childId);
-              }
-            }
-            root.containerOpen = false;
+    // Now do some cleanup on the imported nodes so that the various guids
+    // match what we need for insertTree, and we also have mappings of folders
+    // so we can repair any searches after inserting the bookmarks (see bug 824502).
+    for (let node of nodes) {
+      if (!node.children || node.children.length == 0)
+        continue;  // Nothing to restore for this root
 
-            for (let i = 0; i < childIds.length; i++) {
-              let rootItemId = childIds[i];
-              if (PlacesUtils.isRootItem(rootItemId)) {
-                PlacesUtils.bookmarks.removeFolderChildren(rootItemId,
-                                                           this._source);
-              } else {
-                PlacesUtils.bookmarks.removeItem(rootItemId, this._source);
-              }
-            }
-          }
+      // Ensure we set the source correctly.
+      node.source = this._source;
 
-          let searchIds = [];
-          let folderIdMap = [];
+      // Translate the node for insertTree.
+      let [folders, searches] = translateTreeTypes(node);
 
-          for (let node of batch.nodes) {
-            if (!node.children || node.children.length == 0)
-              continue; // Nothing to restore for this root
+      folderIdToGuidMap = Object.assign(folderIdToGuidMap, folders);
+      searchGuids = searchGuids.concat(searches);
+    }
 
-            if (node.root) {
-              let container = PlacesUtils.placesRootId; // Default to places root
-              switch (node.root) {
-                case "bookmarksMenuFolder":
-                  container = PlacesUtils.bookmarksMenuFolderId;
-                  break;
-                case "tagsFolder":
-                  container = PlacesUtils.tagsFolderId;
-                  break;
-                case "unfiledBookmarksFolder":
-                  container = PlacesUtils.unfiledBookmarksFolderId;
-                  break;
-                case "toolbarFolder":
-                  container = PlacesUtils.toolbarFolderId;
-                  break;
-                case "mobileFolder":
-                  container = PlacesUtils.mobileFolderId;
-                  break;
-              }
+    // Now we can add the actual nodes to the database.
+    for (let node of nodes) {
+      // Drop any nodes without children, we can't insert them.
+      if (!node.children || node.children.length == 0) {
+        continue;
+      }
+
+      // Places is moving away from supporting user-defined folders at the top
+      // of the tree, however, until we have a migration strategy we need to
+      // ensure any non-built-in folders are created (xref bug 1310299).
+      if (!PlacesUtils.bookmarks.userContentRoots.includes(node.guid)) {
+        node.parentGuid = PlacesUtils.bookmarks.rootGuid;
+        await PlacesUtils.bookmarks.insert(node);
+      }
+
+      await PlacesUtils.bookmarks.insertTree(node);
 
-              // Insert the data into the db
-              for (let child of node.children) {
-                let index = child.index;
-                let [folders, searches] =
-                  this.importJSONNode(child, container, index, 0);
-                for (let i = 0; i < folders.length; i++) {
-                  if (folders[i])
-                    folderIdMap[i] = folders[i];
-                }
-                searchIds = searchIds.concat(searches);
-              }
-            } else {
-              let [folders, searches] = this.importJSONNode(
-                node, PlacesUtils.placesRootId, node.index, 0);
-              for (let i = 0; i < folders.length; i++) {
-                if (folders[i])
-                  folderIdMap[i] = folders[i];
-              }
-              searchIds = searchIds.concat(searches);
-            }
-          }
+      // Now add any favicons.
+      try {
+        insertFaviconsForTree(node);
+      } catch (ex) {
+        Cu.reportError(`Failed to insert favicons: ${ex}`);
+      }
+    }
 
-          // Fixup imported place: uris that contain folders
-          for (let id of searchIds) {
-            let oldURI = PlacesUtils.bookmarks.getBookmarkURI(id);
-            let uri = fixupQuery(oldURI, folderIdMap);
-            if (!uri.equals(oldURI)) {
-              PlacesUtils.bookmarks.changeBookmarkURI(id, uri, this._source);
-            }
-          }
-
-          deferred.resolve();
-        }
-      };
-
-      PlacesUtils.bookmarks.runInBatchMode(batch, null);
-    }
-    await deferred.promise;
-    // TODO (bug 1095426) once converted to the new bookmarks API, methods will
-    // yield, so this hack should not be needed anymore.
-    try {
-      await Promise.all(this._importPromises);
-    } finally {
-      delete this._importPromises;
+    // Now update any bookmarks with a place: search that contain an index to
+    // a folder id.
+    for (let guid of searchGuids) {
+      let searchBookmark = await PlacesUtils.bookmarks.fetch(guid);
+      let url = await fixupQuery(searchBookmark.url, folderIdToGuidMap);
+      if (url != searchBookmark.url) {
+        await PlacesUtils.bookmarks.update({ guid, url, source: this._source });
+      }
     }
   },
-
-  /**
-   * Takes a JSON-serialized node and inserts it into the db.
-   *
-   * @param aData
-   *        The unwrapped data blob of dropped or pasted data.
-   * @param aContainer
-   *        The container the data was dropped or pasted into
-   * @param aIndex
-   *        The index within the container the item was dropped or pasted at
-   * @return an array containing of maps of old folder ids to new folder ids,
-   *         and an array of saved search ids that need to be fixed up.
-   *         eg: [[[oldFolder1, newFolder1]], [search1]]
-   */
-  importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex,
-                                             aGrandParentId) {
-    let folderIdMap = [];
-    let searchIds = [];
-    let id = -1;
-    switch (aData.type) {
-      case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
-        if (aContainer == PlacesUtils.tagsFolderId) {
-          // Node is a tag
-          if (aData.children) {
-            for (let child of aData.children) {
-              try {
-                PlacesUtils.tagging.tagURI(
-                  NetUtil.newURI(child.uri), [aData.title], this._source);
-              } catch (ex) {
-                // Invalid tag child, skip it
-              }
-            }
-            return [folderIdMap, searchIds];
-          }
-        } else if (aData.annos &&
-                   aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
-          // Node is a livemark
-          let feedURI = null;
-          let siteURI = null;
-          aData.annos = aData.annos.filter(function(aAnno) {
-            switch (aAnno.name) {
-              case PlacesUtils.LMANNO_FEEDURI:
-                feedURI = NetUtil.newURI(aAnno.value);
-                return false;
-              case PlacesUtils.LMANNO_SITEURI:
-                siteURI = NetUtil.newURI(aAnno.value);
-                return false;
-              default:
-                return true;
-            }
-          });
-
-          if (feedURI) {
-            let lmPromise = PlacesUtils.livemarks.addLivemark({
-              title: aData.title,
-              feedURI,
-              parentId: aContainer,
-              index: aIndex,
-              lastModified: aData.lastModified,
-              siteURI,
-              guid: aData.guid,
-              source: this._source
-            }).then(aLivemark => {
-              let id = aLivemark.id;
-              if (aData.dateAdded)
-                PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
-                                                       this._source);
-              if (aData.annos && aData.annos.length)
-                PlacesUtils.setAnnotationsForItem(id, aData.annos,
-                                                  this._source);
-            });
-            this._importPromises.push(lmPromise);
-          }
-        } else {
-          let isMobileFolder = aData.annos &&
-                               aData.annos.some(anno => anno.name == PlacesUtils.MOBILE_ROOT_ANNO);
-          if (isMobileFolder) {
-            // Mobile bookmark folders are special: we move their children to
-            // the mobile root instead of importing them. We also rewrite
-            // queries to use the special folder ID, and ignore generic
-            // properties like timestamps and annotations set on the folder.
-            id = PlacesUtils.mobileFolderId;
-          } else {
-            // For other folders, set `id` so that we can import timestamps
-            // and annotations at the end of this function.
-            id = PlacesUtils.bookmarks.createFolder(
-                   aContainer, aData.title, aIndex, aData.guid, this._source);
-          }
-          folderIdMap[aData.id] = id;
-          // Process children
-          if (aData.children) {
-            for (let i = 0; i < aData.children.length; i++) {
-              let child = aData.children[i];
-              let [folders, searches] =
-                this.importJSONNode(child, id, i, aContainer);
-              for (let j = 0; j < folders.length; j++) {
-                if (folders[j])
-                  folderIdMap[j] = folders[j];
-              }
-              searchIds = searchIds.concat(searches);
-            }
-          }
-        }
-        break;
-      case PlacesUtils.TYPE_X_MOZ_PLACE:
-        id = PlacesUtils.bookmarks.insertBookmark(
-               aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title, aData.guid, this._source);
-        if (aData.keyword) {
-          // POST data could be set in 2 ways:
-          // 1. new backups have a postData property
-          // 2. old backups have an item annotation
-          let postDataAnno = aData.annos &&
-                             aData.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
-          let postData = aData.postData || (postDataAnno && postDataAnno.value);
-          let kwPromise = PlacesUtils.keywords.insert({ keyword: aData.keyword,
-                                                        url: aData.uri,
-                                                        postData,
-                                                        source: this._source });
-          this._importPromises.push(kwPromise);
-        }
-        if (aData.tags) {
-          let tags = aData.tags.split(",").filter(aTag =>
-            aTag.length <= Ci.nsITaggingService.MAX_TAG_LENGTH);
-          if (tags.length) {
-            try {
-              PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags, this._source);
-            } catch (ex) {
-              // Invalid tag child, skip it.
-              Cu.reportError(`Unable to set tags "${tags.join(", ")}" for ${aData.uri}: ${ex}`);
-            }
-          }
-        }
-        if (aData.charset) {
-          PlacesUtils.annotations.setPageAnnotation(
-            NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
-            0, Ci.nsIAnnotationService.EXPIRE_NEVER);
-        }
-        if (aData.uri.substr(0, 6) == "place:")
-          searchIds.push(id);
-        if (aData.icon) {
-          try {
-            // Create a fake faviconURI to use (FIXME: bug 523932)
-            let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri);
-            PlacesUtils.favicons.replaceFaviconDataFromDataURL(
-              faviconURI, aData.icon, 0,
-              Services.scriptSecurityManager.getSystemPrincipal());
-            PlacesUtils.favicons.setAndFetchFaviconForPage(
-              NetUtil.newURI(aData.uri), faviconURI, false,
-              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
-              Services.scriptSecurityManager.getSystemPrincipal());
-          } catch (ex) {
-            Components.utils.reportError("Failed to import favicon data:" + ex);
-          }
-        }
-        if (aData.iconUri) {
-          try {
-            PlacesUtils.favicons.setAndFetchFaviconForPage(
-              NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false,
-              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
-              Services.scriptSecurityManager.getSystemPrincipal());
-          } catch (ex) {
-            Components.utils.reportError("Failed to import favicon URI:" + ex);
-          }
-        }
-        break;
-      case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
-        id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex, aData.guid, this._source);
-        break;
-      default:
-        // Unknown node type
-    }
-
-    // Set generic properties, valid for all nodes except tags and the mobile
-    // root.
-    if (id != -1 && id != PlacesUtils.mobileFolderId &&
-        aContainer != PlacesUtils.tagsFolderId &&
-        aGrandParentId != PlacesUtils.tagsFolderId) {
-      if (aData.dateAdded)
-        PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
-                                               this._source);
-      if (aData.lastModified)
-        PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified,
-                                                  this._source);
-      if (aData.annos && aData.annos.length)
-        PlacesUtils.setAnnotationsForItem(id, aData.annos, this._source);
-    }
-
-    return [folderIdMap, searchIds];
-  }
-}
+};
 
 function notifyObservers(topic) {
   Services.obs.notifyObservers(null, topic, "json");
 }
 
 /**
  * Replaces imported folder ids with their local counterparts in a place: URI.
  *
- * @param   aURI
+ * @param   {nsIURI} aQueryURI
  *          A place: URI with folder ids.
- * @param   aFolderIdMap
- *          An array mapping old folder id to new folder ids.
- * @returns the fixed up URI if all matched. If some matched, it returns
- *          the URI with only the matching folders included. If none matched
- *          it returns the input URI unchanged.
+ * @param   {Object} aFolderIdMap
+ *          An array mapping of old folder IDs to new folder GUIDs.
+ * @return {String} the fixed up URI if all matched. If some matched, it returns
+ *         the URI with only the matching folders included. If none matched
+ *         it returns the input URI unchanged.
+ */
+async function fixupQuery(aQueryURI, aFolderIdMap) {
+  const reGlobal = /folder=([0-9]+)/g;
+  const re = /([0-9]+)/;
+
+  // Unfortunately .replace can't handle async functions. Therefore,
+  // we find the folder guids we need to know the ids for first, then
+  // do the async request, and finally replace everything in one go.
+  let uri = aQueryURI.href;
+  let found = uri.match(reGlobal);
+  if (!found) {
+    return uri;
+  }
+
+  let queryFolderGuids = [];
+  for (let folderString of found) {
+    let existingFolderId = folderString.match(re)[0];
+    queryFolderGuids.push(aFolderIdMap[existingFolderId])
+  }
+
+  let newFolderIds = await PlacesUtils.promiseManyItemIds(queryFolderGuids);
+  let convert = function(str, p1) {
+    return "folder=" + newFolderIds.get(aFolderIdMap[p1]);
+  }
+  return uri.replace(reGlobal, convert);
+}
+
+/**
+ * A mapping of root folder names to Guids. To help fixupRootFolderGuid.
+ */
+const rootToFolderGuidMap = {
+  "placesRoot": PlacesUtils.bookmarks.rootGuid,
+  "bookmarksMenuFolder": PlacesUtils.bookmarks.menuGuid,
+  "unfiledBookmarksFolder": PlacesUtils.bookmarks.unfiledGuid,
+  "toolbarFolder": PlacesUtils.bookmarks.toolbarGuid,
+  "mobileFolder": PlacesUtils.bookmarks.mobileGuid
+};
+
+/**
+ * Updates a bookmark node from the json version to the places GUID. This
+ * will only change GUIDs for the built-in folders. Other folders will remain
+ * unchanged.
+ *
+ * @param {Object} A bookmark node that is updated with the new GUID if necessary.
+ */
+function fixupRootFolderGuid(node) {
+  if (!node.guid && node.root && node.root in rootToFolderGuidMap) {
+    node.guid = rootToFolderGuidMap[node.root];
+  }
+}
+
+/**
+ * Translates the JSON types for a node and its children into Places compatible
+ * types. Also handles updating of other parameters e.g. dateAdded and lastModified.
+ *
+ * @param {Object} node A node to be updated. If it contains children, they will
+ *                      be updated as well.
+ * @return {Array} An array containing two items:
+ *       - {Object} A map of current folder ids to GUIDS
+ *       - {Array} An array of GUIDs for nodes that contain query URIs
  */
-function fixupQuery(aQueryURI, aFolderIdMap) {
-  let convert = function(str, p1, offset, s) {
-    return "folder=" + aFolderIdMap[p1];
+function translateTreeTypes(node) {
+  let folderIdToGuidMap = {};
+  let searchGuids = [];
+
+  // Do the uri fixup first, so we can be consistent in this function.
+  if (node.uri) {
+    node.url = node.uri;
+    delete node.uri;
+  }
+
+  switch (node.type) {
+    case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
+      node.type = PlacesUtils.bookmarks.TYPE_FOLDER;
+
+      // Older type mobile folders have a random guid with an annotation. We need
+      // to make sure those go into the proper mobile folder.
+      let isMobileFolder = node.annos &&
+                           node.annos.some(anno => anno.name == PlacesUtils.MOBILE_ROOT_ANNO);
+      if (isMobileFolder) {
+        node.guid = PlacesUtils.bookmarks.mobileGuid;
+      } else {
+        // In case the Guid is broken, we need to fix it up.
+        fixupRootFolderGuid(node);
+      }
+
+      // Record the current id and the guid so that we can update any search
+      // queries later.
+      folderIdToGuidMap[node.id] = node.guid;
+      break;
+    case PlacesUtils.TYPE_X_MOZ_PLACE:
+      node.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+
+      if (node.url && node.url.substr(0, 6) == "place:") {
+        searchGuids.push(node.guid);
+      }
+
+      break;
+    case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
+      node.type = PlacesUtils.bookmarks.TYPE_SEPARATOR;
+      if ("title" in node) {
+        delete node.title;
+      }
+      break;
+    default:
+      // TODO We should handle this in a more robust fashion, see bug 1373610.
+      Cu.reportError(`Unexpected bookmark type ${node.type}`);
+      break;
+  }
+
+  if (node.dateAdded) {
+    node.dateAdded = PlacesUtils.toDate(node.dateAdded);
+  }
+
+  if (node.lastModified) {
+    let lastModified = PlacesUtils.toDate(node.lastModified);
+    // Ensure we get a last modified date that's later or equal to the dateAdded
+    // so that we don't upset the Bookmarks API.
+    if (lastModified >= node.dataAdded) {
+      node.lastModified = lastModified;
+    } else {
+      delete node.lastModified;
+    }
+  }
+
+  if (node.tags) {
+     // Separate any tags into an array, and ignore any that are too long.
+    node.tags = node.tags.split(",").filter(aTag =>
+      aTag.length > 0 && aTag.length <= Ci.nsITaggingService.MAX_TAG_LENGTH);
+
+    // If we end up with none, then delete the property completely.
+    if (!node.tags.length) {
+      delete node.tags;
+    }
   }
-  let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
+
+  // Sometimes postData can be null, so delete it to make the validators happy.
+  if (node.postData == null) {
+    delete node.postData;
+  }
+
+  // Now handle any children.
+  if (!node.children) {
+    return [folderIdToGuidMap, searchGuids];
+  }
+
+  // First sort the children by index.
+  node.children = node.children.sort((a, b) => {
+    return a.index - b.index;
+  });
+
+  // Now do any adjustments required for the children.
+  for (let child of node.children) {
+    let [folders, searches] = translateTreeTypes(child);
+    folderIdToGuidMap = Object.assign(folderIdToGuidMap, folders);
+    searchGuids = searchGuids.concat(searches);
+  }
+
+  return [folderIdToGuidMap, searchGuids];
+}
 
-  return NetUtil.newURI(stringURI);
+/**
+ * Handles inserting favicons into the database for a bookmark node.
+ * It is assumed the node has already been inserted into the bookmarks
+ * database.
+ *
+ * @param {Object} node The bookmark node for icons to be inserted.
+ */
+function insertFaviconForNode(node) {
+  if (node.icon) {
+    try {
+      // Create a fake faviconURI to use (FIXME: bug 523932)
+      let faviconURI = Services.io.newURI("fake-favicon-uri:" + node.url);
+      PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+        faviconURI, node.icon, 0,
+        Services.scriptSecurityManager.getSystemPrincipal());
+      PlacesUtils.favicons.setAndFetchFaviconForPage(
+        Services.io.newURI(node.url), faviconURI, false,
+        PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+        Services.scriptSecurityManager.getSystemPrincipal());
+    } catch (ex) {
+      Components.utils.reportError("Failed to import favicon data:" + ex);
+    }
+  }
+
+  if (!node.iconUri) {
+    return;
+  }
+
+  try {
+    PlacesUtils.favicons.setAndFetchFaviconForPage(
+      Services.io.newURI(node.url), Services.io.newURI(node.iconUri), false,
+      PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+      Services.scriptSecurityManager.getSystemPrincipal());
+  } catch (ex) {
+    Components.utils.reportError("Failed to import favicon URI:" + ex);
+  }
 }
+
+/**
+ * Handles inserting favicons into the database for a bookmark tree - a node
+ * and its children.
+ *
+ * It is assumed the nodes have already been inserted into the bookmarks
+ * database.
+ *
+ * @param {Object} nodeTree The bookmark node tree for icons to be inserted.
+ */
+function insertFaviconsForTree(nodeTree) {
+  insertFaviconForNode(nodeTree);
+
+  if (nodeTree.children) {
+    for (let child of nodeTree.children) {
+      insertFaviconsForTree(child);
+    }
+  }
+}
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -145,16 +145,22 @@ var Bookmarks = Object.freeze({
    toolbarGuid: "toolbar_____",
    unfiledGuid: "unfiled_____",
    mobileGuid:  "mobile______",
 
    // With bug 424160, tags will stop being bookmarks, thus this root will
    // be removed.  Do not rely on this, rather use the tagging service API.
    tagsGuid:    "tags________",
 
+   /**
+    * The GUIDs of the user content root folders that we support, for easy access
+    * as a set.
+    */
+   userContentRoots: ["toolbar_____", "menu________", "unfiled_____", "mobile______"],
+
   /**
    * Inserts a bookmark-item into the bookmarks tree.
    *
    * For creating a bookmark, the following set of properties is required:
    *  - type
    *  - parentGuid
    *  - url, only for bookmarked URLs
    *
@@ -258,17 +264,17 @@ var Bookmarks = Object.freeze({
    *   ]
    * }
    *
    * Children will be appended to any existing children of the parent
    * that is specified. The source specified on the root of the tree
    * will be used for all the items inserted. Any indices or custom parentGuids
    * set on children will be ignored and overwritten.
    *
-   * @param tree
+   * @param {Object} tree
    *        object representing a tree of bookmark items to insert.
    *
    * @return {Promise} resolved when the creation is complete.
    * @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.
    */
   insertTree(tree) {
@@ -276,17 +282,17 @@ var Bookmarks = Object.freeze({
       throw new Error("Should be provided a valid tree object.");
     }
 
     if (!Array.isArray(tree.children) || !tree.children.length) {
       throw new Error("Should have a non-zero number of children to insert.");
     }
 
     if (!PlacesUtils.isValidGuid(tree.guid)) {
-      throw new Error("The parent guid is not valid.");
+      throw new Error(`The parent guid is not valid (${tree.guid} ${tree.title}).`);
     }
 
     if (tree.guid == this.rootGuid) {
       throw new Error("Can't insert into the root.");
     }
 
     if (tree.guid == this.tagsGuid) {
       throw new Error("Can't use insertTree to insert tags.");
@@ -294,16 +300,17 @@ var Bookmarks = Object.freeze({
 
     if (tree.hasOwnProperty("source") &&
         !Object.values(this.SOURCES).includes(tree.source)) {
       throw new Error("Can't use source value " + tree.source);
     }
 
     // Serialize the tree into an array of items to insert into the db.
     let insertInfos = [];
+    let insertLivemarkInfos = [];
     let urlsThatMightNeedPlaces = [];
 
     // We want to use the same 'last added' time for all the entries
     // we import (so they won't differ by a few ms based on where
     // they are in the tree, and so we don't needlessly construct
     // multiple dates).
     let fallbackLastAdded = new Date();
 
@@ -351,26 +358,47 @@ var Bookmarks = Object.freeze({
           dateAdded: { defaultValue: time,
                        validIf: b => !b.lastModified ||
                                       b.dateAdded <= b.lastModified },
           lastModified: { defaultValue: time,
                           validIf: b => (!b.dateAdded && b.lastModified >= time) ||
                                         (b.dateAdded && b.lastModified >= b.dateAdded) },
           index: { replaceWith: indexToUse++ },
           source: { replaceWith: source },
+          annos: {},
+          keyword: { validIf: b => b.type == TYPE_BOOKMARK },
+          charset: { validIf: b => b.type == TYPE_BOOKMARK },
+          postData: { validIf: b => b.type == TYPE_BOOKMARK },
+          tags: { validIf: b => b.type == TYPE_BOOKMARK },
           children: { validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children) }
         });
+
         if (shouldUseNullIndices) {
           insertInfo.index = null;
         }
         // Store the URL if this is a bookmark, so we can ensure we create an
         // entry in moz_places for it.
         if (insertInfo.type == Bookmarks.TYPE_BOOKMARK) {
           urlsThatMightNeedPlaces.push(insertInfo.url);
         }
+
+        // As we don't track indexes for children of root folders, and we
+        // insert livemarks separately, we create a temporary placeholder in
+        // the bookmarks, and later we'll replace it by the real livemark.
+        if (isLivemark(insertInfo)) {
+          // Make the current insertInfo item a placeholder.
+          let livemarkInfo = Object.assign({}, insertInfo);
+
+          // Delete the annotations that make it a livemark.
+          delete insertInfo.annos;
+
+          // Now save the livemark info for later.
+          insertLivemarkInfos.push(livemarkInfo);
+        }
+
         insertInfos.push(insertInfo);
         // Process any children. We have to use info.children here rather than
         // insertInfo.children because validateBookmarkObject doesn't copy over
         // the children ref, as the default bookmark validators object doesn't
         // know about children.
         if (info.children) {
           // start children of this item off at index 0.
           let childrenLastAdded = appendInsertionInfoForInfoArray(info.children, 0, insertInfo.guid);
@@ -384,16 +412,17 @@ var Bookmarks = Object.freeze({
 
         // Ensure we track what time to update the parent to.
         if (insertInfo.dateAdded > lastAddedForParent) {
           lastAddedForParent = insertInfo.dateAdded;
         }
       }
       return lastAddedForParent;
     }
+
     // We want to validate synchronously, but we can't know the index at which
     // we're inserting into the parent. We just use NULL instead,
     // and the SQL query with which we insert will update it as necessary.
     let lastAddedForParent = appendInsertionInfoForInfoArray(tree.children, null, tree.guid);
 
     return (async function() {
       let parent = await fetchBookmark({ guid: tree.guid });
       if (!parent) {
@@ -401,16 +430,19 @@ var Bookmarks = Object.freeze({
       }
 
       if (parent._parentId == PlacesUtils.tagsFolderId) {
         throw new Error("Can't use insertTree to insert tags.");
       }
 
       await insertBookmarkTree(insertInfos, source, parent,
                                urlsThatMightNeedPlaces, lastAddedForParent);
+
+      await insertLivemarkData(insertLivemarkInfos);
+
       // Now update the indices of root items in the objects we return.
       // These may be wrong if someone else modified the table between
       // when we fetched the parent and inserted our items, but the actual
       // inserts will have been correct, and we don't want to query the DB
       // again if we don't have to. bug 1347230 covers improving this.
       let rootIndex = parent._childCount;
       for (let insertInfo of insertInfos) {
         if (insertInfo.parentGuid == tree.guid) {
@@ -421,23 +453,42 @@ var Bookmarks = Object.freeze({
       // complete we may stop using them.
       let itemIdMap = await PlacesUtils.promiseManyItemIds(insertInfos.map(info => info.guid));
       // Notify onItemAdded to listeners.
       let observers = PlacesUtils.bookmarks.getObservers();
       for (let i = 0; i < insertInfos.length; i++) {
         let item = insertInfos[i];
         let itemId = itemIdMap.get(item.guid);
         let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
-        notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
+        // For sub-folders, we need to make sure their children have the correct parent ids.
+        let parentId;
+        if (item.guid === parent.guid ||
+            Bookmarks.userContentRoots.includes(item.parentGuid)) {
+          // We're the item being inserted at the top-level, or we're a top-level
+          // folder, so the parent id won't have changed.
+          parentId = parent._id;
+        } else {
+          // This is a parent folder that's been updated, so we need to
+          // use the new item id.
+          parentId = itemIdMap.get(item.parentGuid);
+        }
+
+        notify(observers, "onItemAdded", [ itemId, parentId, item.index,
                                            item.type, uri, item.title || null,
                                            PlacesUtils.toPRTime(item.dateAdded), item.guid,
                                            item.parentGuid, item.source ],
                                          { isTagging: false });
         // Remove non-enumerable properties.
         delete item.source;
+
+        // Note, annotations for livemark data are deleted from insertInfo
+        // within appendInsertionInfoForInfoArray, so we won't be duplicating
+        // the insertions here.
+        await handleBookmarkItemSpecialData(itemId, item);
+
         insertInfos[i] = Object.assign({}, item);
       }
       return insertInfos;
     })();
   },
 
   /**
    * Updates a bookmark-item.
@@ -693,28 +744,26 @@ var Bookmarks = Object.freeze({
    * @return {Promise} resolved when the removal is complete.
    * @resolves once the removal is complete.
    */
   eraseEverything(options = {}) {
     if (!options.source) {
       options.source = Bookmarks.SOURCES.DEFAULT;
     }
 
-    const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid,
-                          this.mobileGuid];
     return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
       async function(db) {
         let urls;
 
         await db.executeTransaction(async function() {
-          urls = await removeFoldersContents(db, folderGuids, options);
+          urls = await removeFoldersContents(db, Bookmarks.userContentRoots, options);
           const time = PlacesUtils.toPRTime(new Date());
           const syncChangeDelta =
             PlacesSyncUtils.bookmarks.determineSyncChangeDelta(options.source);
-          for (let folderGuid of folderGuids) {
+          for (let folderGuid of Bookmarks.userContentRoots) {
             await db.executeCached(
               `UPDATE moz_bookmarks SET lastModified = :time,
                                         syncChangeCounter = syncChangeCounter + :syncChangeDelta
                WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
               `, { folderGuid, time, syncChangeDelta });
           }
         });
 
@@ -1312,16 +1361,26 @@ function insertBookmark(item, parent) {
     // Don't return an empty title to the caller.
     if (item.hasOwnProperty("title") && item.title === null)
       delete item.title;
 
     return item;
   });
 }
 
+/**
+ * Determines if a bookmark is a Livemark depending on how it is annotated.
+ *
+ * @param {Object} node The bookmark node to check.
+ * @returns {Boolean} True if the node is a Livemark, false otherwise.
+ */
+function isLivemark(node) {
+  return node.annos && node.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI);
+}
+
 function insertBookmarkTree(items, source, parent, urls, lastAddedForParent) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmarkTree", async function(db) {
     await db.executeTransaction(async function transaction() {
       await maybeInsertManyPlaces(db, urls);
 
       let syncChangeDelta =
         PlacesSyncUtils.bookmarks.determineSyncChangeDelta(source);
       let syncStatus =
@@ -1352,16 +1411,112 @@ function insertBookmarkTree(items, sourc
 
     // We don't wait for the frecency calculation.
     updateFrecency(db, urls, true).catch(Cu.reportError);
 
     return items;
   });
 }
 
+/**
+ * Handles any Livemarks within the passed items.
+ *
+ * @param {Array} items Livemark items that need to be added.
+ */
+async function insertLivemarkData(items) {
+  for (let item of items) {
+    let feedURI = null;
+    let siteURI = null;
+    item.annos = item.annos.filter(function(aAnno) {
+      switch (aAnno.name) {
+        case PlacesUtils.LMANNO_FEEDURI:
+          feedURI = NetUtil.newURI(aAnno.value);
+          return false;
+        case PlacesUtils.LMANNO_SITEURI:
+          siteURI = NetUtil.newURI(aAnno.value);
+          return false;
+        default:
+          return true;
+      }
+    });
+
+    let index = null;
+
+    // Delete the placeholder but note the index of it, so that we
+    // can insert the livemark item at the right place.
+    let placeholder = await Bookmarks.fetch(item.guid);
+    index = placeholder.index;
+
+    await Bookmarks.remove(item.guid, {source: item.source});
+
+    if (feedURI) {
+      item.feedURI = feedURI;
+      item.siteURI = siteURI;
+      item.index = index;
+
+      if (item.dateAdded) {
+        item.dateAdded = PlacesUtils.toPRTime(item.dateAdded);
+      }
+      if (item.lastModified) {
+        item.lastModified = PlacesUtils.toPRTime(item.lastModified);
+      }
+
+      let livemark = await PlacesUtils.livemarks.addLivemark(item);
+
+      let id = livemark.id;
+      if (item.annos && item.annos.length) {
+        PlacesUtils.setAnnotationsForItem(id, item.annos,
+                                          item.source);
+      }
+    }
+  }
+}
+
+/**
+ * Handles special data on a bookmark, e.g. annotations, keywords, tags, charsets,
+ * inserting the data into the appropriate place.
+ *
+ * @param {Integer} itemId The ID of the item within the bookmarks database.
+ * @param {Object} item The bookmark item with possible special data to be inserted.
+ */
+async function handleBookmarkItemSpecialData(itemId, item) {
+  if (item.annos && item.annos.length) {
+    PlacesUtils.setAnnotationsForItem(itemId, item.annos, item.source)
+  }
+  if ("keyword" in item && item.keyword) {
+    // POST data could be set in 2 ways:
+    // 1. new backups have a postData property
+    // 2. old backups have an item annotation
+    let postDataAnno = item.annos &&
+                       item.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
+    let postData = item.postData || (postDataAnno && postDataAnno.value);
+    try {
+      await PlacesUtils.keywords.insert({
+        keyword: item.keyword,
+        url: item.url,
+        postData,
+        source: item.source
+      });
+    } catch (ex) {
+      Cu.reportError(`Failed to insert keywords: ${ex}`);
+    }
+  }
+  if ("tags" in item) {
+    try {
+      PlacesUtils.tagging.tagURI(NetUtil.newURI(item.url), item.tags, item._source);
+    } catch (ex) {
+      // Invalid tag child, skip it.
+      Cu.reportError(`Unable to set tags "${item.tags.join(", ")}" for ${item.url}: ${ex}`);
+    }
+  }
+  if ("charset" in item && item.charset) {
+    await PlacesUtils.setCharsetForURI(NetUtil.newURI(item.url), item.charset);
+  }
+}
+
 // Query implementation.
 
 async function queryBookmarks(info) {
   let queryParams = {
     tags_folder: await promiseTagsFolderId(),
     type: Bookmarks.TYPE_SEPARATOR,
   };
   // We're searching for bookmarks, so exclude tags and separators.
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -76,16 +76,40 @@ function getBackupFileForSameDate(aFilen
     for (let backupFile of backupFiles) {
       if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
         return backupFile;
     }
     return null;
   })();
 }
 
+/**
+ * Returns the top-level bookmark folders ids and guids.
+ *
+ * @return {Promise} Resolve with an array of objects containing id and guid
+ *                   when the query is complete.
+ */
+async function getTopLevelFolderIds() {
+  let db =  await PlacesUtils.promiseDBConnection();
+  let rows = await db.execute(
+    "SELECT id, guid FROM moz_bookmarks WHERE parent = :parentId",
+    { parentId: PlacesUtils.placesRootId }
+  );
+
+  let guids = [];
+  for (let row of rows) {
+    guids.push({
+      id: row.getResultByName("id"),
+      guid: row.getResultByName("guid")
+    });
+  }
+  return guids;
+}
+
+
 this.PlacesBackups = {
   /**
    * Matches the backup filename:
    *  0: file name
    *  1: date in form Y-m-d
    *  2: bookmarks count
    *  3: contents hash
    *  4: file extension
@@ -539,11 +563,49 @@ this.PlacesBackups = {
     try {
       Services.telemetry
               .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
               .add(Date.now() - startTime);
     } catch (ex) {
       Components.utils.reportError("Unable to report telemetry.");
     }
     return [root, root.itemsCount];
-  }
+  },
+
+  /**
+   * Wrapper for PlacesUtils.bookmarks.eraseEverything that removes non-default
+   * roots.
+   *
+   * Note that default roots are preserved, only their children will be removed.
+   *
+   * TODO Ideally we wouldn't need to worry about non-default roots. However,
+   * until bug 1310299 is fixed, we still need to manage them.
+   *
+   * @param {Object} [options={}]
+   *        Additional options. Currently supports the following properties:
+   *         - source: The change source, forwarded to all bookmark observers.
+   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
+   *
+   * @return {Promise} resolved when the removal is complete.
+   * @resolves once the removal is complete.
+   */
+  async eraseEverythingIncludingUserRoots(options = {}) {
+    if (!options.source) {
+      options.source = PlacesUtils.bookmarks.SOURCES.DEFAULT;
+    }
+
+    let excludeItems =
+      PlacesUtils.annotations.getItemsWithAnnotation(PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
+
+    let rootFolderChildren = await getTopLevelFolderIds();
+
+    // We only need to do top-level roots here.
+    for (let child of rootFolderChildren) {
+      if (!PlacesUtils.bookmarks.userContentRoots.includes(child.guid) &&
+          child.guid != PlacesUtils.bookmarks.tagsGuid &&
+          !excludeItems.includes(child.id)) {
+       await PlacesUtils.bookmarks.remove(child.guid, {source: options.source});
+      }
+    }
+
+    return PlacesUtils.bookmarks.eraseEverything(options);
+  },
 }
-
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -244,16 +244,21 @@ const BOOKMARK_VALIDATORS = Object.freez
     if (typeof(v) === "string")
       return new URL(v);
     if (v instanceof Ci.nsIURI)
       return new URL(v.spec);
     return v;
   },
   source: simpleValidateFunc(v => Number.isInteger(v) &&
                                   Object.values(PlacesUtils.bookmarks.SOURCES).includes(v)),
+  annos: simpleValidateFunc(v => Array.isArray(v) && v.length),
+  keyword: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
+  charset: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
+  postData: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
+  tags: simpleValidateFunc(v => Array.isArray(v) && v.length),
 });
 
 // Sync bookmark records can contain additional properties.
 const SYNC_BOOKMARK_VALIDATORS = Object.freeze({
   // Sync uses Places GUIDs for all records except roots.
   syncId: simpleValidateFunc(v => typeof v == "string" && (
                                   (PlacesSyncUtils.bookmarks.ROOTS.includes(v) ||
                                    PlacesUtils.isValidGuid(v)))),
@@ -1569,18 +1574,18 @@ this.PlacesUtils = {
     if (index != -1) {
       this._bookmarksServiceObserversQueue.splice(index, 1);
     }
   },
 
   /**
    * Sets the character-set for a URI.
    *
-   * @param aURI nsIURI
-   * @param aCharset character-set value.
+   * @param {nsIURI} aURI
+   * @param {String} aCharset character-set value.
    * @return {Promise}
    */
   setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
     return new Promise(resolve => {
 
       // Delaying to catch issues with asynchronous behavior while waiting
       // to implement asynchronous annotations in bug 699844.
       Services.tm.dispatchToMainThread(function() {
@@ -1718,23 +1723,32 @@ this.PlacesUtils = {
 
   /**
    * Get the item id for an item (a bookmark, a folder or a separator) given
    * its unique id.
    *
    * @param aGuid
    *        an item GUID
    * @return {Promise}
-   * @resolves to the GUID.
+   * @resolves to the item id.
    * @rejects if there's no item for the given GUID.
    */
   promiseItemId(aGuid) {
     return GuidHelper.getItemId(aGuid)
   },
 
+  /**
+   * Get the item ids for multiple items (a bookmark, a folder or a separator)
+   * given the unique ids for each item.
+   *
+   * @param {Array} aGuids An array of item GUIDs.
+   * @return {Promise}
+   * @resolves to a Map of item ids.
+   * @rejects if not all of the GUIDs could be found.
+   */
   promiseManyItemIds(aGuids) {
     return GuidHelper.getManyItemIds(aGuids);
   },
 
   /**
    * Invalidate the GUID cache for the given itemId.
    *
    * @param aItemId
--- a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
+++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
@@ -35,90 +35,146 @@ scenarios:
 
 */
 
 const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX;
 
 var test = {
   _testRootId: null,
   _testRootTitle: "test root",
-  _folderIds: [],
+  _folderGuids: [],
   _bookmarkURIs: [],
   _count: 3,
+  _extraBookmarksCount: 10,
 
-  populate: function populate() {
+  populate: async function populate() {
     // folder to hold this test
-    PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
-    this._testRootId =
-      PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
-                                         this._testRootTitle, DEFAULT_INDEX);
+    await PlacesUtils.bookmarks.eraseEverything();
+
+    let testFolderItems = [];
+    // Set a date 60 seconds ago, so that we can set newer bookmarks later.
+    let dateAdded = new Date(new Date() - 60000);
 
     // create test folders each with a bookmark
-    for (var i = 0; i < this._count; i++) {
-      var folderId =
-        PlacesUtils.bookmarks.createFolder(this._testRootId, "folder" + i, DEFAULT_INDEX);
-      this._folderIds.push(folderId)
-
-      var bookmarkURI = uri("http://" + i);
-      PlacesUtils.bookmarks.insertBookmark(folderId, bookmarkURI,
-                                           DEFAULT_INDEX, "bookmark" + i);
-      this._bookmarkURIs.push(bookmarkURI);
+    for (let i = 0; i < this._count; i++) {
+      this._folderGuids.push(PlacesUtils.history.makeGuid());
+      testFolderItems.push({
+        guid: this._folderGuids[i],
+        title: `folder${i}`,
+        type: PlacesUtils.bookmarks.TYPE_FOLDER,
+        dateAdded,
+        children: [{
+          dateAdded,
+          url: `http://${i}`,
+          title: `bookmark${i}`,
+        }]
+      });
     }
 
+    let bookmarksTree = {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      children: [{
+        dateAdded,
+        title: this._testRootTitle,
+        type: PlacesUtils.bookmarks.TYPE_FOLDER,
+        children: testFolderItems
+      }]
+    };
+
+    let insertedBookmarks = await PlacesUtils.bookmarks.insertTree(bookmarksTree);
+
     // create a query URI with 1 folder (ie: folder shortcut)
-    this._queryURI1 = uri("place:folder=" + this._folderIds[0] + "&queryType=1");
+    let folderIdsMap = await PlacesUtils.promiseManyItemIds(this._folderGuids);
+    let folderIds = [];
+    for (let id of folderIdsMap.values()) {
+      folderIds.push(id);
+    }
+
+    this._queryURI1 = `place:folder=${folderIdsMap.get(this._folderGuids[0])}&queryType=1`;
     this._queryTitle1 = "query1";
-    PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI1,
-                                         DEFAULT_INDEX, this._queryTitle1);
+    await PlacesUtils.bookmarks.insert({
+      parentGuid: insertedBookmarks[0].guid,
+      dateAdded,
+      url: this._queryURI1,
+      title: this._queryTitle1
+    });
 
     // create a query URI with _count folders
-    this._queryURI2 = uri("place:folder=" + this._folderIds.join("&folder=") + "&queryType=1");
+    this._queryURI2 = `place:folder=${folderIds.join("&folder=")}&queryType=1`;
     this._queryTitle2 = "query2";
-    PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI2,
-                                         DEFAULT_INDEX, this._queryTitle2);
+    await PlacesUtils.bookmarks.insert({
+      parentGuid: insertedBookmarks[0].guid,
+      dateAdded,
+      url: this._queryURI2,
+      title: this._queryTitle2
+    });
 
     // create a query URI with _count queries (each with a folder)
     // first get a query object for each folder
-    var queries = this._folderIds.map(function(aFolderId) {
+    var queries = folderIds.map(function(aFolderId) {
       var query = PlacesUtils.history.getNewQuery();
       query.setFolders([aFolderId], 1);
       return query;
     });
+
     var options = PlacesUtils.history.getNewQueryOptions();
     options.queryType = options.QUERY_TYPE_BOOKMARKS;
     this._queryURI3 =
-      uri(PlacesUtils.history.queriesToQueryString(queries, queries.length, options));
+      PlacesUtils.history.queriesToQueryString(queries, queries.length, options);
     this._queryTitle3 = "query3";
-    PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI3,
-                                         DEFAULT_INDEX, this._queryTitle3);
+    await PlacesUtils.bookmarks.insert({
+      parentGuid: insertedBookmarks[0].guid,
+      dateAdded,
+      url: this._queryURI3,
+      title: this._queryTitle3
+    });
+
+    // Create a query URI for most recent bookmarks with NO folders specified.
+    this._queryURI4 = "place:queryType=1&sort=12&excludeItemIfParentHasAnnotation=livemark%2FfeedURI&maxResults=10&excludeQueries=1";
+    this._queryTitle4 = "query4";
+    await PlacesUtils.bookmarks.insert({
+      parentGuid: insertedBookmarks[0].guid,
+      dateAdded,
+      url: this._queryURI4,
+      title: this._queryTitle4
+    });
+
+    dump_table("moz_bookmarks");
+    dump_table("moz_places");
   },
 
   clean() {},
 
-  validate: function validate() {
-    // Throw a wrench in the works by inserting some new bookmarks,
-    // ensuring folder ids won't be the same, when restoring.
-    for (let i = 0; i < 10; i++) {
-      PlacesUtils.bookmarks.
-                  insertBookmark(PlacesUtils.bookmarksMenuFolderId, uri("http://aaaa" + i), DEFAULT_INDEX, "");
+  validate: async function validate(addExtras) {
+    if (addExtras) {
+      // Throw a wrench in the works by inserting some new bookmarks,
+      // ensuring folder ids won't be the same, when restoring.
+      let date = new Date() - (this._extraBookmarksCount * 1000);
+      for (let i = 0; i < this._extraBookmarksCount; i++) {
+        await PlacesUtils.bookmarks.insert({
+          parentGuid: PlacesUtils.bookmarks.menuGuid,
+          url: uri("http://aaaa" + i),
+          dateAdded: new Date(date + ((this._extraBookmarksCount - i) * 1000)),
+        });
+      }
     }
 
     var toolbar =
       PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId,
                                     false, true).root;
     do_check_true(toolbar.childCount, 1);
 
     var folderNode = toolbar.getChild(0);
     do_check_eq(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
     do_check_eq(folderNode.title, this._testRootTitle);
     folderNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
     folderNode.containerOpen = true;
 
-    // |_count| folders + the query node
-    do_check_eq(folderNode.childCount, this._count + 3);
+    // |_count| folders + the query nodes
+    do_check_eq(folderNode.childCount, this._count + 4);
 
     for (let i = 0; i < this._count; i++) {
       var subFolder = folderNode.getChild(i);
       do_check_eq(subFolder.title, "folder" + i);
       subFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode);
       subFolder.containerOpen = true;
       do_check_eq(subFolder.childCount, 1);
       var child = subFolder.getChild(0);
@@ -130,31 +186,34 @@ var test = {
     this.validateQueryNode1(folderNode.getChild(this._count));
 
     // validate folders query
     this.validateQueryNode2(folderNode.getChild(this._count + 1));
 
     // validate multiple queries query
     this.validateQueryNode3(folderNode.getChild(this._count + 2));
 
+    // validate recent folders query
+    this.validateQueryNode4(folderNode.getChild(this._count + 3));
+
     // clean up
     folderNode.containerOpen = false;
     toolbar.containerOpen = false;
   },
 
   validateQueryNode1: function validateQueryNode1(aNode) {
     do_check_eq(aNode.title, this._queryTitle1);
     do_check_true(PlacesUtils.nodeIsFolder(aNode));
 
     aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
     aNode.containerOpen = true;
     do_check_eq(aNode.childCount, 1);
     var child = aNode.getChild(0);
-    do_check_true(uri(child.uri).equals(uri("http://0")))
-    do_check_eq(child.title, "bookmark0")
+    do_check_true(uri(child.uri).equals(uri("http://0")));
+    do_check_eq(child.title, "bookmark0");
     aNode.containerOpen = false;
   },
 
   validateQueryNode2: function validateQueryNode2(aNode) {
     do_check_eq(aNode.title, this._queryTitle2);
     do_check_true(PlacesUtils.nodeIsQuery(aNode));
 
     aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
@@ -176,46 +235,57 @@ var test = {
     aNode.containerOpen = true;
     do_check_eq(aNode.childCount, this._count);
     for (var i = 0; i < aNode.childCount; i++) {
       var child = aNode.getChild(i);
       do_check_true(uri(child.uri).equals(uri("http://" + i)))
       do_check_eq(child.title, "bookmark" + i)
     }
     aNode.containerOpen = false;
-  }
+  },
+
+  validateQueryNode4(aNode) {
+    do_check_eq(aNode.title, this._queryTitle4);
+    do_check_true(PlacesUtils.nodeIsQuery(aNode));
+
+    aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+    aNode.containerOpen = true;
+    // The query will list the extra bookmarks added at the start of validate.
+    do_check_eq(aNode.childCount, this._extraBookmarksCount);
+    for (var i = 0; i < aNode.childCount; i++) {
+      var child = aNode.getChild(i);
+      do_check_eq(child.uri, `http://aaaa${i}/`);
+    }
+    aNode.containerOpen = false;
+  },
 }
 tests.push(test);
 
-function run_test() {
-  run_next_test();
-}
-
 add_task(async function() {
   // make json file
   let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
 
   // populate db
-  tests.forEach(function(aTest) {
-    aTest.populate();
+  for (let singleTest of tests) {
+    await singleTest.populate();
     // sanity
-    aTest.validate();
-  });
+    await singleTest.validate(true);
+  }
 
   // export json to file
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // clean
-  tests.forEach(function(aTest) {
-    aTest.clean();
-  });
+  for (let singleTest of tests) {
+    singleTest.clean();
+  }
 
   // restore json file
   await BookmarkJSONUtils.importFromFile(jsonFile, true);
 
   // validate
-  tests.forEach(function(aTest) {
-    aTest.validate();
-  });
+  for (let singleTest of tests) {
+    await singleTest.validate(false);
+  }
 
   // clean up
   await OS.File.remove(jsonFile);
 });
--- a/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
@@ -1,15 +1,14 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup";
 // Menu, Toolbar, Unsorted, Tags, Mobile
 const PLACES_ROOTS_COUNT  = 5;
 var tests = [];
 
 /*
 
 Backup/restore tests example:
 
@@ -44,27 +43,27 @@ var test = {
     // add a test bookmark to be exclude
     this._restoreRootExcludeURI = uri("http://exclude.uri");
     var exItemId = PlacesUtils.bookmarks
                               .insertBookmark(restoreRootId,
                                               this._restoreRootExcludeURI,
                                               idx, "exclude uri");
     // Annotate the bookmark for exclusion.
     PlacesUtils.annotations.setItemAnnotation(exItemId,
-                                              EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
+                                              PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
                                               PlacesUtils.annotations.EXPIRE_NEVER);
 
     // create a root to be exclude
     this._excludeRootTitle = "exclude root";
     this._excludeRootId = PlacesUtils.bookmarks
                                      .createFolder(PlacesUtils.placesRootId,
                                                    this._excludeRootTitle, idx);
     // Annotate the root for exclusion.
     PlacesUtils.annotations.setItemAnnotation(this._excludeRootId,
-                                              EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
+                                              PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
                                               PlacesUtils.annotations.EXPIRE_NEVER);
     // add a test bookmark exclude by exclusion of its parent
     PlacesUtils.bookmarks.insertBookmark(this._excludeRootId,
                                          this._restoreRootExcludeURI,
                                          idx, "exclude uri");
   },
 
   validate: function validate(aEmptyBookmarks) {
@@ -100,36 +99,40 @@ var test = {
     var restoreRootChildNode = restoreRootNode.getChild(0);
     do_check_eq(restoreRootChildNode.uri, this._restoreRootURI.spec);
     restoreRootNode.containerOpen = false;
 
     rootNode.containerOpen = false;
   }
 }
 
-function run_test() {
-  run_next_test();
-}
+// make json file
+var jsonFile;
 
-add_task(async function() {
-  // make json file
-  let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+add_task(async function setup() {
+  jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+});
 
+add_task(async function test_export_import_excluded_file() {
   // populate db
   test.populate();
 
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // restore json file
+  do_print("Restoring json file");
   await BookmarkJSONUtils.importFromFile(jsonFile, true);
 
   // validate without removing all bookmarks
   // restore do not remove backup exclude entries
+  do_print("Validating...");
   test.validate(false);
+});
 
+add_task(async function test_clearing_then_importing() {
   // cleanup
   await PlacesUtils.bookmarks.eraseEverything();
   // manually remove the excluded root
   PlacesUtils.bookmarks.removeItem(test._excludeRootId);
   // restore json file
   await BookmarkJSONUtils.importFromFile(jsonFile, true);
 
   // validate after a complete bookmarks cleanup
--- a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -15,16 +15,19 @@ var myTest = {
   validate: function () { ... query for your bookmarks ... }
 }
 
 this.push(myTest);
 
 */
 
 tests.push({
+  // Initialise something to avoid undefined property warnings in validate.
+  _litterTitle: "",
+
   populate: function populate() {
     // check initial size
     var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
                                                  false, false).root;
     do_check_eq(rootNode.childCount, 5);
 
     // create a test root
     this._folderTitle = "test folder";
@@ -114,20 +117,16 @@ tests.push({
         node.containerOpen = false;
       }
     }
     do_check_eq(foundTestFolder, 1);
     rootNode.containerOpen = false;
   }
 });
 
-function run_test() {
-  run_next_test();
-}
-
 add_task(async function() {
   // make json file
   let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
 
   // populate db
   tests.forEach(function(aTest) {
     aTest.populate();
     // sanity
--- a/toolkit/components/places/tests/unit/bookmarks.json
+++ b/toolkit/components/places/tests/unit/bookmarks.json
@@ -45,38 +45,58 @@
               "parent": 6,
               "dateAdded": 1361551979365662,
               "lastModified": 1361551979368077,
               "type": "text/x-moz-place",
               "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/",
               "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
             },
             {
+              "guid": "OCyeUO5uu9FJ",
+              "index": 3,
+              "title": "About Us",
+              "id": 10,
+              "parent": 6,
+              "dateAdded": 1361551979376699,
+              "lastModified": 1361551979379060,
+              "type": "text/x-moz-place",
+              "uri": "http://en-us.www.mozilla.com/en-US/about/",
+              "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+            },
+            {
               "guid": "OCyeUO5uu9FI",
               "index": 2,
               "title": "Get Involved",
               "id": 9,
               "parent": 6,
               "dateAdded": 1361551979371071,
               "lastModified": 1361551979373745,
               "type": "text/x-moz-place",
               "uri": "http://en-us.www.mozilla.com/en-US/firefox/community/",
               "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
             },
             {
-              "guid": "OCyeUO5uu9FJ",
-              "index": 3,
-              "title": "About Us",
-              "id": 10,
-              "parent": 6,
-              "dateAdded": 1361551979376699,
-              "lastModified": 1361551979379060,
+              "guid": "QFM-QnE2ZpMz",
+              "title": "Test null postData",
+              "index": 4,
+              "dateAdded": 1481639510868000,
+              "lastModified": 1489563704300000,
+              "id": 17,
+              "charset": "UTF-8",
+              "annos": [
+                {
+                  "name": "bookmarkProperties/description",
+                  "flags": 0,
+                  "expires": 4,
+                  "value": "The best"
+                }
+              ],
               "type": "text/x-moz-place",
-              "uri": "http://en-us.www.mozilla.com/en-US/about/",
-              "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+              "uri": "http://example.com/search?q=%s&suggid=",
+              "postData": null
             }
           ]
         },
         {
           "guid": "OCyeUO5uu9FK",
           "index": 1,
           "title": "",
           "id": 11,
--- a/toolkit/components/places/tests/unit/test_384370.js
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -6,20 +6,16 @@ var tagData = [
   { uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), tags: ["dinosaur", "dj", "rad word"] }
 ];
 
 var bookmarkData = [
   { uri: uri("http://slint.us"), title: "indie, kentucky, music" },
   { uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), title: "dinosaur, dj, rad word" }
 ];
 
-function run_test() {
-  run_next_test();
-}
-
 /*
   HTML+FEATURES SUMMARY:
   - import legacy bookmarks
   - export as json, import, test (tests integrity of html > json)
   - export as html, import, test (tests integrity of json > html)
 
   BACKUP/RESTORE SUMMARY:
   - create a bookmark in each root
@@ -47,37 +43,41 @@ add_task(async function() {
                                          title });
   }
   for (let { uri, title } of bookmarkData) {
     await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                                          url: uri,
                                          title });
   }
 
-  await validate();
+  await validate("initial database");
 
   // Test exporting a Places canonical json file.
   // 1. export to bookmarks.exported.json
   await BookmarkJSONUtils.exportToFile(jsonFile);
   do_print("exported json");
 
   // 2. empty bookmarks db
   // 3. import bookmarks.exported.json
   await BookmarkJSONUtils.importFromFile(jsonFile, true);
   do_print("imported json");
 
   // 4. run the test-suite
-  await validate();
+  await validate("re-imported json");
   do_print("validated import");
 });
 
-async function validate() {
+async function validate(infoMsg) {
+  do_print(`Validating ${infoMsg}: testMenuBookmarks`);
   await testMenuBookmarks();
+  do_print(`Validating ${infoMsg}: testToolbarBookmarks`);
   await testToolbarBookmarks();
+  do_print(`Validating ${infoMsg}: testUnfiledBookmarks`);
   testUnfiledBookmarks();
+  do_print(`Validating ${infoMsg}: testTags`);
   testTags();
   await PlacesTestUtils.promiseAsyncUpdates();
 }
 
 // Tests a bookmarks datastore that has a set of bookmarks, etc
 // that flex each supported field and feature.
 async function testMenuBookmarks() {
   let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
--- a/toolkit/components/places/tests/unit/test_bookmarks_json.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -31,16 +31,20 @@ var test_bookmarks = {
           title: "Get Involved",
           url: "http://en-us.www.mozilla.com/en-US/firefox/community/",
           icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
         },
         { guid: "OCyeUO5uu9FJ",
           title: "About Us",
           url: "http://en-us.www.mozilla.com/en-US/about/",
           icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+        },
+        { guid: "QFM-QnE2ZpMz",
+          title: "Test null postData",
+          url: "http://example.com/search?q=%s&suggid="
         }
       ]
     },
     {
       guid: "OCyeUO5uu9FK",
       type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR
     },
     {
@@ -67,17 +71,20 @@ var test_bookmarks = {
     { guid: "OCyeUO5uu9FB",
       title: "Getting Started",
       url: "http://en-us.www.mozilla.com/en-US/firefox/central/",
       icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
     },
     { guid: "OCyeUO5uu9FR",
       title: "Latest Headlines",
       url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
-      feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
+      feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+      // Note: date gets truncated to milliseconds, whereas the value in bookmarks.json
+      // has full microseconds.
+      dateAdded: 1361551979451000,
     }
   ],
   unfiled: [
     { guid: "OCyeUO5uu9FW",
       title: "Example.tld",
       url: "http://example.tld/"
     }
   ]
@@ -187,17 +194,17 @@ function checkItem(aExpected, aNode) {
           PlacesUtils.favicons.getFaviconDataForPage(
             NetUtil.newURI(aExpected.url),
             function(aURI, aDataLen, aData, aMimeType) {
               deferred.resolve(aData);
             });
           let data = await deferred.promise;
           let base64Icon = "data:image/png;base64," +
                            base64EncodeString(String.fromCharCode.apply(String, data));
-          do_check_true(base64Icon == aExpected.icon);
+          do_check_eq(base64Icon, aExpected.icon);
           break;
         case "keyword": {
           let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
           Assert.equal(entry.keyword, aExpected.keyword);
           break;
         }
         case "guid":
           let guid = await PlacesUtils.promiseItemGuid(id);
--- a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -72,18 +72,18 @@ add_task(async function test_import_mobi
 
   await treeEquals(PlacesUtils.bookmarks.rootGuid, {
     guid: PlacesUtils.bookmarks.rootGuid,
     index: 0,
     children: [{
       guid: PlacesUtils.bookmarks.menuGuid,
       index: 0,
       children: [
-        { guid: "Utodo9b0oVws", index: 0 },
-        { guid: "X6lUyOspVYwi", index: 1 },
+        { guid: "X6lUyOspVYwi", index: 0 },
+        { guid: "Utodo9b0oVws", index: 1 },
       ],
     }, {
       guid: PlacesUtils.bookmarks.toolbarGuid,
       index: 1,
     }, {
       guid: PlacesUtils.bookmarks.unfiledGuid,
       index: 3,
     }, {
@@ -91,20 +91,22 @@ add_task(async function test_import_mobi
       index: 4,
       annos: [{
         name: "mobile/bookmarksRoot",
         flags: 0,
         expires: 4,
         value: 1,
       }],
       children: [
-        { guid: "a17yW6-nTxEJ", index: 0 },
-        { guid: "xV10h9Wi3FBM", index: 1 },
-        { guid: "_o8e1_zxTJFg", index: 2 },
-        { guid: "QCtSqkVYUbXB", index: 3 },
+        // The first two are in ..._import.json, the second two are in
+        // ..._merge.json
+        { guid: "_o8e1_zxTJFg", index: 0 },
+        { guid: "QCtSqkVYUbXB", index: 1 },
+        { guid: "a17yW6-nTxEJ", index: 2 },
+        { guid: "xV10h9Wi3FBM", index: 3 },
       ],
     }],
   }, "Should merge bookmarks root contents");
 
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
 add_task(async function test_restore_mobile_bookmarks_folder() {
@@ -164,19 +166,19 @@ add_task(async function test_import_mobi
 
   await treeEquals(PlacesUtils.bookmarks.rootGuid, {
     guid: PlacesUtils.bookmarks.rootGuid,
     index: 0,
     children: [{
       guid: PlacesUtils.bookmarks.menuGuid,
       index: 0,
       children: [
-        { guid: "Utodo9b0oVws", index: 0 },
-        { guid: "X6lUyOspVYwi", index: 1 },
-        { guid: "XF4yRP6bTuil", index: 2 },
+        { guid: "X6lUyOspVYwi", index: 0 },
+        { guid: "XF4yRP6bTuil", index: 1 },
+        { guid: "Utodo9b0oVws", index: 2 },
       ],
     }, {
       guid: PlacesUtils.bookmarks.toolbarGuid,
       index: 1,
       children: [{ guid: "buy7711R3ZgE", index: 0 }],
     }, {
       guid: PlacesUtils.bookmarks.unfiledGuid,
       index: 3,
@@ -186,20 +188,20 @@ add_task(async function test_import_mobi
       index: 4,
       annos: [{
         name: "mobile/bookmarksRoot",
         flags: 0,
         expires: 4,
         value: 1,
       }],
       children: [
-        { guid: "a17yW6-nTxEJ", index: 0 },
-        { guid: "xV10h9Wi3FBM", index: 1 },
-        { guid: "_o8e1_zxTJFg", index: 2 },
-        { guid: "QCtSqkVYUbXB", index: 3 },
+        { guid: "_o8e1_zxTJFg", index: 0 },
+        { guid: "QCtSqkVYUbXB", index: 1 },
+        { guid: "a17yW6-nTxEJ", index: 2 },
+        { guid: "xV10h9Wi3FBM", index: 3 },
       ],
     }],
   }, "Should merge bookmarks folder contents into mobile root");
 
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
 add_task(async function test_restore_multiple_bookmarks_folders() {
@@ -230,18 +232,18 @@ add_task(async function test_restore_mul
       index: 4,
       annos: [{
         name: "mobile/bookmarksRoot",
         flags: 0,
         expires: 4,
         value: 1,
       }],
       children: [
-        { guid: "sSZ86WT9WbN3", index: 0 },
-        { guid: "a17yW6-nTxEJ", index: 1 },
+        { guid: "a17yW6-nTxEJ", index: 0 },
+        { guid: "sSZ86WT9WbN3", index: 1 },
       ],
     }],
   }, "Should restore multiple bookmarks folder contents into root");
 
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
 add_task(async function test_import_multiple_bookmarks_folders() {
@@ -252,20 +254,20 @@ add_task(async function test_import_mult
 
   await treeEquals(PlacesUtils.bookmarks.rootGuid, {
     guid: PlacesUtils.bookmarks.rootGuid,
     index: 0,
     children: [{
       guid: PlacesUtils.bookmarks.menuGuid,
       index: 0,
       children: [
-        { guid: "buy7711R3ZgE", index: 0 },
-        { guid: "F_LBgd1fS_uQ", index: 1 },
-        { guid: "oIpmQXMWsXvY", index: 2 },
-        { guid: "X6lUyOspVYwi", index: 3 },
+        { guid: "X6lUyOspVYwi", index: 0 },
+        { guid: "buy7711R3ZgE", index: 1 },
+        { guid: "F_LBgd1fS_uQ", index: 2 },
+        { guid: "oIpmQXMWsXvY", index: 3 },
       ],
     }, {
       guid: PlacesUtils.bookmarks.toolbarGuid,
       index: 1,
       children: [{ guid: "Utodo9b0oVws", index: 0 }],
     }, {
       guid: PlacesUtils.bookmarks.unfiledGuid,
       index: 3,
@@ -275,18 +277,18 @@ add_task(async function test_import_mult
       index: 4,
       annos: [{
         name: "mobile/bookmarksRoot",
         flags: 0,
         expires: 4,
         value: 1,
       }],
       children: [
-        { guid: "sSZ86WT9WbN3", index: 0 },
-        { guid: "a17yW6-nTxEJ", index: 1 },
-        { guid: "_o8e1_zxTJFg", index: 2 },
-        { guid: "QCtSqkVYUbXB", index: 3 },
+        { guid: "_o8e1_zxTJFg", index: 0 },
+        { guid: "QCtSqkVYUbXB", index: 1 },
+        { guid: "a17yW6-nTxEJ", index: 2 },
+        { guid: "sSZ86WT9WbN3", index: 3 },
       ],
     }],
   }, "Should merge multiple mobile folders into root");
 
   await PlacesUtils.bookmarks.eraseEverything();
 });
--- a/toolkit/components/places/tests/unit/test_sync_utils.js
+++ b/toolkit/components/places/tests/unit/test_sync_utils.js
@@ -1,27 +1,23 @@
 Cu.import("resource://gre/modules/ObjectUtils.jsm");
 Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
 const {
   // `fetchGuidsWithAnno` isn't exported, but we can still access it here via a
   // backstage pass.
   fetchGuidsWithAnno,
 } = Cu.import("resource://gre/modules/PlacesSyncUtils.jsm", {});
 Cu.import("resource://testing-common/httpd.js");
-Cu.importGlobalProperties(["crypto", "URLSearchParams"]);
+Cu.importGlobalProperties(["URLSearchParams"]);
 
 const DESCRIPTION_ANNO = "bookmarkProperties/description";
 const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
 const SYNC_PARENT_ANNO = "sync/parent";
 
-function makeGuid() {
-  return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
-    pad: false,
-  });
-}
+var makeGuid = PlacesUtils.history.makeGuid;
 
 function makeLivemarkServer() {
   let server = new HttpServer();
   server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml"));
   server.start(-1);
   return {
     server,
     get site() {