Bug 1095426 - Convert JSON backups code to the new async Bookmarks.jsm API. r=mak
☠☠ backed out by 8b59ef19993d ☠ ☠
authorMark Banner <standard8@mozilla.com>
Fri, 12 May 2017 08:04:51 +0100
changeset 364374 4c465fe2d7c9c5e55574de790d307f3fb40f7c3f
parent 364373 0cf7134d243a52d0b0b70e1b41a9ac6132cfa5e3
child 364375 7d70baae3e157fcd51662443366d1e93277f5025
push id44882
push usermbanner@mozilla.com
push dateFri, 16 Jun 2017 12:11:56 +0000
treeherderautoland@4c465fe2d7c9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1095426
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1095426 - Convert JSON backups code to the new async Bookmarks.jsm API. r=mak MozReview-Commit-ID: FNZZGwWVSI2
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/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() {