Bug 1199077 - Split change sources for automatic and manual bookmark restores. r=mak draft
authorKit Cambridge <kit@yakshaving.ninja>
Thu, 01 Mar 2018 13:37:13 -0800
changeset 765147 f6691e30218f4492c6ec4dfcc2586b2256e692de
parent 765146 13d81caa4fd4535a6cf811a2b224762be1f52a07
child 765148 7050621de8e36160457633b9eff2d4ccc3582c29
push id101984
push userbmo:kit@mozilla.com
push dateFri, 09 Mar 2018 06:22:41 +0000
reviewersmak
bugs1199077
milestone60.0a1
Bug 1199077 - Split change sources for automatic and manual bookmark restores. r=mak MozReview-Commit-ID: 1glcCPj2X90
browser/components/migration/MigrationUtils.jsm
browser/components/nsBrowserGlue.js
browser/components/places/content/places.js
services/sync/modules/engines/bookmarks.js
services/sync/tests/unit/test_bookmark_engine.js
testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py
toolkit/components/places/BookmarkHTMLUtils.jsm
toolkit/components/places/BookmarkJSONUtils.jsm
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/PlacesSyncUtils.jsm
toolkit/components/places/nsINavBookmarksService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/tests/bookmarks/test_1129529.js
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/bookmarks/test_424958-json-quoted-folders.js
toolkit/components/places/tests/bookmarks/test_448584.js
toolkit/components/places/tests/bookmarks/test_458683.js
toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js
toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js
toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js
toolkit/components/places/tests/sync/test_bookmark_deletion.js
toolkit/components/places/tests/sync/test_sync_utils.js
toolkit/components/places/tests/unit/test_384370.js
toolkit/components/places/tests/unit/test_bookmarks_html.js
toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
toolkit/components/places/tests/unit/test_bookmarks_json.js
toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js
toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -369,17 +369,20 @@ var MigratorPrototype = {
       (async function() {
         // Tell nsBrowserGlue we're importing default bookmarks.
         let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].
                           getService(Ci.nsIObserver);
         browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, "");
 
         // Import the default bookmarks. We ignore whether or not we succeed.
         await BookmarkHTMLUtils.importFromURL(
-          "chrome://browser/locale/bookmarks.html", true).catch(r => r);
+          "chrome://browser/locale/bookmarks.html", {
+            replace: true,
+            source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+          }).catch(Cu.reportError);
 
         // We'll tell nsBrowserGlue we've imported bookmarks, but before that
         // we need to make sure we're going to know when it's finished
         // initializing places:
         let placesInitedPromise = new Promise(resolve => {
           let onPlacesInited = function() {
             Services.obs.removeObserver(onPlacesInited, TOPIC_PLACES_DEFAULTS_FINISHED);
             resolve();
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1599,17 +1599,20 @@ BrowserGlue.prototype = {
 
       // If the user did not require to restore default bookmarks, or import
       // from bookmarks.html, we will try to restore from JSON
       if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
         // get latest JSON backup
         lastBackupFile = await PlacesBackups.getMostRecentBackup();
         if (lastBackupFile) {
           // restore from JSON backup
-          await BookmarkJSONUtils.importFromFile(lastBackupFile, true);
+          await BookmarkJSONUtils.importFromFile(lastBackupFile, {
+            replace: true,
+            source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+          });
           importBookmarks = false;
         } else {
           // We have created a new database but we don't have any backup available
           importBookmarks = true;
           if (await OS.File.exists(BookmarkHTMLUtils.defaultPath)) {
             // If bookmarks.html is available in current profile import it...
             importBookmarksHTML = true;
           } else {
@@ -1647,17 +1650,20 @@ BrowserGlue.prototype = {
           bookmarksUrl = "chrome://browser/locale/bookmarks.html";
         } else if (await OS.File.exists(BookmarkHTMLUtils.defaultPath)) {
           bookmarksUrl = OS.Path.toFileURI(BookmarkHTMLUtils.defaultPath);
         }
 
         if (bookmarksUrl) {
           // Import from bookmarks.html file.
           try {
-            await BookmarkHTMLUtils.importFromURL(bookmarksUrl, true);
+            await BookmarkHTMLUtils.importFromURL(bookmarksUrl, {
+              replace: true,
+              source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+            });
           } catch (e) {
             Cu.reportError("Bookmarks.html file could be corrupt. " + e);
           }
           try {
             // Now apply distribution customized bookmarks.
             // This should always run after Places initialization.
             await this._distributionCustomizer.applyBookmarks();
             // Ensure that smart bookmarks are created once the operation is
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -400,17 +400,17 @@ var PlacesOrganizer = {
   /**
    * Open a file-picker and import the selected file into the bookmarks store
    */
   importFromFile: function PO_importFromFile() {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     let fpCallback = function fpCallback_done(aResult) {
       if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) {
         ChromeUtils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
-        BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false)
+        BookmarkHTMLUtils.importFromURL(fp.fileURL.spec)
                          .catch(Cu.reportError);
       }
     };
 
     fp.init(window, PlacesUIUtils.getString("SelectImport"),
             Ci.nsIFilePicker.modeOpen);
     fp.appendFilters(Ci.nsIFilePicker.filterHTML);
     fp.open(fpCallback);
@@ -539,17 +539,19 @@ var PlacesOrganizer = {
     // confirm ok to delete existing bookmarks
     if (!Services.prompt.confirm(null,
            PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
            PlacesUIUtils.getString("bookmarksRestoreAlert")))
       return;
 
     (async function() {
       try {
-        await BookmarkJSONUtils.importFromFile(aFilePath, true);
+        await BookmarkJSONUtils.importFromFile(aFilePath, {
+          replace: true,
+        });
       } catch (ex) {
         PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
       }
     })();
   },
 
   _showErrorAlert: function PO__showErrorAlert(aMsg) {
     var brandShortName = document.getElementById("brandStrings").
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -31,40 +31,38 @@ XPCOMUtils.defineLazyGetter(this, "Place
 
 XPCOMUtils.defineLazyGetter(this, "ANNOS_TO_TRACK", () => [
   PlacesSyncUtils.bookmarks.DESCRIPTION_ANNO,
   PlacesSyncUtils.bookmarks.SIDEBAR_ANNO, PlacesUtils.LMANNO_FEEDURI,
   PlacesUtils.LMANNO_SITEURI,
 ]);
 
 const FOLDER_SORTINDEX = 1000000;
-const {
-  SOURCE_SYNC,
-  SOURCE_IMPORT,
-  SOURCE_IMPORT_REPLACE,
-  SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
-} = Ci.nsINavBookmarksService;
 
 // Roots that should be deleted from the server, instead of applied locally.
 // This matches `AndroidBrowserBookmarksRepositorySession::forbiddenGUID`,
 // but allows tags because we don't want to reparent tag folders or tag items
 // to "unfiled".
 const FORBIDDEN_INCOMING_IDS = ["pinned", "places", "readinglist"];
 
 // Items with these parents should be deleted from the server. We allow
 // children of the Places root, to avoid orphaning left pane queries and other
 // descendants of custom roots.
 const FORBIDDEN_INCOMING_PARENT_IDS = ["pinned", "readinglist"];
 
-// The tracker ignores changes made by bookmark import and restore, and
-// changes made by Sync. We don't need to exclude `SOURCE_IMPORT`, but both
-// import and restore fire `bookmarks-restore-*` observer notifications, and
-// the tracker doesn't currently distinguish between the two.
-const IGNORED_SOURCES = [SOURCE_SYNC, SOURCE_IMPORT, SOURCE_IMPORT_REPLACE,
-                         SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN];
+// The tracker ignores changes made by import and restore, to avoid bumping the
+// score and triggering syncs during the process, as well as changes made by
+// Sync.
+XPCOMUtils.defineLazyGetter(this, "IGNORED_SOURCES", () => [
+  PlacesUtils.bookmarks.SOURCES.SYNC,
+  PlacesUtils.bookmarks.SOURCES.IMPORT,
+  PlacesUtils.bookmarks.SOURCES.RESTORE,
+  PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+  PlacesUtils.bookmarks.SOURCES.SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
+]);
 
 function isSyncedRootNode(node) {
   return node.root == "bookmarksMenuFolder" ||
          node.root == "unfiledBookmarksFolder" ||
          node.root == "toolbarFolder" ||
          node.root == "mobileFolder";
 }
 
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -302,17 +302,17 @@ async function test_restoreOrImport(engi
     // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
     let wbos = collection.keys(function(id) {
       return !["menu", "toolbar", "mobile", "unfiled", folder1.guid].includes(id);
     });
     Assert.equal(wbos.length, 1);
     Assert.equal(wbos[0], bmk2.guid);
 
     _(`Now ${verb} from a backup.`);
-    await bookmarkUtils.importFromFile(backupFilePath, replace);
+    await bookmarkUtils.importFromFile(backupFilePath, { replace });
     await engine._tracker.asyncObserver.promiseObserversComplete();
 
     let bookmarksCollection = server.user("foo").collection("bookmarks");
     if (replace) {
       _("Verify that we wiped the server.");
       Assert.ok(!bookmarksCollection);
     } else {
       _("Verify that we didn't wipe the server.");
--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py
@@ -74,17 +74,17 @@ class Places(BaseLib):
         retval = self.marionette.execute_async_script("""
           Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
 
           // Default bookmarks.html file is stored inside omni.jar,
           // so get it via a resource URI
           let defaultBookmarks = 'chrome://browser/locale/bookmarks.html';
 
           // Trigger the import of the default bookmarks
-          BookmarkHTMLUtils.importFromURL(defaultBookmarks, true)
+          BookmarkHTMLUtils.importFromURL(defaultBookmarks, { replace: true })
                            .then(() => marionetteScriptFinished(true))
                            .catch(() => marionetteScriptFinished(false));
         """, script_timeout=10000)
 
         if not retval:
             raise MarionetteException("Restore Default Bookmarks failed")
 
     # Browser history related helpers #
--- a/toolkit/components/places/BookmarkHTMLUtils.jsm
+++ b/toolkit/components/places/BookmarkHTMLUtils.jsm
@@ -122,56 +122,74 @@ function notifyObservers(aTopic, aInitia
 
 var BookmarkHTMLUtils = Object.freeze({
   /**
    * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
    *
    * @param aSpec
    *        String containing the "file:" URI for the existing "bookmarks.html"
    *        file to be loaded.
-   * @param aInitialImport
-   *        Whether this is the initial import executed on a new profile.
+   * @param [options.replace]
+   *        Whether we should erase existing bookmarks before loading.
+   *        Defaults to `false`.
+   * @param [options.source]
+   *        The bookmark change source, used to determine the sync status for
+   *        imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
+   *        `IMPORT` otherwise.
    *
    * @return {Promise}
    * @resolves When the new bookmarks have been created.
    * @rejects JavaScript exception.
    */
-  async importFromURL(aSpec, aInitialImport) {
+  async importFromURL(aSpec, {
+    replace: aInitialImport = false,
+    source: aSource = aInitialImport ? PlacesUtils.bookmarks.SOURCES.RESTORE :
+                                       PlacesUtils.bookmarks.SOURCES.IMPORT,
+  } = {}) {
     notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
     try {
-      let importer = new BookmarkImporter(aInitialImport);
+      let importer = new BookmarkImporter(aInitialImport, aSource);
       await importer.importFromURL(aSpec);
 
       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
     } catch (ex) {
       Cu.reportError("Failed to import bookmarks from " + aSpec + ": " + ex);
       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
       throw ex;
     }
   },
 
   /**
    * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
    *
    * @param aFilePath
    *        OS.File path string of the "bookmarks.html" file to be loaded.
-   * @param aInitialImport
-   *        Whether this is the initial import executed on a new profile.
+   * @param [options.replace]
+   *        Whether we should erase existing bookmarks before loading.
+   *        Defaults to `false`.
+   * @param [options.source]
+   *        The bookmark change source, used to determine the sync status for
+   *        imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
+   *        `IMPORT` otherwise.
    *
    * @return {Promise}
    * @resolves When the new bookmarks have been created.
    * @rejects JavaScript exception.
    */
-  async importFromFile(aFilePath, aInitialImport) {
+  async importFromFile(aFilePath, {
+    replace: aInitialImport = false,
+    source: aSource = aInitialImport ? PlacesUtils.bookmarks.SOURCES.RESTORE :
+                                       PlacesUtils.bookmarks.SOURCES.IMPORT,
+  } = {}) {
     notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
     try {
       if (!(await OS.File.exists(aFilePath))) {
         throw new Error("Cannot import from nonexisting html file: " + aFilePath);
       }
-      let importer = new BookmarkImporter(aInitialImport);
+      let importer = new BookmarkImporter(aInitialImport, aSource);
       await importer.importFromURL(OS.Path.toFileURI(aFilePath));
 
       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
     } catch (ex) {
       Cu.reportError("Failed to import bookmarks from " + aFilePath + ": " + ex);
       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
       throw ex;
     }
@@ -281,22 +299,19 @@ function Frame(aFolder) {
   /**
    * Contains the date-added and last-modified-date of an imported item.
    * Used to override the values set by insertBookmark, createFolder, etc.
    */
   this.previousDateAdded = null;
   this.previousLastModifiedDate = null;
 }
 
-function BookmarkImporter(aInitialImport) {
+function BookmarkImporter(aInitialImport, aSource) {
   this._isImportDefaults = aInitialImport;
-  // The bookmark change source, used to determine the sync status and change
-  // counter.
-  this._source = aInitialImport ? PlacesUtils.bookmarks.SOURCE_IMPORT_REPLACE :
-                                  PlacesUtils.bookmarks.SOURCE_IMPORT;
+  this._source = aSource;
 
   // This root is where we construct the bookmarks tree into, following the format
   // of the imported file.
   // If we're doing an initial import, the non-menu roots will be created as
   // children of this root, so in _getBookmarkTrees we'll split them out.
   // If we're not doing an initial import, everything gets imported under the
   // bookmark menu folder, so there won't be any need for _getBookmarkTrees to
   // do separation.
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -36,59 +36,73 @@ function generateHash(aString) {
 }
 
 var BookmarkJSONUtils = Object.freeze({
   /**
    * Import bookmarks from a url.
    *
    * @param aSpec
    *        url of the bookmark data.
-   * @param aReplace
-   *        Boolean if true, replace existing bookmarks, else merge.
+   * @param [options.replace]
+   *        Whether we should erase existing bookmarks before importing.
+   * @param [options.source]
+   *        The bookmark change source, used to determine the sync status for
+   *        imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
+   *        `IMPORT` otherwise.
    *
    * @return {Promise}
    * @resolves When the new bookmarks have been created.
    * @rejects JavaScript exception.
    */
-  importFromURL: function BJU_importFromURL(aSpec, aReplace) {
-    return (async function() {
-      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aReplace);
-      try {
-        let importer = new BookmarkImporter(aReplace);
-        await importer.importFromURL(aSpec);
+  async importFromURL(aSpec, {
+    replace: aReplace = false,
+    source: aSource = aReplace ? PlacesUtils.bookmarks.SOURCES.RESTORE :
+                                 PlacesUtils.bookmarks.SOURCES.IMPORT,
+  } = {}) {
+    notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aReplace);
+    try {
+      let importer = new BookmarkImporter(aReplace, aSource);
+      await importer.importFromURL(aSpec);
 
-        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aReplace);
-      } catch (ex) {
-        Cu.reportError("Failed to restore bookmarks from " + aSpec + ": " + ex);
-        notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aReplace);
-      }
-    })();
+      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aReplace);
+    } catch (ex) {
+      Cu.reportError("Failed to restore bookmarks from " + aSpec + ": " + ex);
+      notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aReplace);
+    }
   },
 
   /**
    * Restores bookmarks and tags from a JSON file.
    * @note any item annotated with "places/excludeFromBackup" won't be removed
    *       before executing the restore.
    *
    * @param aFilePath
    *        OS.File path string of bookmarks in JSON or JSONlz4 format to be restored.
-   * @param aReplace
-   *        Boolean if true, replace existing bookmarks, else merge.
+   * @param [options.replace]
+   *        Whether we should erase existing bookmarks before importing.
+   * @param [options.source]
+   *        The bookmark change source, used to determine the sync status for
+   *        imported bookmarks. Defaults to `RESTORE` if `replace = true`, or
+   *        `IMPORT` otherwise.
    *
    * @return {Promise}
    * @resolves When the new bookmarks have been created.
    * @rejects JavaScript exception.
    */
-  async importFromFile(aFilePath, aReplace) {
+  async importFromFile(aFilePath, {
+    replace: aReplace = false,
+    source: aSource = aReplace ? PlacesUtils.bookmarks.SOURCES.RESTORE :
+                                 PlacesUtils.bookmarks.SOURCES.IMPORT,
+  } = {}) {
     notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aReplace);
     try {
       if (!(await OS.File.exists(aFilePath)))
         throw new Error("Cannot restore from nonexisting json file");
 
-      let importer = new BookmarkImporter(aReplace);
+      let importer = new BookmarkImporter(aReplace, aSource);
       if (aFilePath.endsWith("jsonlz4")) {
         await importer.importFromCompressedFile(aFilePath);
       } else {
         await importer.importFromURL(OS.Path.toFileURI(aFilePath));
       }
       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aReplace);
     } catch (ex) {
       Cu.reportError("Failed to restore bookmarks from " + aFilePath + ": " + ex);
@@ -142,22 +156,19 @@ var BookmarkJSONUtils = Object.freeze({
     if (aOptions.compress)
       writeOptions.compression = "lz4";
 
     await OS.File.writeAtomic(aFilePath, jsonString, writeOptions);
     return { count, hash };
   }
 });
 
-function BookmarkImporter(aReplace) {
+function BookmarkImporter(aReplace, aSource) {
   this._replace = aReplace;
-  // The bookmark change source, used to determine the sync status and change
-  // counter.
-  this._source = aReplace ? PlacesUtils.bookmarks.SOURCE_IMPORT_REPLACE :
-                            PlacesUtils.bookmarks.SOURCE_IMPORT;
+  this._source = aSource;
 }
 BookmarkImporter.prototype = {
   /**
    * Import bookmarks from a url.
    *
    * @param aSpec
    *        url of the bookmark data.
    *
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -122,18 +122,19 @@ var Bookmarks = Object.freeze({
   /**
    * Bookmark change source constants, passed as optional properties and
    * forwarded to observers. See nsINavBookmarksService.idl for an explanation.
    */
   SOURCES: {
     DEFAULT: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
     SYNC: Ci.nsINavBookmarksService.SOURCE_SYNC,
     IMPORT: Ci.nsINavBookmarksService.SOURCE_IMPORT,
-    IMPORT_REPLACE: Ci.nsINavBookmarksService.SOURCE_IMPORT_REPLACE,
     SYNC_REPARENT_REMOVED_FOLDER_CHILDREN: Ci.nsINavBookmarksService.SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
+    RESTORE: Ci.nsINavBookmarksService.SOURCE_RESTORE,
+    RESTORE_ON_STARTUP: Ci.nsINavBookmarksService.SOURCE_RESTORE_ON_STARTUP,
   },
 
   /**
    * Special GUIDs associated with bookmark roots.
    * It's guaranteed that the roots will always have these guids.
    */
   rootGuid:    "root________",
   menuGuid:    "menu________",
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -979,17 +979,18 @@ const BookmarkSyncUtils = PlacesSyncUtil
   /**
    * Returns the record status for a new item inserted by a change source.
    */
   determineInitialSyncStatus(source) {
     if (source == PlacesUtils.bookmarks.SOURCES.SYNC) {
       // Incoming bookmarks are "NORMAL", since they already exist on the server.
       return PlacesUtils.bookmarks.SYNC_STATUS.NORMAL;
     }
-    if (source == PlacesUtils.bookmarks.SOURCES.IMPORT_REPLACE) {
+    if (source == PlacesUtils.bookmarks.SOURCES.RESTORE ||
+        source == PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP) {
       // If the user restores from a backup, or Places automatically recovers
       // from a corrupt database, all prior record tracking is lost. Setting the
       // status to "UNKNOWN" allows Record to reconcile restored bookmarks with
       // those on the server.
       return PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN;
     }
     // For all other sources, mark items as "NEW". We'll update their statuses
     // to "NORMAL" after the first record.
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -314,33 +314,44 @@ interface nsINavBookmarksService : nsISu
 
   // Change source constants. These are used to distinguish changes made by
   // Sync and bookmarks import from other Places consumers, though they can
   // be extended to support other callers. Sources are passed as optional
   // parameters to methods used by Sync, and forwarded to observers.
   const unsigned short SOURCE_DEFAULT = 0;
   const unsigned short SOURCE_SYNC = 1;
   const unsigned short SOURCE_IMPORT = 2;
-  const unsigned short SOURCE_IMPORT_REPLACE = 3;
   const unsigned short SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN = 4;
+  const unsigned short SOURCE_RESTORE = 5;
+  const unsigned short SOURCE_RESTORE_ON_STARTUP = 6;
 
   /**
-   * Sync status flags.
+   * Sync status flags, stored in Places for each item. These affect conflict
+   * resolution, when an item is changed both locally and remotely; deduping,
+   * when a local item matches a remote item with similar contents and different
+   * GUIDs; and whether we write a tombstone when an item is deleted locally.
+   *
+   * Status  | Description               | Conflict   | Can     | Needs
+   *         |                           | resolution | dedupe? | tombstone?
+   * -----------------------------------------------------------------------
+   * UNKNOWN | Automatically restored    | Prefer     | No      | No
+   *         | on startup to recover     | deletion   |         |
+   *         | from database corruption, |            |         |
+   *         | or sync ID change on      |            |         |
+   *         | server.                   |            |         |
+   * -----------------------------------------------------------------------
+   * NEW     | Item not uploaded to      | Prefer     | Yes     | No
+   *         | server yet, or Sync       | newer      |         |
+   *         | disconnected.             |            |         |
+   * -----------------------------------------------------------------------
+   * NORMAL  | Item uploaded to server.  | Prefer     | No      | Yes
+   *         |                           | newer      |         |
    */
-  // "UNKNOWN" means sync tracking information was lost because the item
-  // was restored from a backup, and needs to be reconciled with the server.
-  // This is set for migrated and restored bookmarks, and changed to "NORMAL"
-  // before upload.
   const unsigned short SYNC_STATUS_UNKNOWN = 0;
-  // "NEW" means the item has never been synced, so we don't need to write
-  // a tombstone if it's deleted. This is set for all new local bookmarks, and
-  // changed to "NORMAL" before upload.
   const unsigned short SYNC_STATUS_NEW = 1;
-  // "NORMAL" means the item has been uploaded to the server, and needs a
-  // tombstone if it's deleted.
   const unsigned short SYNC_STATUS_NORMAL = 2;
 
   /**
    * Inserts a child bookmark into the given folder.
    *
    *  @param aParentId
    *         The id of the parent folder
    *  @param aURI
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -134,17 +134,18 @@ DetermineSyncChangeDelta(uint16_t aSourc
 }
 
 // Returns the sync status for a new item inserted by a change source.
 inline int32_t
 DetermineInitialSyncStatus(uint16_t aSource) {
   if (aSource == nsINavBookmarksService::SOURCE_SYNC) {
     return nsINavBookmarksService::SYNC_STATUS_NORMAL;
   }
-  if (aSource == nsINavBookmarksService::SOURCE_IMPORT_REPLACE) {
+  if (aSource == nsINavBookmarksService::SOURCE_RESTORE ||
+      aSource == nsINavBookmarksService::SOURCE_RESTORE_ON_STARTUP) {
     return nsINavBookmarksService::SYNC_STATUS_UNKNOWN;
   }
   return nsINavBookmarksService::SYNC_STATUS_NEW;
 }
 
 // Indicates whether an item has been uploaded to the server and
 // needs a tombstone on deletion.
 inline bool
--- a/toolkit/components/places/tests/bookmarks/test_1129529.js
+++ b/toolkit/components/places/tests/bookmarks/test_1129529.js
@@ -54,17 +54,17 @@ add_task(async function() {
           uri: "http://test2.com/"
         }
       ]
     }]
   };
 
   let contentType = "application/json";
   let uri = "data:" + contentType + "," + JSON.stringify(aData);
-  await BookmarkJSONUtils.importFromURL(uri, false);
+  await BookmarkJSONUtils.importFromURL(uri);
 
   let [bookmarks] = await PlacesBackups.getBookmarksTree();
   let unsortedBookmarks = bookmarks.children[2].children;
   Assert.equal(unsortedBookmarks.length, 3);
 
   for (let i = 0; i < unsortedBookmarks.length; ++i) {
     let bookmark = unsortedBookmarks[i];
     Assert.equal(bookmark.charset, "UTF-8");
--- a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
+++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
@@ -274,17 +274,17 @@ add_task(async function() {
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // clean
   for (let singleTest of tests) {
     singleTest.clean();
   }
 
   // restore json file
-  await BookmarkJSONUtils.importFromFile(jsonFile, true);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
 
   // 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
@@ -113,30 +113,30 @@ add_task(async function setup() {
 add_task(async function test_export_import_excluded_file() {
   // populate db
   test.populate();
 
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // restore json file
   info("Restoring json file");
-  await BookmarkJSONUtils.importFromFile(jsonFile, true);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
 
   // validate without removing all bookmarks
   // restore do not remove backup exclude entries
   info("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);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
 
   // validate after a complete bookmarks cleanup
   test.validate(true);
 
   // clean up
   await OS.File.remove(jsonFile);
 });
--- a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -135,17 +135,17 @@ add_task(async function() {
 
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   tests.forEach(function(aTest) {
     aTest.inbetween();
   });
 
   // restore json file
-  await BookmarkJSONUtils.importFromFile(jsonFile, true);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
 
   // validate
   tests.forEach(function(aTest) {
     aTest.validate();
   });
 
   // clean up
   await OS.File.remove(jsonFile);
--- a/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
+++ b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
@@ -69,17 +69,17 @@ add_task(async function() {
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // clean
   tests.forEach(function(aTest) {
     aTest.clean();
   });
 
   // restore json file
-  await BookmarkJSONUtils.importFromFile(jsonFile, true);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
 
   // validate
   tests.forEach(function(aTest) {
     aTest.validate();
   });
 
   // clean up
   await OS.File.remove(jsonFile);
--- a/toolkit/components/places/tests/bookmarks/test_448584.js
+++ b/toolkit/components/places/tests/bookmarks/test_448584.js
@@ -73,17 +73,17 @@ add_task(async function() {
 
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // clean
   await PlacesUtils.bookmarks.remove(badBookmark);
 
   // restore json file
   try {
-    await BookmarkJSONUtils.importFromFile(jsonFile, true);
+    await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
   } catch (ex) { do_throw("couldn't import the exported file: " + ex); }
 
   // validate
   validateResults(1);
 
   // clean up
   await OS.File.remove(jsonFile);
 });
--- a/toolkit/components/places/tests/bookmarks/test_458683.js
+++ b/toolkit/components/places/tests/bookmarks/test_458683.js
@@ -88,15 +88,15 @@ add_task(async function() {
 
   await BookmarkJSONUtils.exportToFile(jsonFile);
 
   // clean
   PlacesUtils.tagging.untagURI(Services.io.newURI(ITEM_URL), [TAG_NAME]);
   await PlacesUtils.bookmarks.remove(item);
 
   // restore json file
-  await BookmarkJSONUtils.importFromFile(jsonFile, true);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
 
   validateResults();
 
   // clean up
   await OS.File.remove(jsonFile);
 });
--- a/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js
+++ b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js
@@ -36,17 +36,17 @@ add_task(async function compress_bookmar
     title: "bookmark",
     url,
   });
 
   // Force create a compressed backup, Remove the bookmark, the restore the backup
   await PlacesBackups.create(undefined, true);
   let recentBackup = await PlacesBackups.getMostRecentBackup();
   await PlacesUtils.bookmarks.remove(bm);
-  await BookmarkJSONUtils.importFromFile(recentBackup, true);
+  await BookmarkJSONUtils.importFromFile(recentBackup, { replace: true });
   let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
   let node = root.getChild(0);
   Assert.equal(node.uri, url);
 
   root.containerOpen = false;
   await PlacesUtils.bookmarks.eraseEverything();
 
   // Cleanup.
--- a/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js
+++ b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js
@@ -31,17 +31,18 @@ add_task(async function() {
   folder2.parentGuid = folder1.guid;
   await PlacesUtils.bookmarks.update(folder2);
 
   // Create a backup.
   await PlacesBackups.create();
 
   // Remove the bookmarks, then restore the backup.
   await PlacesUtils.bookmarks.remove(folder1);
-  await BookmarkJSONUtils.importFromFile((await PlacesBackups.getMostRecentBackup()), true);
+  await BookmarkJSONUtils.importFromFile((await PlacesBackups.getMostRecentBackup()),
+                                         { replace: true });
 
   info("Checking first level");
   let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
   let level1 = root.getChild(0);
   Assert.equal(level1.title, "f1");
   info("Checking second level");
   PlacesUtils.asContainer(level1).containerOpen = true;
   let level2 = level1.getChild(0);
--- a/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js
+++ b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js
@@ -16,17 +16,17 @@ add_task(async function() {
   let file = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.997030.html");
   if ((await OS.File.exists(file))) {
     await OS.File.remove(file);
   }
   await BookmarkHTMLUtils.exportToFile(file);
 
   // Remove the bookmarks, then restore the backup.
   await PlacesUtils.bookmarks.remove(bm);
-  await BookmarkHTMLUtils.importFromFile(file, true);
+  await BookmarkHTMLUtils.importFromFile(file, { replace: true });
 
   info("Checking first level");
   let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
   let node = root.getChild(0);
   Assert.equal(node.uri, url);
 
   root.containerOpen = false;
   await PlacesUtils.bookmarks.eraseEverything();
--- a/toolkit/components/places/tests/sync/test_bookmark_deletion.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_deletion.js
@@ -600,17 +600,17 @@ add_task(async function test_nonexistent
   info("Create local tombstone for nonexistent remote item A");
   await PlacesUtils.bookmarks.insert({
     guid: "bookmarkAAAA",
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     title: "A",
     url: "http://example.com/a",
     // Pretend a bookmark restore added A, so that we'll write a tombstone when
     // we remove it.
-    source: PlacesUtils.bookmarks.SOURCES.IMPORT_REPLACE,
+    source: PlacesUtils.bookmarks.SOURCES.RESTORE,
   });
   await PlacesUtils.bookmarks.remove("bookmarkAAAA");
 
   // B doesn't exist in Places, and we don't currently persist tombstones (bug
   // 1343103), so we should ignore it.
   info("Create remote tombstone for nonexistent local item B");
   await buf.store([{
     id: "bookmarkBBBB",
--- a/toolkit/components/places/tests/sync/test_sync_utils.js
+++ b/toolkit/components/places/tests/sync/test_sync_utils.js
@@ -2020,17 +2020,17 @@ add_task(async function test_pullChanges
       unsyncedBmk.guid);
     ok(fields.every(field =>
       field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
     ), "Unsynced bookmark statuses should match");
   }
 
   info("Import new bookmarks from HTML");
   let { path } = do_get_file("./sync_utils_bookmarks.html");
-  await BookmarkHTMLUtils.importFromFile(path, false);
+  await BookmarkHTMLUtils.importFromFile(path);
 
   // Bookmarks.html doesn't store IDs, so we need to look these up.
   let mozBmk = await PlacesUtils.bookmarks.fetch({
     url: "https://www.mozilla.org/",
   });
   let fxBmk = await PlacesUtils.bookmarks.fetch({
     url: "https://www.mozilla.org/en-US/firefox/",
   });
@@ -2082,17 +2082,17 @@ add_task(async function test_pullChanges
   });
   await PlacesTestUtils.setBookmarkSyncFields({
     guid: syncedFolder.guid,
     syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
   });
 
   info("Import new bookmarks from JSON");
   let { path } = do_get_file("./sync_utils_bookmarks.json");
-  await BookmarkJSONUtils.importFromFile(path, false);
+  await BookmarkJSONUtils.importFromFile(path);
   {
     let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
       syncedFolder.guid, "NnvGl3CRA4hC", "APzP8MupzA8l");
     deepEqual(fields.map(field => field.syncStatus), [
       PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
       PlacesUtils.bookmarks.SYNC_STATUS.NEW,
       PlacesUtils.bookmarks.SYNC_STATUS.NEW,
     ], "Sync statuses should match for JSON imports");
@@ -2149,17 +2149,17 @@ add_task(async function test_pullChanges
     deepEqual(fields.map(field => field.syncStatus), [
       PlacesUtils.bookmarks.SYNC_STATUS.NEW,
       PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
     ], "Sync statuses should match before restoring from JSON");
   }
 
   info("Restore from JSON, replacing existing items");
   let { path } = do_get_file("./sync_utils_bookmarks.json");
-  await BookmarkJSONUtils.importFromFile(path, true);
+  await BookmarkJSONUtils.importFromFile(path, { replace: true });
   {
     let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
       "NnvGl3CRA4hC", "APzP8MupzA8l");
     ok(fields.every(field =>
       field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN
     ), "All bookmarks should be UNKNOWN after restoring from JSON");
   }
 
--- a/toolkit/components/places/tests/unit/test_384370.js
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -26,17 +26,17 @@ add_task(async function() {
   // Remove eventual bookmarks.exported.json.
   let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.json");
   if ((await OS.File.exists(jsonFile)))
     await OS.File.remove(jsonFile);
 
   // Test importing a pre-Places canonical bookmarks file.
   // Note: we do not empty the db before this import to catch bugs like 380999
   let htmlFile = OS.Path.join(do_get_cwd().path, "bookmarks.preplaces.html");
-  await BookmarkHTMLUtils.importFromFile(htmlFile, true);
+  await BookmarkHTMLUtils.importFromFile(htmlFile, { replace: true });
 
   // Populate the database.
   for (let { uri, tags } of tagData) {
     PlacesUtils.tagging.tagURI(uri, tags);
   }
   for (let { uri, title } of bookmarkData) {
     await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                          url: uri,
@@ -52,17 +52,17 @@ add_task(async function() {
 
   // Test exporting a Places canonical json file.
   // 1. export to bookmarks.exported.json
   await BookmarkJSONUtils.exportToFile(jsonFile);
   info("exported json");
 
   // 2. empty bookmarks db
   // 3. import bookmarks.exported.json
-  await BookmarkJSONUtils.importFromFile(jsonFile, true);
+  await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
   info("imported json");
 
   // 4. run the test-suite
   await validate("re-imported json");
   info("validated import");
 });
 
 async function validate(infoMsg) {
--- a/toolkit/components/places/tests/unit/test_bookmarks_html.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js
@@ -89,30 +89,30 @@ add_task(async function setup() {
     await OS.File.remove(gBookmarksFileNew);
   }
 
   // This test must be the first one, since it setups the new bookmarks.html.
   // Test importing a pre-Places canonical bookmarks file.
   // 1. import bookmarks.preplaces.html
   // 2. run the test-suite
   // Note: we do not empty the db before this import to catch bugs like 380999
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await testImportedBookmarks();
 
   await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
   await PlacesTestUtils.promiseAsyncUpdates();
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
 add_task(async function test_import_new() {
   // Test importing a Places bookmarks.html file.
   // 1. import bookmarks.exported.html
   // 2. run the test-suite
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
 
   await testImportedBookmarks();
   await PlacesTestUtils.promiseAsyncUpdates();
 
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
@@ -123,31 +123,31 @@ add_task(async function test_emptytitle_
   // 3. export to bookmarks.exported.html
   // 4. empty bookmarks db
   // 5. import bookmarks.exported.html
   // 6. run the test-suite
   // 7. remove the empty-titled bookmark
   // 8. export to bookmarks.exported.html
   // 9. empty bookmarks db and continue
 
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
 
   const NOTITLE_URL = "http://notitle.mozilla.org/";
   let bookmark = await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     url: NOTITLE_URL
   });
   test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
 
   await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
   await PlacesTestUtils.promiseAsyncUpdates();
   await PlacesUtils.bookmarks.eraseEverything();
 
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await testImportedBookmarks();
 
   // Cleanup.
   test_bookmarks.unfiled.pop();
   // HTML imports don't restore GUIDs yet.
   let reimportedBookmark = await PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
@@ -173,17 +173,17 @@ add_task(async function test_import_chro
   // 8. export to bookmarks.exported.html
   // 9. empty bookmarks db and continue
 
   const PAGE_URI = NetUtil.newURI("http://example.com/chromefavicon_page");
   const CHROME_FAVICON_URI = NetUtil.newURI("chrome://global/skin/icons/info.svg");
   const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png");
 
   info("Importing from html");
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
 
   info("Insert bookmark");
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     url: PAGE_URI,
     title: "Test"
   });
@@ -217,17 +217,17 @@ add_task(async function test_import_chro
     PlacesUtils.favicons.setAndFetchFaviconForPage(
       PAGE_URI, CHROME_FAVICON_URI_2, true,
       PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
       resolve, Services.scriptSecurityManager.getSystemPrincipal());
   });
 
   info("import from html");
   await PlacesUtils.bookmarks.eraseEverything();
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
 
   info("Test imported bookmarks");
   await testImportedBookmarks();
 
   // Cleanup.
   test_bookmarks.unfiled.pop();
   // HTML imports don't restore GUIDs yet.
@@ -246,22 +246,22 @@ add_task(async function test_import_onto
   // Test importing the exported bookmarks.html file *on top of* the existing
   // bookmarks.
   // 1. empty bookmarks db
   // 2. import the exported bookmarks file
   // 3. export to file
   // 3. import the exported bookmarks file
   // 4. run the test-suite
 
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
   await PlacesTestUtils.promiseAsyncUpdates();
 
-  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await testImportedBookmarks();
   await PlacesTestUtils.promiseAsyncUpdates();
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
 async function testImportedBookmarks() {
   for (let group in test_bookmarks) {
--- a/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
@@ -10,17 +10,17 @@ const TEST_FAVICON_PAGE_URL = "http://en
 const TEST_FAVICON_DATA_SIZE = 580;
 
 add_task(async function test_corrupt_file() {
   // avoid creating the places smart folder during tests
   Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
 
   // Import bookmarks from the corrupt file.
   let corruptHtml = OS.Path.join(do_get_cwd().path, "bookmarks.corrupt.html");
-  await BookmarkHTMLUtils.importFromFile(corruptHtml, true);
+  await BookmarkHTMLUtils.importFromFile(corruptHtml, { replace: true });
 
   // Check that bookmarks that are not corrupt have been imported.
   await PlacesTestUtils.promiseAsyncUpdates();
   await database_check();
 });
 
 add_task(async function test_corrupt_database() {
   // Create corruption in the database, then export.
@@ -34,17 +34,17 @@ add_task(async function test_corrupt_dat
 
   let bookmarksFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.html");
   if ((await OS.File.exists(bookmarksFile)))
     await OS.File.remove(bookmarksFile);
   await BookmarkHTMLUtils.exportToFile(bookmarksFile);
 
   // Import again and check for correctness.
   await PlacesUtils.bookmarks.eraseEverything();
-  await BookmarkHTMLUtils.importFromFile(bookmarksFile, true);
+  await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await database_check();
 });
 
 /*
  * Check for imported bookmarks correctness
  *
  * @return {Promise}
--- a/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
@@ -39,17 +39,17 @@ add_task(async function test_import_tags
   await BookmarkHTMLUtils.exportToFile(HTMLFile);
 
   // Deletes bookmarks and tags from the database.
   for (let bookmark of bookmarkList) {
     await PlacesUtils.bookmarks.remove(bookmark.guid);
   }
 
   // Re-imports the bookmarks from the HTML file.
-  await BookmarkHTMLUtils.importFromFile(HTMLFile, true);
+  await BookmarkHTMLUtils.importFromFile(HTMLFile, { replace: true });
 
   // Tests to ensure that the tags are still present for each bookmark URI.
   for (let { uri, tags } of bookmarkData) {
     info("Test tags for " + uri.spec + ": " + tags + "\n");
     let foundTags = PlacesUtils.tagging.getTagsForURI(uri);
     Assert.equal(foundTags.length, tags.length);
     Assert.ok(tags.every(tag => foundTags.includes(tag)));
   }
--- a/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
@@ -6,17 +6,17 @@
 
 // Test for bug #801450
 
 // Get Services
 ChromeUtils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
 
 add_task(async function test_bookmarks_html_singleframe() {
   let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks_html_singleframe.html");
-  await BookmarkHTMLUtils.importFromFile(bookmarksFile, true);
+  await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true });
 
   let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
   Assert.equal(root.childCount, 1);
   let folder = root.getChild(0);
   PlacesUtils.asContainer(folder).containerOpen = true;
   Assert.equal(folder.title, "Subtitle");
   Assert.equal(folder.childCount, 1);
   let bookmark = folder.getChild(0);
--- a/toolkit/components/places/tests/unit/test_bookmarks_json.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -95,42 +95,42 @@ var test_bookmarks = {
 };
 
 // Exported bookmarks file pointer.
 var bookmarksExportedFile;
 
 add_task(async function test_import_bookmarks() {
   let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks.json");
 
-  await BookmarkJSONUtils.importFromFile(bookmarksFile, true);
+  await BookmarkJSONUtils.importFromFile(bookmarksFile, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await testImportedBookmarks();
 });
 
 add_task(async function test_export_bookmarks() {
   bookmarksExportedFile = OS.Path.join(OS.Constants.Path.profileDir,
                                        "bookmarks.exported.json");
   await BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
   await PlacesTestUtils.promiseAsyncUpdates();
 });
 
 add_task(async function test_import_exported_bookmarks() {
   await PlacesUtils.bookmarks.eraseEverything();
-  await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+  await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await testImportedBookmarks();
 });
 
 add_task(async function test_import_ontop() {
   await PlacesUtils.bookmarks.eraseEverything();
-  await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+  await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
   await PlacesTestUtils.promiseAsyncUpdates();
-  await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+  await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
   await testImportedBookmarks();
 });
 
 add_task(async function test_clean() {
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
--- a/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js
@@ -5,17 +5,17 @@
 ChromeUtils.import("resource://gre/modules/BookmarkJSONUtils.jsm");
 
 // Exported bookmarks file pointer.
 var bookmarksExportedFile;
 
 add_task(async function test_import_bookmarks() {
   let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks_corrupt.json");
 
-  await BookmarkJSONUtils.importFromFile(bookmarksFile, true);
+  await BookmarkJSONUtils.importFromFile(bookmarksFile, { replace: true });
   await PlacesTestUtils.promiseAsyncUpdates();
 
   let bookmarks = await PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.menuGuid);
 
   Assert.equal(bookmarks.children.length, 1, "should only be one bookmark");
   let bookmark = bookmarks.children[0];
   Assert.equal(bookmark.guid, "OCyeUO5uu9FH", "should have correct guid");
   Assert.equal(bookmark.title, "Customize Firefox", "should have correct title");
--- a/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
@@ -148,17 +148,17 @@ add_task(async function test_json_restor
 
   info("JSON restore: normal restore should succeed");
   let file = await promiseFile("bookmarks-test_restoreNotification.json");
   await addBookmarks();
 
   await BookmarkJSONUtils.exportToFile(file);
   await PlacesUtils.bookmarks.eraseEverything();
   try {
-    await BookmarkJSONUtils.importFromFile(file, true);
+    await BookmarkJSONUtils.importFromFile(file, { replace: true });
   } catch (e) {
     do_throw("  Restore should not have failed" + e);
   }
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
 
@@ -167,17 +167,17 @@ add_task(async function test_json_restor
     data:       NSIOBSERVER_DATA_JSON,
     folderId:   null
   };
   let expectPromises = registerObservers(true);
 
   info("JSON restore: empty file should succeed");
   let file = await promiseFile("bookmarks-test_restoreNotification.json");
   try {
-    await BookmarkJSONUtils.importFromFile(file, true);
+    await BookmarkJSONUtils.importFromFile(file, { replace: true });
   } catch (e) {
     do_throw("  Restore should not have failed" + e);
   }
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
 
@@ -186,17 +186,17 @@ add_task(async function test_json_restor
     data:       NSIOBSERVER_DATA_JSON,
     folderId:   null
   };
   let expectPromises = registerObservers(false);
 
   info("JSON restore: nonexistent file should fail");
   let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
   file.append("this file doesn't exist because nobody created it 1");
-  Assert.rejects(BookmarkJSONUtils.importFromFile(file.path, true),
+  Assert.rejects(BookmarkJSONUtils.importFromFile(file.path, { replace: true }),
     /Cannot restore from nonexisting json file/, "Restore should reject for a non-existent file.");
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
 
 add_task(async function test_html_restore_normal() {
   let expectedData = {
@@ -206,17 +206,17 @@ add_task(async function test_html_restor
   let expectPromises = registerObservers(true);
 
   info("HTML restore: normal restore should succeed");
   let file = await promiseFile("bookmarks-test_restoreNotification.html");
   await addBookmarks();
   await BookmarkHTMLUtils.exportToFile(file);
   await PlacesUtils.bookmarks.eraseEverything();
   try {
-    BookmarkHTMLUtils.importFromFile(file, false)
+    BookmarkHTMLUtils.importFromFile(file)
                      .catch(do_report_unexpected_exception);
   } catch (e) {
     do_throw("  Restore should not have failed");
   }
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
@@ -226,17 +226,17 @@ add_task(async function test_html_restor
     data:       NSIOBSERVER_DATA_HTML,
     folderId:   null
   };
   let expectPromises = registerObservers(true);
 
   info("HTML restore: empty file should succeed");
   let file = await promiseFile("bookmarks-test_restoreNotification.init.html");
   try {
-    BookmarkHTMLUtils.importFromFile(file, false)
+    BookmarkHTMLUtils.importFromFile(file)
                      .catch(do_report_unexpected_exception);
   } catch (e) {
     do_throw("  Restore should not have failed");
   }
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
@@ -246,17 +246,17 @@ add_task(async function test_html_restor
     data:       NSIOBSERVER_DATA_HTML,
     folderId:   null
   };
   let expectPromises = registerObservers(false);
 
   info("HTML restore: nonexistent file should fail");
   let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
   file.append("this file doesn't exist because nobody created it 2");
-  Assert.rejects(BookmarkHTMLUtils.importFromFile(file.path, false),
+  Assert.rejects(BookmarkHTMLUtils.importFromFile(file.path),
     /Cannot import from nonexisting html file/, "Restore should reject for a non-existent file.");
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
 
 add_task(async function test_html_init_restore_normal() {
   let expectedData = {
@@ -266,17 +266,17 @@ add_task(async function test_html_init_r
   let expectPromises = registerObservers(true);
 
   info("HTML initial restore: normal restore should succeed");
   let file = await promiseFile("bookmarks-test_restoreNotification.init.html");
   await addBookmarks();
   await BookmarkHTMLUtils.exportToFile(file);
   await PlacesUtils.bookmarks.eraseEverything();
   try {
-    BookmarkHTMLUtils.importFromFile(file, true)
+    BookmarkHTMLUtils.importFromFile(file, { replace: true })
                      .catch(do_report_unexpected_exception);
   } catch (e) {
     do_throw("  Restore should not have failed");
   }
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
@@ -286,17 +286,17 @@ add_task(async function test_html_init_r
     data:       NSIOBSERVER_DATA_HTML_INIT,
     folderId:   null
   };
   let expectPromises = registerObservers(true);
 
   info("HTML initial restore: empty file should succeed");
   let file = await promiseFile("bookmarks-test_restoreNotification.init.html");
   try {
-    BookmarkHTMLUtils.importFromFile(file, true)
+    BookmarkHTMLUtils.importFromFile(file, { replace: true })
                      .catch(do_report_unexpected_exception);
   } catch (e) {
     do_throw("  Restore should not have failed");
   }
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
@@ -306,14 +306,14 @@ add_task(async function test_html_init_r
     data:       NSIOBSERVER_DATA_HTML_INIT,
     folderId:   null
   };
   let expectPromises = registerObservers(false);
 
   info("HTML initial restore: nonexistent file should fail");
   let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
   file.append("this file doesn't exist because nobody created it 3");
-  Assert.rejects(BookmarkHTMLUtils.importFromFile(file.path, true),
+  Assert.rejects(BookmarkHTMLUtils.importFromFile(file.path, { replace: true }),
     /Cannot import from nonexisting html file/, "Restore should reject for a non-existent file.");
 
   await checkObservers(expectPromises, expectedData);
   await teardown(file);
 });
--- a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -1,14 +1,14 @@
 async function importFromFixture(fixture, replace) {
   let cwd = await OS.File.getCurrentDirectory();
   let path = OS.Path.join(cwd, fixture);
 
   info(`Importing from ${path}`);
-  await BookmarkJSONUtils.importFromFile(path, replace);
+  await BookmarkJSONUtils.importFromFile(path, { replace });
   await PlacesTestUtils.promiseAsyncUpdates();
 }
 
 async function treeEquals(guid, expected, message) {
   let root = await PlacesUtils.promiseBookmarksTree(guid);
   let bookmarks = (function nodeToEntry(node) {
     let entry = { guid: node.guid, index: node.index };
     if (node.children) {