Bug 1477671 - Replace synced livemarks with tombstones, r=mak,markh
authorLina Cambridge <lina@yakshaving.ninja>
Sat, 06 Oct 2018 10:36:15 +0100
changeset 496385 224c0fc59fc12565320b507bfdb7b8f80a07d12d
parent 496384 6ea457f6ca9d48bf51ffd0ccec57305d2e191882
child 496386 5aee88f016b05ac1a2239f46db3207af42f45199
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, markh
bugs1477671
milestone64.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 1477671 - Replace synced livemarks with tombstones, r=mak,markh This commit replaces incoming synced livemarks with tombstones, and reuploads the tombstone and updated parent to the server. Existing livemarks are left untouched to minimize data loss; we'll either delete them during migration, or when another client runs a full sync. Differential Revision: https://phabricator.services.mozilla.com/D7084
services/sync/modules/engines/bookmarks.js
services/sync/tests/tps/test_bug562515.js
services/sync/tests/tps/test_sync.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_bookmark_livemarks.js
services/sync/tests/unit/test_bookmark_tracker.js
services/sync/tests/unit/xpcshell.ini
services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
toolkit/components/places/PlacesSyncUtils.jsm
toolkit/components/places/SyncedBookmarksMirror.jsm
toolkit/components/places/tests/sync/livemark.xml
toolkit/components/places/tests/sync/test_bookmark_kinds.js
toolkit/components/places/tests/sync/test_sync_utils.js
toolkit/components/places/tests/sync/xpcshell.ini
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -1071,16 +1071,29 @@ BookmarksStore.prototype = {
     // Figure out the local id of the parent GUID if available
     let parentGUID = record.parentid;
     if (!parentGUID) {
       throw new Error(
           `Record ${record.id} has invalid parentid: ${parentGUID}`);
     }
     this._log.debug("Remote parent is " + parentGUID);
 
+    if (record.type == "livemark") {
+      // Places no longer supports livemarks, so we replace new and updated
+      // livemarks with tombstones, and insert new change records for the engine
+      // to upload.
+      let livemarkInfo = record.toSyncBookmark();
+      let newChanges = await PlacesSyncUtils.bookmarks.removeLivemark(
+        livemarkInfo);
+      if (newChanges) {
+        this.engine._modified.insert(newChanges);
+        return;
+      }
+    }
+
     // Do the normal processing of incoming records
     await Store.prototype.applyIncoming.call(this, record);
 
     if (record.type == "folder" && record.children) {
       this._childrenToOrder[record.id] = record.children;
     }
   },
 
--- a/services/sync/tests/tps/test_bug562515.js
+++ b/services/sync/tests/tps/test_bug562515.js
@@ -34,20 +34,16 @@ var bookmarks_initial = {
   ],
   "menu/foldera": [
     { uri: "http://www.yahoo.com",
       title: "testing Yahoo",
     },
     { uri: "http://www.cnn.com",
       description: "This is a description of the site a at www.cnn.com",
     },
-    { livemark: "Livemark1",
-      feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
-      siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html",
-    },
   ],
   "menu/folderb": [
     { uri: "http://www.apple.com",
       tags: [ "apple", "mac" ],
     },
   ],
   "toolbar": [
     { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
--- a/services/sync/tests/tps/test_sync.js
+++ b/services/sync/tests/tps/test_sync.js
@@ -47,23 +47,16 @@ var bookmarks_initial = {
     },
     { uri: "http://www.cnn.com",
       description: "This is a description of the site a at www.cnn.com",
       changes: {
         uri: "http://money.cnn.com",
         description: "new description",
       },
     },
-    { livemark: "Livemark1",
-      feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
-      siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html",
-      changes: {
-        livemark: "LivemarkOne",
-      },
-    },
   ],
   "menu/folderb": [
     { uri: "http://www.apple.com",
       tags: ["apple", "mac"],
       changes: {
         uri: "http://www.apple.com/iphone/",
         title: "iPhone",
         location: "menu",
@@ -107,20 +100,16 @@ var bookmarks_after_first_modify = {
       },
     },
   ],
   "menu/foldera": [
     { uri: "http://money.cnn.com",
       title: "http://www.cnn.com",
       description: "new description",
     },
-    { livemark: "LivemarkOne",
-      feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
-      siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html",
-    },
   ],
   "menu/folderb": [
     { uri: "http://www.yahoo.com",
       title: "testing Yahoo",
     },
   ],
   "toolbar": [
     { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
@@ -154,20 +143,16 @@ var bookmarks_after_second_modify = {
     { uri: "http://www.mozilla.com" },
     { separator: true },
   ],
   "menu/foldera": [
     { uri: "http://money.cnn.com",
       title: "http://www.cnn.com",
       description: "new description",
     },
-    { livemark: "LivemarkOne",
-      feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
-      siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html",
-    },
     { folder: "Folder B",
       description: "folder description",
     },
   ],
   "menu/foldera/Folder B": [
     { uri: "http://www.yahoo.com",
       title: "testing Yahoo",
     },
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -558,29 +558,20 @@ add_task(async function test_mismatched_
     newR.parentid = PlacesUtils.bookmarks.toolbarGuid;
 
     await store.applyIncoming(oldR);
     _("Applied old. It's a folder.");
     let oldID = await PlacesUtils.promiseItemId(oldR.id);
     _("Old ID: " + oldID);
     let oldInfo = await PlacesUtils.bookmarks.fetch(oldR.id);
     Assert.equal(oldInfo.type, PlacesUtils.bookmarks.TYPE_FOLDER);
-    Assert.ok(!PlacesUtils.annotations
-                          .itemHasAnnotation(oldID, PlacesUtils.LMANNO_FEEDURI));
 
     await store.applyIncoming(newR);
-    let newID = await PlacesUtils.promiseItemId(newR.id);
-    _("New ID: " + newID);
-
-    _("Applied new. It's a livemark.");
-    let newInfo = await PlacesUtils.bookmarks.fetch(newR.id);
-    Assert.equal(newInfo.type, PlacesUtils.bookmarks.TYPE_FOLDER);
-    Assert.ok(PlacesUtils.annotations
-                         .itemHasAnnotation(newID, PlacesUtils.LMANNO_FEEDURI));
-
+    await Assert.rejects(PlacesUtils.promiseItemId(newR.id),
+      /no item found for the given GUID/, "Should not apply Livemark");
   } finally {
     await cleanup(engine, server);
   }
 });
 
 add_task(async function test_bookmark_guidMap_fail() {
   _("Ensure that failures building the GUID map cause early death.");
 
@@ -1277,8 +1268,139 @@ add_task(async function test_mirror_sync
   equal(await buf.getSyncId(), newSyncID,
     "Should update new sync ID in mirror");
 
   strictEqual(await buf.getCollectionHighWaterMark(), 0,
     "Should reset high water mark on sync ID change in Places");
 
   await bufferedEngine.wipeClient();
 });
+
+add_bookmark_test(async function test_livemarks(engine) {
+  _("Ensure we replace new and existing livemarks with tombstones");
+
+  let server = await serverForFoo(engine);
+  await SyncTestingInfrastructure(server);
+
+  let collection = server.user("foo").collection("bookmarks");
+  let now = Date.now();
+
+  try {
+    _("Insert existing livemark");
+    let modifiedForA = now - 5 * 60 * 1000;
+    await PlacesUtils.bookmarks.insert({
+      guid: "livemarkAAAA",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+      parentGuid: PlacesUtils.bookmarks.menuGuid,
+      title: "A",
+      lastModified: new Date(modifiedForA),
+      dateAdded: new Date(modifiedForA),
+      source: PlacesUtils.bookmarks.SOURCE_SYNC,
+    });
+    collection.insert("menu", encryptPayload({
+      id: "menu",
+      type: "folder",
+      parentName: "",
+      title: "menu",
+      children: ["livemarkAAAA"],
+      parentid: "places",
+    }), modifiedForA / 1000);
+    collection.insert("livemarkAAAA", encryptPayload({
+      id: "livemarkAAAA",
+      type: "livemark",
+      feedUri: "http://example.com/a",
+      parentName: "menu",
+      title: "A",
+      parentid: "menu",
+    }), modifiedForA / 1000);
+
+    _("Insert remotely updated livemark");
+    await PlacesUtils.bookmarks.insert({
+      guid: "livemarkBBBB",
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+      title: "B",
+      lastModified: new Date(now),
+      dateAdded: new Date(now),
+    });
+    collection.insert("toolbar", encryptPayload({
+      id: "toolbar",
+      type: "folder",
+      parentName: "",
+      title: "toolbar",
+      children: ["livemarkBBBB"],
+      parentid: "places",
+    }), now / 1000);
+    collection.insert("livemarkBBBB", encryptPayload({
+      id: "livemarkBBBB",
+      type: "livemark",
+      feedUri: "http://example.com/b",
+      parentName: "toolbar",
+      title: "B",
+      parentid: "toolbar",
+    }), now / 1000);
+
+    _("Insert new remote livemark");
+    collection.insert("unfiled", encryptPayload({
+      id: "unfiled",
+      type: "folder",
+      parentName: "",
+      title: "unfiled",
+      children: ["livemarkCCCC"],
+      parentid: "places",
+    }), now / 1000);
+    collection.insert("livemarkCCCC", encryptPayload({
+      id: "livemarkCCCC",
+      type: "livemark",
+      feedUri: "http://example.com/c",
+      parentName: "unfiled",
+      title: "C",
+      parentid: "unfiled",
+    }), now / 1000);
+
+    _("Bump last sync time to ignore A");
+    await engine.setLastSync(Date.now() / 1000 - 60);
+
+    _("Sync");
+    await sync_engine_and_validate_telem(engine, false);
+
+    deepEqual(collection.keys().sort(), ["livemarkAAAA", "livemarkBBBB",
+      "livemarkCCCC", "menu", "mobile", "toolbar", "unfiled"],
+      "Should store original livemark A and tombstones for B and C on server");
+
+    let payloads = collection.payloads();
+
+    deepEqual(payloads.find(payload => payload.id == "menu").children,
+      ["livemarkAAAA"], "Should keep A in menu");
+    ok(!payloads.find(payload => payload.id == "livemarkAAAA").deleted,
+      "Should not upload tombstone for A");
+
+    deepEqual(payloads.find(payload => payload.id == "toolbar").children,
+      [], "Should remove B from toolbar");
+    ok(payloads.find(payload => payload.id == "livemarkBBBB").deleted,
+      "Should upload tombstone for B");
+
+    deepEqual(payloads.find(payload => payload.id == "unfiled").children,
+      [], "Should remove C from unfiled");
+    ok(payloads.find(payload => payload.id == "livemarkCCCC").deleted,
+      "Should replace C with tombstone");
+
+    await assertBookmarksTreeMatches("", [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [{
+        guid: "livemarkAAAA",
+        index: 0,
+      }],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+    }], "Should keep A and remove B locally");
+  } finally {
+    await cleanup(engine, server);
+  }
+});
deleted file mode 100644
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-ChromeUtils.import("resource://gre/modules/Log.jsm");
-ChromeUtils.import("resource://services-sync/record.js");
-ChromeUtils.import("resource://services-sync/engines.js");
-ChromeUtils.import("resource://services-sync/engines/bookmarks.js");
-ChromeUtils.import("resource://services-sync/util.js");
-ChromeUtils.import("resource://services-sync/service.js");
-
-// Record borrowed from Bug 631361.
-const record631361 = {
-  id: "M5bwUKK8hPyF",
-  index: 150,
-  modified: 1296768176.49,
-  payload:
-  {"id": "M5bwUKK8hPyF",
-   "type": "livemark",
-   "siteUri": "http://www.bbc.co.uk/go/rss/int/news/-/news/",
-   "feedUri": "http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
-   "parentName": "Bookmarks Toolbar",
-   "parentid": "toolbar",
-   "title": "Latest Headlines",
-   "description": "",
-   "children":
-     ["7oBdEZB-8BMO", "SUd1wktMNCTB", "eZe4QWzo1BcY", "YNBhGwhVnQsN",
-      "92Aw2SMEkFg0", "uw0uKqrVFwd-", "x7mx2P3--8FJ", "d-jVF8UuC9Ye",
-      "DV1XVtKLEiZ5", "g4mTaTjr837Z", "1Zi5W3lwBw8T", "FEYqlUHtbBWS",
-      "qQd2u7LjosCB", "VUs2djqYfbvn", "KuhYnHocu7eg", "u2gcg9ILRg-3",
-      "hfK_RP-EC7Ol", "Aq5qsa4E5msH", "6pZIbxuJTn-K", "k_fp0iN3yYMR",
-      "59YD3iNOYO8O", "01afpSdAk2iz", "Cq-kjXDEPIoP", "HtNTjt9UwWWg",
-      "IOU8QRSrTR--", "HJ5lSlBx6d1D", "j2dz5R5U6Khc", "5GvEjrNR0yJl",
-      "67ozIBF5pNVP", "r5YB0cUx6C_w", "FtmFDBNxDQ6J", "BTACeZq9eEtw",
-      "ll4ozQ-_VNJe", "HpImsA4_XuW7", "nJvCUQPLSXwA", "94LG-lh6TUYe",
-      "WHn_QoOL94Os", "l-RvjgsZYlej", "LipQ8abcRstN", "74TiLvarE3n_",
-      "8fCiLQpQGK1P", "Z6h4WkbwfQFa", "GgAzhqakoS6g", "qyt92T8vpMsK",
-      "RyOgVCe2EAOE", "bgSEhW3w6kk5", "hWODjHKGD7Ph", "Cky673aqOHbT",
-      "gZCYT7nx3Nwu", "iJzaJxxrM58L", "rUHCRv68aY5L", "6Jc1hNJiVrV9",
-      "lmNgoayZ-ym8", "R1lyXsDzlfOd", "pinrXwDnRk6g", "Sn7TmZV01vMM",
-      "qoXyU6tcS1dd", "TRLanED-QfBK", "xHbhMeX_FYEA", "aPqacdRlAtaW",
-      "E3H04Wn2RfSi", "eaSIMI6kSrcz", "rtkRxFoG5Vqi", "dectkUglV0Dz",
-      "B4vUE0BE15No", "qgQFW5AQrgB0", "SxAXvwOhu8Zi", "0S6cRPOg-5Z2",
-      "zcZZBGeLnaWW", "B0at8hkQqVZQ", "sgPtgGulbP66", "lwtwGHSCPYaQ",
-      "mNTdpgoRZMbW", "-L8Vci6CbkJY", "bVzudKSQERc1", "Gxl9lb4DXsmL",
-      "3Qr13GucOtEh"]},
-  collection: "bookmarks",
-};
-
-function makeLivemark(p, mintGUID) {
-  let b = new Livemark("bookmarks", p.id);
-  // Copy here, because tests mutate the contents.
-  b.cleartext = Cu.cloneInto(p, {});
-
-  if (mintGUID) {
-    b.id = Utils.makeGUID();
-  }
-
-  return b;
-}
-
-add_task(async function test_livemark_invalid() {
-  let engine = new BookmarksEngine(Service);
-  await engine.initialize();
-  let store = engine._store;
-
-  _("Livemarks considered invalid by nsLivemarkService are skipped.");
-
-  _("Parent is unknown. Will be set to unfiled.");
-  let lateParentRec = makeLivemark(record631361.payload, true);
-  lateParentRec.parentid = Utils.makeGUID();
-
-  await store.create(lateParentRec);
-  let recInfo = await PlacesUtils.bookmarks.fetch(lateParentRec.id);
-  Assert.equal(recInfo.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
-
-  _("No feed URI, which is invalid. Will be skipped.");
-  let noFeedURIRec = makeLivemark(record631361.payload, true);
-  delete noFeedURIRec.cleartext.feedUri;
-  await store.create(noFeedURIRec);
-  // No exception, but no creation occurs.
-  let noFeedURIItem = await PlacesUtils.bookmarks.fetch(noFeedURIRec.id);
-  Assert.equal(null, noFeedURIItem);
-
-  _("Parent is a Livemark. Will be skipped.");
-  let lmParentRec = makeLivemark(record631361.payload, true);
-  lmParentRec.parentid = recInfo.guid;
-  await store.create(lmParentRec);
-  // No exception, but no creation occurs.
-  let lmParentItem = await PlacesUtils.bookmarks.fetch(lmParentRec.id);
-  Assert.equal(null, lmParentItem);
-
-  await engine.finalize();
-});
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -853,74 +853,16 @@ add_task(async function test_onFaviconCh
     await verifyTrackedItems([]);
     Assert.equal(tracker.score, 0);
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
 
-add_task(async function test_onLivemarkAdded() {
-  _("New livemarks should be tracked");
-
-  try {
-    await startTracking();
-
-    _("Insert a livemark");
-    let totalSyncChanges = PlacesUtils.bookmarks.totalSyncChanges;
-    let livemark = await PlacesUtils.livemarks.addLivemark({
-      parentGuid: PlacesUtils.bookmarks.menuGuid,
-      // Use a local address just in case, to avoid potential aborts for
-      // non-local connections.
-      feedURI: CommonUtils.makeURI("http://localhost:0"),
-    });
-    // Prevent the livemark refresh timer from requesting the URI.
-    livemark.terminate();
-
-    await verifyTrackedItems(["menu", livemark.guid]);
-    // Two observer notifications: one for creating the livemark folder, and
-    // one for setting the "livemark/feedURI" anno on the folder.
-    Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE * 2);
-    Assert.equal(PlacesUtils.bookmarks.totalSyncChanges, totalSyncChanges + 2);
-  } finally {
-    _("Clean up.");
-    await cleanup();
-  }
-});
-
-add_task(async function test_onLivemarkDeleted() {
-  _("Deleted livemarks should be tracked");
-
-  try {
-    await tracker.stop();
-
-    _("Insert a livemark");
-    let livemark = await PlacesUtils.livemarks.addLivemark({
-      parentGuid: PlacesUtils.bookmarks.menuGuid,
-      feedURI: CommonUtils.makeURI("http://localhost:0"),
-    });
-    livemark.terminate();
-
-    await startTracking();
-
-    _("Remove a livemark");
-    let totalSyncChanges = PlacesUtils.bookmarks.totalSyncChanges;
-    await PlacesUtils.livemarks.removeLivemark({
-      guid: livemark.guid,
-    });
-
-    await verifyTrackedItems(["menu", livemark.guid]);
-    Assert.equal(tracker.score, SCORE_INCREMENT_XLARGE);
-    Assert.equal(PlacesUtils.bookmarks.totalSyncChanges, totalSyncChanges + 2);
-  } finally {
-    _("Clean up.");
-    await cleanup();
-  }
-});
-
 add_task(async function test_async_onItemMoved_moveToFolder() {
   _("Items moved via `moveToFolder` should be tracked");
 
   try {
     await tracker.stop();
 
     await PlacesUtils.bookmarks.insertTree({
       guid: PlacesUtils.bookmarks.menuGuid,
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -131,17 +131,16 @@ tags = addons
 tags = addons
 [test_addons_validator.js]
 tags = addons
 [test_bookmark_batch_fail.js]
 [test_bookmark_duping.js]
 run-sequentially = Frequent timeouts, bug 1395148
 [test_bookmark_engine.js]
 [test_bookmark_invalid.js]
-[test_bookmark_livemarks.js]
 [test_bookmark_order.js]
 [test_bookmark_places_query_rewriting.js]
 [test_bookmark_record.js]
 [test_bookmark_repair.js]
 skip-if = release_or_beta
 run-sequentially = Frequent timeouts, bug 1395148
 [test_bookmark_repair_requestor.js]
 # Repair is enabled only on Aurora and Nightly
--- a/services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
@@ -2,17 +2,17 @@
  * 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/. */
 
  /* This is a JavaScript module (JSM) to be imported via
   * Components.utils.import() and acts as a singleton. Only the following
   * listed symbols will exposed on import, and only when and where imported.
   */
 
-var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator", "Livemark",
+var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator",
                         "BookmarkFolder", "DumpBookmarks"];
 
 ChromeUtils.import("resource://gre/modules/PlacesBackups.jsm");
 ChromeUtils.import("resource://gre/modules/PlacesSyncUtils.jsm");
 ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://tps/logger.jsm");
 
@@ -35,19 +35,16 @@ function PlacesItemProps(props) {
   this.uri = null;
   this.keyword = null;
   this.title = null;
   this.after = null;
   this.before = null;
   this.folder = null;
   this.position = null;
   this.delete = false;
-  this.siteUri = null;
-  this.feedUri = null;
-  this.livemark = null;
   this.tags = null;
   this.last_item_pos = null;
   this.type = null;
 
   for (var prop in props) {
     if (prop in this) {
       this[prop] = props[prop];
     }
@@ -85,17 +82,17 @@ PlacesItem.prototype = {
   _typeMap: new Map([
     [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, PlacesUtils.bookmarks.TYPE_FOLDER],
     [PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, PlacesUtils.bookmarks.TYPE_SEPARATOR],
     [PlacesUtils.TYPE_X_MOZ_PLACE, PlacesUtils.bookmarks.TYPE_BOOKMARK],
   ]),
 
   toString() {
     var that = this;
-    var props = ["uri", "title", "location", "folder", "feedUri", "siteUri", "livemark"];
+    var props = ["uri", "title", "location", "folder"];
     var string = (this.props.type ? this.props.type + " " : "") +
       "(" +
       (function() {
         var ret = [];
         for (var i in props) {
           if (that.props[props[i]]) {
             ret.push(props[i] + ": " + that.props[props[i]]);
           }
@@ -640,144 +637,16 @@ BookmarkFolder.prototype = {
     await this.SetPosition(this.updateProps.position);
     await this.SetTitle(this.updateProps.folder);
   },
 };
 
 extend(BookmarkFolder, PlacesItem);
 
 /**
- * Livemark class constructor. Initialzes instance properties.
- */
-function Livemark(props) {
-  PlacesItem.call(this, props);
-  this.props.type = "livemark";
-}
-
-/**
- * Livemark instance methods
- */
-Livemark.prototype = {
-  /**
-   * Create
-   *
-   * Creates the livemark described by this object's properties.
-   *
-   * @return the id of the created livemark
-   */
-  async Create() {
-    this.props.parentGuid = await this.GetOrCreateFolder(this.props.location);
-    Logger.AssertTrue(this.props.parentGuid, "Unable to create " +
-      "folder, error creating parent folder " + this.props.location);
-    let siteURI = null;
-    if (this.props.siteUri != null)
-      siteURI = Services.io.newURI(this.props.siteUri);
-    let livemarkObj = {parentGuid: this.props.parentGuid,
-                       title: this.props.livemark,
-                       siteURI,
-                       feedURI: Services.io.newURI(this.props.feedUri),
-                       index: PlacesUtils.bookmarks.DEFAULT_INDEX};
-
-    let livemark = await PlacesUtils.livemarks.addLivemark(livemarkObj);
-    this.props.guid = livemark.guid;
-    return this.props.guid;
-  },
-
-  /**
-   * Find
-   *
-   * Locates the livemark which corresponds to this object's
-   * properties.
-   *
-   * @return the item guid if the livemark was found, otherwise null
-   */
-  async Find() {
-    this.props.parentGuid = await this.GetFolder(this.props.location);
-    if (this.props.parentGuid == null) {
-      Logger.logError("Unable to find folder " + this.props.location);
-      return null;
-    }
-    this.props.guid = await this.GetPlacesChildGuid(
-                              this.props.parentGuid,
-                              PlacesUtils.bookmarks.TYPE_FOLDER,
-                              this.props.livemark);
-    if (!this.props.guid) {
-      Logger.logPotentialError("can't find livemark for " + this.toString());
-      return null;
-    }
-    let itemId = await PlacesUtils.promiseItemId(this.props.guid);
-    if (!PlacesUtils.annotations
-                    .itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) {
-      Logger.logPotentialError("livemark folder found, but it's just a regular folder, for " +
-        this.toString());
-      this.props.guid = null;
-      return null;
-    }
-    let feedURI = Services.io.newURI(this.props.feedUri);
-    let lmFeedURISpec =
-      PlacesUtils.annotations.getItemAnnotation(itemId,
-                                                PlacesUtils.LMANNO_FEEDURI);
-    if (feedURI.spec != lmFeedURISpec) {
-      Logger.logPotentialError("livemark feed uri not correct, expected: " +
-        this.props.feedUri + ", actual: " + lmFeedURISpec +
-        " for " + this.toString());
-      return null;
-    }
-    if (this.props.siteUri != null) {
-      let siteURI = Services.io.newURI(this.props.siteUri);
-      let lmSiteURISpec =
-        PlacesUtils.annotations.getItemAnnotation(itemId,
-                                                  PlacesUtils.LMANNO_SITEURI);
-      if (siteURI.spec != lmSiteURISpec) {
-        Logger.logPotentialError("livemark site uri not correct, expected: " +
-        this.props.siteUri + ", actual: " + lmSiteURISpec + " for " +
-        this.toString());
-        return null;
-      }
-    }
-    if (!(await this.CheckPosition(this.props.before,
-                                   this.props.after,
-                                   this.props.last_item_pos)))
-      return null;
-    return this.props.guid;
-  },
-
-  /**
-   * Update
-   *
-   * Updates this livemark's properties according the properties on this
-   * object's 'updateProps' property.
-   *
-   * @return nothing
-   */
-  async Update() {
-    Logger.AssertTrue(this.props.guid, "Invalid guid during Update");
-    await this.SetLocation(this.updateProps.location);
-    await this.SetPosition(this.updateProps.position);
-    await this.SetTitle(this.updateProps.livemark);
-    return true;
-  },
-
-  /**
-   * Remove
-   *
-   * Removes this livemark.  The livemark should have been located previously
-   * by a call to Find.
-   *
-   * @return nothing
-   */
-  async Remove() {
-    Logger.AssertTrue(this.props.guid, "Invalid guid during Remove");
-    await PlacesUtils.bookmarks.remove(this.props.guid);
-  },
-};
-
-extend(Livemark, PlacesItem);
-
-/**
  * Separator class constructor. Initializes instance properties.
  */
 function Separator(props) {
   PlacesItem.call(this, props);
   this.props.type = "separator";
 }
 
 /**
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -1166,60 +1166,52 @@ const BookmarkSyncUtils = PlacesSyncUtil
    *
    *  - kind (all): A string representing the item's kind.
    *  - recordId (all): The item's record ID.
    *  - parentRecordId (all): The record ID of the item's parent.
    *  - parentTitle (all): The title of the item's parent, used for de-duping.
    *    Omitted for the Places root and parents with empty titles.
    *  - dateAdded (all): Timestamp in milliseconds, when the bookmark was added
    *    or created on a remote device if known.
-   *  - title ("bookmark", "folder", "livemark", "query"): The item's title.
+   *  - title ("bookmark", "folder", "query"): The item's title.
    *    Omitted if empty.
    *  - url ("bookmark", "query"): The item's URL.
    *  - tags ("bookmark", "query"): An array containing the item's tags.
    *  - keyword ("bookmark"): The bookmark's keyword, if one exists.
-   *  - feed ("livemark"): A `URL` object pointing to the livemark's feed URL.
-   *  - site ("livemark"): A `URL` object pointing to the livemark's site URL,
-   *    or `null` if one isn't set.
    *  - childRecordIds ("folder"): An array containing the record IDs of the item's
    *    children, used to determine child order.
    *  - folder ("query"): The tag folder name, if this is a tag query.
    *  - index ("separator"): The separator's position within its parent.
    */
   async fetch(recordId) {
     let guid = BookmarkSyncUtils.recordIdToGuid(recordId);
     let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
     if (!bookmarkItem) {
       return null;
     }
     return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: fetch",
       async function(db) {
         // Convert the Places bookmark object to a Sync bookmark and add
         // kind-specific properties. Titles are required for bookmarks,
-        // folders, and livemarks; optional for queries, and omitted for
-        // separators.
+        // and folders; optional for queries, and omitted for separators.
         let kind = await getKindForItem(db, bookmarkItem);
         let item;
         switch (kind) {
           case BookmarkSyncUtils.KINDS.BOOKMARK:
             item = await fetchBookmarkItem(db, bookmarkItem);
             break;
 
           case BookmarkSyncUtils.KINDS.QUERY:
             item = await fetchQueryItem(db, bookmarkItem);
             break;
 
           case BookmarkSyncUtils.KINDS.FOLDER:
             item = await fetchFolderItem(db, bookmarkItem);
             break;
 
-          case BookmarkSyncUtils.KINDS.LIVEMARK:
-            item = await fetchLivemarkItem(db, bookmarkItem);
-            break;
-
           case BookmarkSyncUtils.KINDS.SEPARATOR:
             item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
             item.index = bookmarkItem.index;
             break;
 
           default:
             throw new Error(`Unknown bookmark kind: ${kind}`);
         }
@@ -1287,16 +1279,62 @@ const BookmarkSyncUtils = PlacesSyncUtil
         SET syncChangeCounter = syncChangeCounter + :syncChangeDelta
       WHERE type = :type AND
             fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND
                   url = :url)`,
       { syncChangeDelta, type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
         url: url.href });
   },
 
+  async removeLivemark(livemarkInfo) {
+    let info = validateSyncBookmarkObject(
+      "BookmarkSyncUtils: removeLivemark",
+      livemarkInfo,
+      { kind: { required: true,
+                validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK },
+        recordId: { required: true },
+        parentRecordId: { required: true } });
+
+    let guid = BookmarkSyncUtils.recordIdToGuid(info.recordId);
+    let parentGuid = BookmarkSyncUtils.recordIdToGuid(info.parentRecordId);
+
+    return PlacesUtils.withConnectionWrapper(
+      "BookmarkSyncUtils: removeLivemark",
+      async function(db) {
+        if (await GUIDMissing(guid)) {
+          // If the livemark doesn't exist in the database, insert a tombstone
+          // and bump its parent's change counter to ensure it's removed from
+          // the server in the current sync.
+          await db.executeTransaction(async function() {
+            await db.executeCached(`
+              UPDATE moz_bookmarks SET
+                syncChangeCounter = syncChangeCounter + 1
+              WHERE guid = :parentGuid`,
+              { parentGuid });
+
+            await db.executeCached(`
+              INSERT OR IGNORE INTO moz_bookmarks_deleted (guid, dateRemoved)
+              VALUES (:guid, ${PlacesUtils.toPRTime(Date.now())})`,
+              { guid });
+          });
+        } else {
+          await PlacesUtils.bookmarks.remove({
+            guid,
+            // `SYNC_REPARENT_REMOVED_FOLDER_CHILDREN` bumps the change counter for
+            // the child and its new parent, without incrementing the bookmark
+            // tracker's score.
+            source: PlacesUtils.bookmarks.SOURCES.SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
+          });
+        }
+
+        return pullSyncChanges(db, [guid, parentGuid]);
+      }
+    );
+  },
+
   /**
    * Returns `0` if no sensible timestamp could be found.
    * Otherwise, returns the earliest sensible timestamp between `existingMillis`
    * and `serverMillis`.
    */
   ratchetTimestampBackwards(existingMillis, serverMillis, lowerBound = BookmarkSyncUtils.EARLIEST_BOOKMARK_TIMESTAMP) {
     const possible = [+existingMillis, +serverMillis].filter(n => !isNaN(n) && n > lowerBound);
     if (!possible.length) {
@@ -1467,63 +1505,31 @@ async function insertSyncBookmark(db, in
       insertInfo.parentRecordId} doesn't exist; reparenting to unfiled`);
     insertInfo.parentRecordId = "unfiled";
   }
 
   // If we're inserting a tag query, make sure the tag exists and fix the
   // folder ID to refer to the local tag folder.
   insertInfo = await updateTagQueryFolder(db, insertInfo);
 
-  let newItem;
-  if (insertInfo.kind == BookmarkSyncUtils.KINDS.LIVEMARK) {
-    newItem = await insertSyncLivemark(db, insertInfo);
-  } else {
-    let bookmarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
-    let bookmarkItem = await PlacesUtils.bookmarks.insert(bookmarkInfo);
-    newItem = await insertBookmarkMetadata(db, bookmarkItem, insertInfo);
-  }
-
-  if (!newItem) {
-    return null;
-  }
+  let bookmarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
+  let bookmarkItem = await PlacesUtils.bookmarks.insert(bookmarkInfo);
+  let newItem = await insertBookmarkMetadata(db, bookmarkItem, insertInfo);
 
   // If the item is an orphan, annotate it with its real parent record ID.
   if (isOrphan) {
     await annotateOrphan(newItem, requestedParentRecordId);
   }
 
   // Reparent all orphans that expect this folder as the parent.
   await reparentOrphans(db, newItem);
 
   return newItem;
 }
 
-// Inserts a synced livemark.
-async function insertSyncLivemark(db, insertInfo) {
-  if (!insertInfo.feed) {
-    BookmarkSyncLog.debug(`insertSyncLivemark: ${
-      insertInfo.recordId} missing feed URL`);
-    return null;
-  }
-  let livemarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
-  let parentIsLivemark = await getAnno(db, livemarkInfo.parentGuid,
-                                       PlacesUtils.LMANNO_FEEDURI);
-  if (parentIsLivemark) {
-    // A livemark can't be a descendant of another livemark.
-    BookmarkSyncLog.debug(`insertSyncLivemark: Invalid parent ${
-      insertInfo.parentRecordId}; skipping livemark record ${
-      insertInfo.recordId}`);
-    return null;
-  }
-
-  let livemarkItem = await PlacesUtils.livemarks.addLivemark(livemarkInfo);
-
-  return insertBookmarkMetadata(db, livemarkItem, insertInfo);
-}
-
 // Keywords are a 1 to 1 mapping between strings and pairs of (URL, postData).
 // (the postData is not synced, so we ignore it). Sync associates keywords with
 // bookmarks, which is not really accurate. -- We might already have a keyword
 // with that name, or we might already have another bookmark with that URL with
 // a different keyword, etc.
 //
 // If we don't handle those cases by removing the conflicting keywords first,
 // the insertion  will fail, and the keywords will either be wrong, or missing.
@@ -1586,20 +1592,17 @@ async function insertBookmarkMetadata(db
 
   return newItem;
 }
 
 // Determines the Sync record kind for an existing bookmark.
 async function getKindForItem(db, item) {
   switch (item.type) {
     case PlacesUtils.bookmarks.TYPE_FOLDER: {
-      let isLivemark = await getAnno(db, item.guid,
-                                     PlacesUtils.LMANNO_FEEDURI);
-      return isLivemark ? BookmarkSyncUtils.KINDS.LIVEMARK :
-                          BookmarkSyncUtils.KINDS.FOLDER;
+      return BookmarkSyncUtils.KINDS.FOLDER;
     }
     case PlacesUtils.bookmarks.TYPE_BOOKMARK:
       return item.url.protocol == "place:" ?
              BookmarkSyncUtils.KINDS.QUERY :
              BookmarkSyncUtils.KINDS.BOOKMARK;
 
     case PlacesUtils.bookmarks.TYPE_SEPARATOR:
       return BookmarkSyncUtils.KINDS.SEPARATOR;
@@ -1611,55 +1614,24 @@ async function getKindForItem(db, item) 
 // record kind.
 function getTypeForKind(kind) {
   switch (kind) {
     case BookmarkSyncUtils.KINDS.BOOKMARK:
     case BookmarkSyncUtils.KINDS.QUERY:
       return PlacesUtils.bookmarks.TYPE_BOOKMARK;
 
     case BookmarkSyncUtils.KINDS.FOLDER:
-    case BookmarkSyncUtils.KINDS.LIVEMARK:
       return PlacesUtils.bookmarks.TYPE_FOLDER;
 
     case BookmarkSyncUtils.KINDS.SEPARATOR:
       return PlacesUtils.bookmarks.TYPE_SEPARATOR;
   }
   throw new Error(`Unknown bookmark kind: ${kind}`);
 }
 
-// Determines if a livemark should be reinserted. Returns true if `updateInfo`
-// specifies different feed or site URLs; false otherwise.
-var shouldReinsertLivemark = async function(updateInfo) {
-  let hasFeed = updateInfo.hasOwnProperty("feed");
-  let hasSite = updateInfo.hasOwnProperty("site");
-  if (!hasFeed && !hasSite) {
-    return false;
-  }
-  let guid = BookmarkSyncUtils.recordIdToGuid(updateInfo.recordId);
-  let livemark = await PlacesUtils.livemarks.getLivemark({
-    guid,
-  });
-  if (hasFeed) {
-    let feedURI = PlacesUtils.toURI(updateInfo.feed);
-    if (!livemark.feedURI.equals(feedURI)) {
-      return true;
-    }
-  }
-  if (hasSite) {
-    if (!updateInfo.site) {
-      return !!livemark.siteURI;
-    }
-    let siteURI = PlacesUtils.toURI(updateInfo.site);
-    if (!livemark.siteURI || !siteURI.equals(livemark.siteURI)) {
-      return true;
-    }
-  }
-  return false;
-};
-
 async function updateSyncBookmark(db, updateInfo) {
   let guid = BookmarkSyncUtils.recordIdToGuid(updateInfo.recordId);
   let oldBookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
   if (!oldBookmarkItem) {
     throw new Error(`Bookmark with record ID ${
       updateInfo.recordId} does not exist`);
   }
 
@@ -1681,40 +1653,30 @@ async function updateSyncBookmark(db, up
     shouldReinsert = true;
     if (BookmarkSyncLog.level <= Log.Level.Warn) {
       let oldRecordId = BookmarkSyncUtils.guidToRecordId(oldBookmarkItem.guid);
       BookmarkSyncLog.warn(`updateSyncBookmark: Local ${
         oldRecordId} kind = ${oldKind}; remote ${
         updateInfo.recordId} kind = ${
         updateInfo.kind}. Deleting and recreating`);
     }
-  } else if (oldKind == BookmarkSyncUtils.KINDS.LIVEMARK) {
-    // Similarly, if we're changing a livemark's site or feed URL, we need to
-    // reinsert.
-    shouldReinsert = await shouldReinsertLivemark(updateInfo);
-    if (BookmarkSyncLog.level <= Log.Level.Debug) {
-      let oldRecordId = BookmarkSyncUtils.guidToRecordId(oldBookmarkItem.guid);
-      BookmarkSyncLog.debug(`updateSyncBookmark: Local ${
-        oldRecordId} and remote ${
-        updateInfo.recordId} livemarks have different URLs`);
-    }
   }
 
   if (shouldReinsert) {
     if (!updateInfo.hasOwnProperty("dateAdded")) {
       updateInfo.dateAdded = oldBookmarkItem.dateAdded.getTime();
     }
     let newInfo = validateNewBookmark("BookmarkSyncUtils: reinsert",
                                       updateInfo);
     await PlacesUtils.bookmarks.remove({
       guid,
       source: SOURCE_SYNC,
     });
     // A reinsertion likely indicates a confused client, since there aren't
-    // public APIs for changing livemark URLs or an item's kind (e.g., turning
+    // public APIs for changing an item's kind (e.g., turning
     // a folder into a separator while preserving its annos and position).
     // This might be a good case to repair later; for now, we assume Sync has
     // passed a complete record for the new item, and don't try to merge
     // `oldBookmarkItem` with `updateInfo`.
     return insertSyncBookmark(db, newInfo);
   }
 
   let isOrphan = false, requestedParentRecordId;
@@ -1804,27 +1766,24 @@ function validateNewBookmark(name, info)
       recordId: { required: true },
       url: { requiredIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
                                 BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind),
             validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
                             BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) },
       parentRecordId: { required: true },
       title: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
                                BookmarkSyncUtils.KINDS.QUERY,
-                               BookmarkSyncUtils.KINDS.FOLDER,
-                               BookmarkSyncUtils.KINDS.LIVEMARK ].includes(b.kind) ||
+                               BookmarkSyncUtils.KINDS.FOLDER ].includes(b.kind) ||
                              b.title === "" },
       query: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.QUERY },
       folder: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.QUERY },
       tags: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
                               BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) },
       keyword: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
                                  BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) },
-      feed: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK },
-      site: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK },
       dateAdded: { required: false },
     });
 
   return insertInfo;
 }
 
 async function fetchGuidsWithAnno(db, anno, val) {
   let rows = await db.executeCached(`
@@ -1832,28 +1791,16 @@ async function fetchGuidsWithAnno(db, an
     JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
     JOIN moz_bookmarks b ON b.id = a.item_id
     WHERE n.name = :anno AND
           a.content = :val`,
     { anno, val });
   return rows.map(row => row.getResultByName("guid"));
 }
 
-// Returns the value of an item's annotation, or `null` if it's not set.
-async function getAnno(db, guid, anno) {
-  let rows = await db.executeCached(`
-    SELECT a.content FROM moz_items_annos a
-    JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
-    JOIN moz_bookmarks b ON b.id = a.item_id
-    WHERE b.guid = :guid AND
-          n.name = :anno`,
-    { guid, anno });
-  return rows.length ? rows[0].getResultByName("content") : null;
-}
-
 function tagItem(item, tags) {
   if (!item.url) {
     return [];
   }
 
   // Remove leading and trailing whitespace, then filter out empty tags.
   let newTags = tags ? tags.map(tag => tag.trim()).filter(Boolean) : [];
 
@@ -1875,19 +1822,19 @@ function tagItem(item, tags) {
 // but doesn't know about additional livemark properties. We check this to avoid
 // having it throw in case we only pass properties like `{ guid, feedURI }`.
 function shouldUpdateBookmark(bookmarkInfo) {
   return bookmarkInfo.hasOwnProperty("parentGuid") ||
          bookmarkInfo.hasOwnProperty("title") ||
          bookmarkInfo.hasOwnProperty("url");
 }
 
-// Converts a Places bookmark or livemark to a Sync bookmark. This function
-// maps Places GUIDs to record IDs and filters out extra Places properties like
-// date added, last modified, and index.
+// Converts a Places bookmark to a Sync bookmark. This function maps Places
+// GUIDs to record IDs and filters out extra Places properties like date added,
+// last modified, and index.
 async function placesBookmarkToSyncBookmark(db, bookmarkItem) {
   let item = {};
 
   for (let prop in bookmarkItem) {
     switch (prop) {
       // Record IDs are identical to Places GUIDs for all items except roots.
       case "guid":
         item.recordId = BookmarkSyncUtils.guidToRecordId(bookmarkItem.guid);
@@ -1907,28 +1854,16 @@ async function placesBookmarkToSyncBookm
       case "title":
       case "url":
         item[prop] = bookmarkItem[prop];
         break;
 
       case "dateAdded":
         item[prop] = new Date(bookmarkItem[prop]).getTime();
         break;
-
-      // Livemark objects contain additional properties. The feed URL is
-      // required; the site URL is optional.
-      case "feedURI":
-        item.feed = new URL(bookmarkItem.feedURI.spec);
-        break;
-
-      case "siteURI":
-        if (bookmarkItem.siteURI) {
-          item.site = new URL(bookmarkItem.siteURI.spec);
-        }
-        break;
     }
   }
 
   return item;
 }
 
 // Converts a Sync bookmark object to a Places bookmark or livemark object.
 // This function maps record IDs to Places GUIDs, and filters out extra Sync
@@ -1954,36 +1889,25 @@ function syncBookmarkToPlacesBookmark(in
         bookmarkInfo.dateAdded = new Date(info.dateAdded);
         break;
 
       case "parentRecordId":
         bookmarkInfo.parentGuid =
           BookmarkSyncUtils.recordIdToGuid(info.parentRecordId);
         // Instead of providing an index, Sync reorders children at the end of
         // the sync using `BookmarkSyncUtils.order`. We explicitly specify the
-        // default index here to prevent `PlacesUtils.bookmarks.update` and
-        // `PlacesUtils.livemarks.addLivemark` from throwing.
+        // default index here to prevent `PlacesUtils.bookmarks.update` from
+        // throwing.
         bookmarkInfo.index = PlacesUtils.bookmarks.DEFAULT_INDEX;
         break;
 
       case "title":
       case "url":
         bookmarkInfo[prop] = info[prop];
         break;
-
-      // Livemark-specific properties.
-      case "feed":
-        bookmarkInfo.feedURI = PlacesUtils.toURI(info.feed);
-        break;
-
-      case "site":
-        if (info.site) {
-          bookmarkInfo.siteURI = PlacesUtils.toURI(info.site);
-        }
-        break;
     }
   }
 
   return bookmarkInfo;
 }
 
 // Creates and returns a Sync bookmark object containing the bookmark's
 // tags, keyword.
@@ -2018,38 +1942,16 @@ async function fetchFolderItem(db, bookm
   let childGuids = await fetchChildGuids(db, bookmarkItem.guid);
   item.childRecordIds = childGuids.map(guid =>
     BookmarkSyncUtils.guidToRecordId(guid)
   );
 
   return item;
 }
 
-// Creates and returns a Sync bookmark object containing the livemark's
-// children (none), feed URI, and site URI.
-async function fetchLivemarkItem(db, bookmarkItem) {
-  let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
-
-  if (!item.title) {
-    item.title = "";
-  }
-
-  let feedAnno = await getAnno(db, bookmarkItem.guid,
-                               PlacesUtils.LMANNO_FEEDURI);
-  item.feed = new URL(feedAnno);
-
-  let siteAnno = await getAnno(db, bookmarkItem.guid,
-                               PlacesUtils.LMANNO_SITEURI);
-  if (siteAnno) {
-    item.site = new URL(siteAnno);
-  }
-
-  return item;
-}
-
 // Creates and returns a Sync bookmark object containing the query's tag
 // folder name.
 async function fetchQueryItem(db, bookmarkItem) {
   let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
   let params = new URLSearchParams(bookmarkItem.url.pathname);
   let tags = params.getAll("tag");
   if (tags.length == 1) {
@@ -2100,42 +2002,54 @@ function addRowToChangeRecords(row, chan
 }
 
 /**
  * Queries the database for synced bookmarks and tombstones, and returns a
  * changeset for the Sync bookmarks engine.
  *
  * @param db
  *        The Sqlite.jsm connection handle.
+ * @param forGuids
+ *        Fetch Sync tracking information for only the requested GUIDs.
  * @return {Promise} resolved once all items have been fetched.
  * @resolves to an object containing records for changed bookmarks, keyed by
  *           the record ID.
  */
-var pullSyncChanges = async function(db) {
+var pullSyncChanges = async function(db, forGuids = []) {
   let changeRecords = {};
 
+  let itemConditions = ["syncChangeCounter >= 1"];
+  let tombstoneConditions = ["1 = 1"];
+  if (forGuids.length) {
+    let restrictToGuids = `guid IN (${forGuids.map(guid =>
+      JSON.stringify(guid)).join(",")})`;
+    itemConditions.push(restrictToGuids);
+    tombstoneConditions.push(restrictToGuids);
+  }
+
   let rows = await db.executeCached(`
     WITH RECURSIVE
     syncedItems(id, guid, modified, syncChangeCounter, syncStatus) AS (
       SELECT b.id, b.guid, b.lastModified, b.syncChangeCounter, b.syncStatus
        FROM moz_bookmarks b
        WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
                         'mobile______')
       UNION ALL
       SELECT b.id, b.guid, b.lastModified, b.syncChangeCounter, b.syncStatus
       FROM moz_bookmarks b
       JOIN syncedItems s ON b.parent = s.id
     )
     SELECT guid, modified, syncChangeCounter, syncStatus, 0 AS tombstone
     FROM syncedItems
-    WHERE syncChangeCounter >= 1
+    WHERE ${itemConditions.join(" AND ")}
     UNION ALL
     SELECT guid, dateRemoved AS modified, 1 AS syncChangeCounter,
            :deletedSyncStatus, 1 AS tombstone
-    FROM moz_bookmarks_deleted`,
+    FROM moz_bookmarks_deleted
+    WHERE ${tombstoneConditions.join(" AND ")}`,
     { deletedSyncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL });
   for (let row of rows) {
     addRowToChangeRecords(row, changeRecords);
   }
 
   return changeRecords;
 };
 
--- a/toolkit/components/places/SyncedBookmarksMirror.jsm
+++ b/toolkit/components/places/SyncedBookmarksMirror.jsm
@@ -1246,36 +1246,26 @@ class SyncedBookmarksMirror {
              /* Map Places item types to Sync record kinds. */
              (CASE s.type
                 WHEN :bookmarkType THEN (
                   CASE SUBSTR((SELECT h.url FROM moz_places h
                                WHERE h.id = s.placeId), 1, 6)
                   /* Queries are bookmarks with a "place:" URL scheme. */
                   WHEN 'place:' THEN :queryKind
                   ELSE :bookmarkKind END)
-                WHEN :folderType THEN (
-                  CASE WHEN EXISTS(
-                    /* Livemarks are folders with a feed URL annotation. */
-                    SELECT 1 FROM moz_items_annos a
-                    JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
-                    WHERE a.item_id = s.id AND
-                          n.name = :feedURLAnno
-                  ) THEN :livemarkKind
-                  ELSE :folderKind END)
+                WHEN :folderType THEN :folderKind
                 ELSE :separatorKind END) AS kind,
              s.lastModified / 1000 AS localModified, s.syncChangeCounter,
              s.level, s.isSyncable
       FROM localItems s
       ORDER BY s.level, s.parentId, s.position`,
       { bookmarkType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
         queryKind: SyncedBookmarksMirror.KIND.QUERY,
         bookmarkKind: SyncedBookmarksMirror.KIND.BOOKMARK,
         folderType: PlacesUtils.bookmarks.TYPE_FOLDER,
-        feedURLAnno: PlacesUtils.LMANNO_FEEDURI,
-        livemarkKind: SyncedBookmarksMirror.KIND.LIVEMARK,
         folderKind: SyncedBookmarksMirror.KIND.FOLDER,
         separatorKind: SyncedBookmarksMirror.KIND.SEPARATOR });
 
     for await (let row of yieldingIterator(itemRows)) {
       let parentGuid = row.getResultByName("parentGuid");
       let node = BookmarkNode.fromLocalRow(row, localTimeSeconds);
       localTree.insert(parentGuid, node);
     }
@@ -1641,39 +1631,16 @@ class SyncedBookmarksMirror {
             bookmarkCleartext.tags = tags;
           }
           changeRecords[recordId] = new BookmarkChangeRecord(
             syncChangeCounter, bookmarkCleartext);
           continue;
         }
 
         case PlacesUtils.bookmarks.TYPE_FOLDER: {
-          let feedURLHref = row.getResultByName("feedURL");
-          if (feedURLHref) {
-            // Places stores livemarks as folders with feed and site URL annos.
-            // See bug 1072833 for discussion about changing them to queries.
-            let livemarkCleartext = {
-              id: recordId,
-              type: "livemark",
-              parentid: parentRecordId,
-              hasDupe: true,
-              parentName: row.getResultByName("parentTitle"),
-              dateAdded: row.getResultByName("dateAdded") || undefined,
-              title: row.getResultByName("title"),
-              feedUri: feedURLHref,
-            };
-            let siteURLHref = row.getResultByName("siteURL");
-            if (siteURLHref) {
-              livemarkCleartext.siteUri = siteURLHref;
-            }
-            changeRecords[recordId] = new BookmarkChangeRecord(
-              syncChangeCounter, livemarkCleartext);
-            continue;
-          }
-
           let folderCleartext = {
             id: recordId,
             type: "folder",
             parentid: parentRecordId,
             hasDupe: true,
             parentName: row.getResultByName("parentTitle"),
             dateAdded: row.getResultByName("dateAdded") || undefined,
             title: row.getResultByName("title"),
@@ -2677,19 +2644,28 @@ async function inflateTree(tree, pseudoT
   if (nodes) {
     for (let node of nodes) {
       await maybeYield();
       node.level = parentNode.level + 1;
       // See `LocalItemsSQLFragment` for a more detailed explanation about
       // syncable and non-syncable items. Non-syncable items might be
       // reuploaded by Android after a node reassignment, or orphaned on the
       // server.
-      node.isSyncable = parentNode == tree.root ?
-        PlacesUtils.bookmarks.userContentRoots.includes(node.guid) :
-        parentNode.isSyncable;
+      if (parentNode == tree.root) {
+        node.isSyncable = PlacesUtils.bookmarks.userContentRoots.includes(node.guid);
+      } else if (node.kind == SyncedBookmarksMirror.KIND.LIVEMARK) {
+        // Places no longer supports livemarks, so we flag unmerged remote
+        // livemarks as non-syncable. This will delete them locally, upload
+        // tombstones, and reupload their parents. Note that we *don't* flag
+        // livemarks that have already been synced, to minimize data loss.
+        // This is only for livemarks that we haven't downloaded yet.
+        node.isSyncable = false;
+      } else {
+        node.isSyncable = parentNode.isSyncable;
+      }
       tree.insert(parentNode.guid, node);
       await inflateTree(tree, pseudoTree, node);
     }
   }
 }
 
 /**
  * Measures and logs the time taken to execute a function, using a monotonic
@@ -2969,30 +2945,16 @@ class BookmarkNode {
     // can cause it to flip kinds - and webextensions are able to change the
     // URL of any bookmark.
     if ((this.kind == SyncedBookmarksMirror.KIND.BOOKMARK &&
          remoteNode.kind == SyncedBookmarksMirror.KIND.QUERY) ||
         (this.kind == SyncedBookmarksMirror.KIND.QUERY &&
          remoteNode.kind == SyncedBookmarksMirror.KIND.BOOKMARK)) {
       return true;
     }
-    // A local folder can become a livemark as the remote may have synced
-    // as a folder before the annotation was added. However, we don't allow
-    // a local livemark to "downgrade" to a folder.
-    // We allow merging local folders and remote livemarks because Places
-    // stores livemarks as empty folders with feed and site URL annotations.
-    // The livemarks service first inserts the folder, and *then* sets
-    // annotations. Since this isn't wrapped in a transaction, we might sync
-    // before the annotations are set, and upload a folder record instead
-    // of a livemark record (bug 632287), then replace the folder with a
-    // livemark on the next sync.
-    if (this.kind == SyncedBookmarksMirror.KIND.FOLDER &&
-        remoteNode.kind == SyncedBookmarksMirror.KIND.LIVEMARK) {
-      return true;
-    }
     return false;
   }
 
   /**
    * Generates a human-readable, ASCII art representation of the node and its
    * descendants. This is useful for visualizing the tree structure in trace
    * logs.
    *
@@ -4429,17 +4391,16 @@ class BookmarkObserverRecorder {
    * URLs. This is called outside the merge transaction.
    */
   async notifyAll() {
     await this.noteAllChanges();
     if (this.shouldInvalidateKeywords) {
       await PlacesUtils.keywords.invalidateCachedKeywords();
     }
     await this.notifyBookmarkObservers();
-    await PlacesUtils.livemarks.invalidateCachedLivemarks();
     await this.updateFrecencies();
   }
 
   async updateFrecencies() {
     MirrorLog.trace("Recalculating frecencies for new URLs");
     await this.db.execute(`
       UPDATE moz_places SET
         frecency = CALCULATE_FRECENCY(id)
deleted file mode 100644
--- a/toolkit/components/places/tests/sync/livemark.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-  <title>Livemark Feed</title>
-  <link href="https://example.com/"/>
-  <updated>2016-08-09T19:51:45.147Z</updated>
-  <author>
-    <name>John Doe</name>
-  </author>
-  <id>urn:uuid:e7947414-6ee0-4009-ae75-8b0ad3c6894b</id>
-  <entry>
-    <title>Some awesome article</title>
-    <link href="https://example.com/some-article"/>
-    <id>urn:uuid:d72ce019-0a56-4a0b-ac03-f66117d78141</id>
-    <updated>2016-08-09T19:57:22.178Z</updated>
-    <summary>My great article summary.</summary>
-  </entry>
-</feed>
--- a/toolkit/components/places/tests/sync/test_bookmark_kinds.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_kinds.js
@@ -1,383 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-add_task(async function test_livemarks() {
-  let { site, stopServer } = makeLivemarkServer();
-
-  try {
-    let buf = await openMirror("livemarks");
-
-    let unfiledFolderId =
-      await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid);
-
-    info("Set up mirror");
-    await PlacesUtils.bookmarks.insertTree({
-      guid: PlacesUtils.bookmarks.menuGuid,
-      children: [{
-        guid: "livemarkAAAA",
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        title: "A",
-        annos: [{
-          name: PlacesUtils.LMANNO_FEEDURI,
-          value: site + "/feed/a",
-        }],
-      }],
-    });
-    await storeRecords(buf, shuffle([{
-      id: "menu",
-      type: "folder",
-      children: ["livemarkAAAA"],
-    }, {
-      id: "livemarkAAAA",
-      type: "livemark",
-      title: "A",
-      feedUri: site + "/feed/a",
-    }]), { needsMerge: false });
-    await PlacesTestUtils.markBookmarksAsSynced();
-
-    info("Make local changes");
-    await PlacesUtils.livemarks.addLivemark({
-      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
-      guid: "livemarkBBBB",
-      title: "B",
-      feedURI: Services.io.newURI(site + "/feed/b-local"),
-      siteURI: Services.io.newURI(site + "/site/b-local"),
-    });
-    let livemarkD = await PlacesUtils.livemarks.addLivemark({
-      parentGuid: PlacesUtils.bookmarks.menuGuid,
-      guid: "livemarkDDDD",
-      title: "D",
-      feedURI: Services.io.newURI(site + "/feed/d"),
-      siteURI: Services.io.newURI(site + "/site/d"),
-    });
-
-    info("Make remote changes");
-    await storeRecords(buf, shuffle([{
-      id: "livemarkAAAA",
-      type: "livemark",
-      title: "A (remote)",
-      feedUri: site + "/feed/a-remote",
-    }, {
-      id: "toolbar",
-      type: "folder",
-      children: ["livemarkCCCC", "livemarkB111"],
-    }, {
-      id: "unfiled",
-      type: "folder",
-      children: ["livemarkEEEE"],
-    }, {
-      id: "livemarkCCCC",
-      type: "livemark",
-      title: "C (remote)",
-      feedUri: site + "/feed/c-remote",
-    }, {
-      id: "livemarkB111",
-      type: "livemark",
-      title: "B",
-      feedUri: site + "/feed/b-remote",
-    }, {
-      id: "livemarkEEEE",
-      type: "livemark",
-      title: "E",
-      feedUri: site + "/feed/e",
-      siteUri: site + "/site/e",
-    }]));
-
-    info("Apply remote");
-    let observer = expectBookmarkChangeNotifications();
-    let changesToUpload = await buf.apply();
-    deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
-
-    let menuInfo = await PlacesUtils.bookmarks.fetch(
-      PlacesUtils.bookmarks.menuGuid);
-    let toolbarInfo = await PlacesUtils.bookmarks.fetch(
-      PlacesUtils.bookmarks.toolbarGuid);
-    deepEqual(changesToUpload, {
-      livemarkDDDD: {
-        tombstone: false,
-        counter: 1,
-        synced: false,
-        cleartext: {
-          id: "livemarkDDDD",
-          type: "livemark",
-          parentid: "menu",
-          hasDupe: true,
-          parentName: BookmarksMenuTitle,
-          dateAdded: PlacesUtils.toDate(livemarkD.dateAdded).getTime(),
-          title: "D",
-          feedUri: site + "/feed/d",
-          siteUri: site + "/site/d",
-        },
-      },
-      menu: {
-        tombstone: false,
-        counter: 2,
-        synced: false,
-        cleartext: {
-          id: "menu",
-          type: "folder",
-          parentid: "places",
-          hasDupe: true,
-          parentName: "",
-          dateAdded: menuInfo.dateAdded.getTime(),
-          title: menuInfo.title,
-          children: ["livemarkAAAA", "livemarkDDDD"],
-        },
-      },
-      toolbar: {
-        tombstone: false,
-        counter: 1,
-        synced: false,
-        cleartext: {
-          id: "toolbar",
-          type: "folder",
-          parentid: "places",
-          hasDupe: true,
-          parentName: "",
-          dateAdded: toolbarInfo.dateAdded.getTime(),
-          title: toolbarInfo.title,
-          children: ["livemarkCCCC", "livemarkB111"],
-        },
-      },
-    }, "Should upload new local livemark A");
-
-    await assertLocalTree(PlacesUtils.bookmarks.rootGuid, {
-      guid: PlacesUtils.bookmarks.rootGuid,
-      type: PlacesUtils.bookmarks.TYPE_FOLDER,
-      index: 0,
-      title: "",
-      children: [{
-        guid: PlacesUtils.bookmarks.menuGuid,
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        index: 0,
-        title: BookmarksMenuTitle,
-        children: [{
-          guid: "livemarkAAAA",
-          type: PlacesUtils.bookmarks.TYPE_FOLDER,
-          index: 0,
-          title: "A (remote)",
-          annos: [{
-            name: PlacesUtils.LMANNO_FEEDURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/feed/a-remote",
-          }],
-        }, {
-          guid: "livemarkDDDD",
-          type: PlacesUtils.bookmarks.TYPE_FOLDER,
-          index: 1,
-          title: "D",
-          annos: [{
-            name: PlacesUtils.LMANNO_FEEDURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/feed/d",
-          }, {
-            name: PlacesUtils.LMANNO_SITEURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/site/d",
-          }],
-        }],
-      }, {
-        guid: PlacesUtils.bookmarks.toolbarGuid,
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        index: 1,
-        title: BookmarksToolbarTitle,
-        children: [{
-          guid: "livemarkCCCC",
-          type: PlacesUtils.bookmarks.TYPE_FOLDER,
-          index: 0,
-          title: "C (remote)",
-          annos: [{
-            name: PlacesUtils.LMANNO_FEEDURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/feed/c-remote",
-          }],
-        }, {
-          guid: "livemarkB111",
-          type: PlacesUtils.bookmarks.TYPE_FOLDER,
-          index: 1,
-          title: "B",
-          annos: [{
-            name: PlacesUtils.LMANNO_FEEDURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/feed/b-remote",
-          }],
-        }],
-      }, {
-        guid: PlacesUtils.bookmarks.unfiledGuid,
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        index: 3,
-        title: UnfiledBookmarksTitle,
-        children: [{
-          guid: "livemarkEEEE",
-          type: PlacesUtils.bookmarks.TYPE_FOLDER,
-          index: 0,
-          title: "E",
-          annos: [{
-            name: PlacesUtils.LMANNO_FEEDURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/feed/e",
-          }, {
-            name: PlacesUtils.LMANNO_SITEURI,
-            flags: 0,
-            expires: PlacesUtils.annotations.EXPIRE_NEVER,
-            value: site + "/site/e",
-          }],
-        }],
-      }, {
-        guid: PlacesUtils.bookmarks.mobileGuid,
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        index: 4,
-        title: MobileBookmarksTitle,
-      }],
-    }, "Should apply and dedupe livemarks");
-
-    let livemarkA = await PlacesUtils.livemarks.getLivemark({
-      guid: "livemarkAAAA",
-    });
-    let livemarkB = await PlacesUtils.livemarks.getLivemark({
-      guid: "livemarkB111",
-    });
-    let livemarkC = await PlacesUtils.livemarks.getLivemark({
-      guid: "livemarkCCCC",
-    });
-    let livemarkE = await PlacesUtils.livemarks.getLivemark({
-      guid: "livemarkEEEE",
-    });
-
-    observer.check([{
-      name: "onItemChanged",
-      params: { itemId: livemarkB.id, property: "guid", isAnnoProperty: false,
-                newValue: "livemarkB111", parentId: PlacesUtils.toolbarFolderId,
-                type: PlacesUtils.bookmarks.TYPE_FOLDER, guid: "livemarkB111",
-                parentGuid: "toolbar_____", oldValue: "livemarkBBBB",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "bookmark-added",
-      params: { itemId: livemarkC.id, parentId: PlacesUtils.toolbarFolderId,
-                index: 0, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                urlHref: "", title: "C (remote)", guid: "livemarkCCCC",
-                parentGuid: PlacesUtils.bookmarks.toolbarGuid,
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "bookmark-added",
-      params: { itemId: livemarkE.id,
-                parentId: unfiledFolderId,
-                index: 0, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                urlHref: "", title: "E", guid: "livemarkEEEE",
-                parentGuid: "unfiled_____",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemMoved",
-      params: { itemId: livemarkB.id, oldParentId: PlacesUtils.toolbarFolderId,
-                oldIndex: 0, newParentId: PlacesUtils.toolbarFolderId,
-                newIndex: 1, type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                guid: "livemarkB111", uri: null,
-                oldParentGuid: PlacesUtils.bookmarks.toolbarGuid,
-                newParentGuid: PlacesUtils.bookmarks.toolbarGuid,
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkA.id, property: "title", isAnnoProperty: false,
-                newValue: "A (remote)", type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.bookmarksMenuFolderId,
-                guid: "livemarkAAAA",
-                parentGuid: PlacesUtils.bookmarks.menuGuid, oldValue: "A",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkA.id, property: PlacesUtils.LMANNO_FEEDURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.bookmarksMenuFolderId,
-                guid: "livemarkAAAA",
-                parentGuid: PlacesUtils.bookmarks.menuGuid, oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkA.id, property: PlacesUtils.LMANNO_FEEDURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.bookmarksMenuFolderId,
-                guid: "livemarkAAAA",
-                parentGuid: PlacesUtils.bookmarks.menuGuid, oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkC.id, property: "livemark/feedURI",
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.toolbarFolderId, guid: "livemarkCCCC",
-                parentGuid: PlacesUtils.bookmarks.toolbarGuid, oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkB.id, property: PlacesUtils.LMANNO_FEEDURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.toolbarFolderId,
-                guid: "livemarkB111",
-                parentGuid: PlacesUtils.bookmarks.toolbarGuid, oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkB.id, property: PlacesUtils.LMANNO_SITEURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.toolbarFolderId,
-                guid: "livemarkB111",
-                parentGuid: PlacesUtils.bookmarks.toolbarGuid, oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkB.id, property: PlacesUtils.LMANNO_FEEDURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: PlacesUtils.toolbarFolderId,
-                guid: "livemarkB111",
-                parentGuid: PlacesUtils.bookmarks.toolbarGuid, oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkE.id, property: PlacesUtils.LMANNO_FEEDURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: unfiledFolderId,
-                guid: "livemarkEEEE",
-                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }, {
-      name: "onItemChanged",
-      params: { itemId: livemarkE.id, property: PlacesUtils.LMANNO_SITEURI,
-                isAnnoProperty: true, newValue: "",
-                type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                parentId: unfiledFolderId,
-                guid: "livemarkEEEE",
-                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                oldValue: "",
-                source: PlacesUtils.bookmarks.SOURCES.SYNC },
-    }]);
-
-    await buf.finalize();
-  } finally {
-    await stopServer();
-  }
-
-  await PlacesUtils.bookmarks.eraseEverything();
-  await PlacesSyncUtils.bookmarks.reset();
-});
-
 add_task(async function test_queries() {
   let buf = await openMirror("queries");
 
   info("Set up places");
 
   // create a tag and grab the local folder ID.
   let tag = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
@@ -531,71 +159,25 @@ add_task(async function test_mismatched_
   }]);
 
   info("Apply remote");
   let changesToUpload = await buf.apply();
   deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
 
   let idsToUpload = inspectChangeRecords(changesToUpload);
   deepEqual(idsToUpload, {
-    updated: [],
-    deleted: [],
-  }, "Should not reupload merged livemark");
+    updated: ["menu", "unfiled"],
+    deleted: ["l1nZZXfB8nC7"],
+  }, "Legacy livemark should be deleted remotely");
 
   await buf.finalize();
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
-add_task(async function test_mismatched_but_incompatible_folder_types() {
-  let sawMismatchError = false;
-  let recordTelemetryEvent = (object, method, value, extra) => {
-    // expecting to see an error for kind mismatches.
-    if (method == "apply" && value == "error" &&
-        extra && extra.why == "Can't merge different item kinds") {
-      sawMismatchError = true;
-    }
-  };
-  let buf = await openMirror("mismatched_incompatible_types",
-                             {recordTelemetryEvent});
-  try {
-    info("Set up mirror");
-    await PlacesUtils.bookmarks.insertTree({
-      guid: PlacesUtils.bookmarks.menuGuid,
-      children: [{
-        guid: "livemarkAAAA",
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        title: "LiveMark",
-        annos: [{
-          name: PlacesUtils.LMANNO_FEEDURI,
-          value: "http://example.com/feed/a",
-        }],
-      }],
-    });
-    await PlacesTestUtils.markBookmarksAsSynced();
-
-    info("Make remote changes");
-    await storeRecords(buf, [{
-      "id": "livemarkAAAA",
-      "type": "folder",
-      "title": "not really a Livemark",
-      "description": null,
-      "parentid": "menu",
-    }]);
-
-    info("Apply remote, should fail");
-    await Assert.rejects(buf.apply(), /Can't merge different item kinds/);
-    Assert.ok(sawMismatchError, "saw the correct mismatch event");
-  } finally {
-    await buf.finalize();
-    await PlacesUtils.bookmarks.eraseEverything();
-    await PlacesSyncUtils.bookmarks.reset();
-  }
-});
-
 add_task(async function test_different_but_compatible_bookmark_types() {
   try {
     let buf = await openMirror("partial_queries");
 
     await PlacesUtils.bookmarks.insertTree({
       guid: PlacesUtils.bookmarks.menuGuid,
       children: [
         {
--- a/toolkit/components/places/tests/sync/test_sync_utils.js
+++ b/toolkit/components/places/tests/sync/test_sync_utils.js
@@ -4,34 +4,16 @@ ChromeUtils.import("resource://testing-c
 ChromeUtils.defineModuleGetter(this, "Preferences",
                                "resource://gre/modules/Preferences.jsm");
 Cu.importGlobalProperties(["URLSearchParams"]);
 
 const SYNC_PARENT_ANNO = "sync/parent";
 
 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() {
-      let { identity } = server;
-      let host = identity.primaryHost.includes(":") ?
-        `[${identity.primaryHost}]` : identity.primaryHost;
-      return `${identity.primaryScheme}://${host}:${identity.primaryPort}`;
-    },
-    stopServer() {
-      return new Promise(resolve => server.stop(resolve));
-    },
-  };
-}
-
 function shuffle(array) {
   let results = [];
   for (let i = 0; i < array.length; ++i) {
     let randomIndex = Math.floor(Math.random() * (i + 1));
     results[i] = results[randomIndex];
     results[randomIndex] = array[i];
   }
   return results;
@@ -1047,256 +1029,16 @@ add_task(async function test_insert() {
     equal(type, PlacesUtils.bookmarks.TYPE_SEPARATOR,
       "Separator should have correct type");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
-add_task(async function test_insert_livemark() {
-  let { site, stopServer } = makeLivemarkServer();
-
-  try {
-    info("Insert livemark with feed URL");
-    {
-      let livemark = await PlacesSyncUtils.bookmarks.insert({
-        kind: "livemark",
-        recordId: makeGuid(),
-        feed: site + "/feed/1",
-        parentRecordId: "menu",
-      });
-      let bmk = await PlacesUtils.bookmarks.fetch({
-        guid: await PlacesSyncUtils.bookmarks.recordIdToGuid(livemark.recordId),
-      });
-      equal(bmk.type, PlacesUtils.bookmarks.TYPE_FOLDER,
-        "Livemarks should be stored as folders");
-    }
-
-    let livemarkRecordId;
-    info("Insert livemark with site and feed URLs");
-    {
-      let livemark = await PlacesSyncUtils.bookmarks.insert({
-        kind: "livemark",
-        recordId: makeGuid(),
-        site,
-        feed: site + "/feed/1",
-        parentRecordId: "menu",
-      });
-      livemarkRecordId = livemark.recordId;
-    }
-
-    info("Try inserting livemark into livemark");
-    {
-      let livemark = await PlacesSyncUtils.bookmarks.insert({
-        kind: "livemark",
-        recordId: makeGuid(),
-        site,
-        feed: site + "/feed/1",
-        parentRecordId: livemarkRecordId,
-      });
-      ok(!livemark, "Should not insert livemark as child of livemark");
-    }
-  } finally {
-    await stopServer();
-  }
-
-  await PlacesUtils.bookmarks.eraseEverything();
-  await PlacesSyncUtils.bookmarks.reset();
-});
-
-add_task(async function test_update_livemark() {
-  let { site, stopServer } = makeLivemarkServer();
-  let feedURI = uri(site + "/feed/1");
-
-  try {
-    // We shouldn't reinsert the livemark if the URLs are the same.
-    info("Update livemark with same URLs");
-    {
-      let livemark = await PlacesUtils.livemarks.addLivemark({
-        parentGuid: PlacesUtils.bookmarks.menuGuid,
-        feedURI,
-        siteURI: uri(site),
-        index: PlacesUtils.bookmarks.DEFAULT_INDEX,
-      });
-
-      await PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        feed: feedURI,
-      });
-      // `nsLivemarkService` returns references to `Livemark` instances, so we
-      // can compare them with `==` to make sure they haven't been replaced.
-      equal(await PlacesUtils.livemarks.getLivemark({
-        guid: livemark.guid,
-      }), livemark, "Livemark with same feed URL should not be replaced");
-
-      await PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        site,
-      });
-      equal(await PlacesUtils.livemarks.getLivemark({
-        guid: livemark.guid,
-      }), livemark, "Livemark with same site URL should not be replaced");
-
-      await PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        feed: feedURI,
-        site,
-      });
-      equal(await PlacesUtils.livemarks.getLivemark({
-        guid: livemark.guid,
-      }), livemark, "Livemark with same feed and site URLs should not be replaced");
-    }
-
-    info("Change livemark feed URL");
-    {
-      let livemark = await PlacesUtils.livemarks.addLivemark({
-        parentGuid: PlacesUtils.bookmarks.menuGuid,
-        feedURI,
-        index: PlacesUtils.bookmarks.DEFAULT_INDEX,
-      });
-
-      // Since we're reinserting, we need to pass all properties required
-      // for a new livemark. `update` won't merge the old and new ones.
-      await Assert.rejects(PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        feed: site + "/feed/2",
-      }), /reinsert: Invalid value for property 'feed'/,
-          "Reinserting livemark with changed feed URL requires full record");
-
-      let newLivemark = await PlacesSyncUtils.bookmarks.update({
-        kind: "livemark",
-        parentRecordId: "menu",
-        recordId: livemark.guid,
-        feed: site + "/feed/2",
-      });
-      equal(newLivemark.recordId, livemark.guid,
-        "IDs should match for reinserted livemark with changed feed URL");
-      equal(newLivemark.feed.href, site + "/feed/2",
-        "Reinserted livemark should have changed feed URI");
-    }
-
-    info("Add livemark site URL");
-    {
-      let livemark = await PlacesUtils.livemarks.addLivemark({
-        parentGuid: PlacesUtils.bookmarks.menuGuid,
-        feedURI,
-      });
-      ok(livemark.feedURI.equals(feedURI), "Livemark feed URI should match");
-      ok(!livemark.siteURI, "Livemark should not have site URI");
-
-      await Assert.rejects(PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        site,
-      }), /reinsert: Invalid value for property 'site'/,
-          "Reinserting livemark with new site URL requires full record");
-
-      let newLivemark = await PlacesSyncUtils.bookmarks.update({
-        kind: "livemark",
-        parentRecordId: "menu",
-        recordId: livemark.guid,
-        feed: feedURI,
-        site,
-      });
-      notEqual(newLivemark, livemark,
-        "Livemark with new site URL should replace old livemark");
-      equal(newLivemark.recordId, livemark.guid,
-        "IDs should match for reinserted livemark with new site URL");
-      equal(newLivemark.site.href, site + "/",
-        "Reinserted livemark should have new site URI");
-      equal(newLivemark.feed.href, feedURI.spec,
-        "Reinserted livemark with new site URL should have same feed URI");
-    }
-
-    info("Remove livemark site URL");
-    {
-      let livemark = await PlacesUtils.livemarks.addLivemark({
-        parentGuid: PlacesUtils.bookmarks.menuGuid,
-        feedURI,
-        siteURI: uri(site),
-        index: PlacesUtils.bookmarks.DEFAULT_INDEX,
-      });
-
-      await Assert.rejects(PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        site: null,
-      }), /reinsert: Invalid value for property 'site'/,
-          "Reinserting livemark witout site URL requires full record");
-
-      let newLivemark = await PlacesSyncUtils.bookmarks.update({
-        kind: "livemark",
-        parentRecordId: "menu",
-        recordId: livemark.guid,
-        feed: feedURI,
-        site: null,
-      });
-      notEqual(newLivemark, livemark,
-        "Livemark without site URL should replace old livemark");
-      equal(newLivemark.recordId, livemark.guid,
-        "IDs should match for reinserted livemark without site URL");
-      ok(!newLivemark.site, "Reinserted livemark should not have site URI");
-    }
-
-    info("Change livemark site URL");
-    {
-      let livemark = await PlacesUtils.livemarks.addLivemark({
-        parentGuid: PlacesUtils.bookmarks.menuGuid,
-        feedURI,
-        siteURI: uri(site),
-        index: PlacesUtils.bookmarks.DEFAULT_INDEX,
-      });
-
-      await Assert.rejects(PlacesSyncUtils.bookmarks.update({
-        recordId: livemark.guid,
-        site: site + "/new",
-      }), /reinsert: Invalid value for property 'site'/,
-          "Reinserting livemark with changed site URL requires full record");
-
-      let newLivemark = await PlacesSyncUtils.bookmarks.update({
-        kind: "livemark",
-        parentRecordId: "menu",
-        recordId: livemark.guid,
-        feed: feedURI,
-        site: site + "/new",
-      });
-      notEqual(newLivemark, livemark,
-        "Livemark with changed site URL should replace old livemark");
-      equal(newLivemark.recordId, livemark.guid,
-        "IDs should match for reinserted livemark with changed site URL");
-      equal(newLivemark.site.href, site + "/new",
-        "Reinserted livemark should have changed site URI");
-    }
-
-    // Livemarks are stored as folders, but have different kinds. We should
-    // remove the folder and insert a livemark with the same GUID instead of
-    // trying to update the folder in-place.
-    info("Replace folder with livemark");
-    {
-      let folder = await PlacesUtils.bookmarks.insert({
-        type: PlacesUtils.bookmarks.TYPE_FOLDER,
-        parentGuid: PlacesUtils.bookmarks.menuGuid,
-        title: "Plain folder",
-      });
-      let livemark = await PlacesSyncUtils.bookmarks.update({
-        kind: "livemark",
-        parentRecordId: "menu",
-        recordId: folder.guid,
-        feed: feedURI,
-      });
-      equal(livemark.guid, folder.recordId,
-        "Livemark should have same GUID as replaced folder");
-    }
-  } finally {
-    await stopServer();
-  }
-
-  await PlacesUtils.bookmarks.eraseEverything();
-  await PlacesSyncUtils.bookmarks.reset();
-});
-
 add_task(async function test_insert_tags() {
   await Promise.all([{
     kind: "bookmark",
     url: "https://example.com",
     recordId: makeGuid(),
     parentRecordId: "menu",
     tags: ["foo", "bar"],
   }, {
@@ -1769,44 +1511,16 @@ add_task(async function test_fetch() {
     equal(item.url.href, `place:tag=taggy`, "Should not rewrite outgoing tag queries");
     equal(item.folder, "taggy", "Should return tag name for tag queries");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
-add_task(async function test_fetch_livemark() {
-  let { site, stopServer } = makeLivemarkServer();
-
-  try {
-    info("Create livemark");
-    let livemark = await PlacesUtils.livemarks.addLivemark({
-      parentGuid: PlacesUtils.bookmarks.menuGuid,
-      feedURI: uri(site + "/feed/1"),
-      siteURI: uri(site),
-      index: PlacesUtils.bookmarks.DEFAULT_INDEX,
-    });
-
-    info("Fetch livemark");
-    let item = await PlacesSyncUtils.bookmarks.fetch(livemark.guid);
-    deepEqual(Object.keys(item).sort(), ["recordId", "kind", "parentRecordId",
-      "feed", "site", "parentTitle", "title", "dateAdded"].sort(),
-      "Should include livemark-specific properties");
-    equal(item.feed.href, site + "/feed/1", "Should return feed URL");
-    equal(item.site.href, site + "/", "Should return site URL");
-    strictEqual(item.title, "", "Should include livemark title even if empty");
-  } finally {
-    await stopServer();
-  }
-
-  await PlacesUtils.bookmarks.eraseEverything();
-  await PlacesSyncUtils.bookmarks.reset();
-});
-
 add_task(async function test_pullChanges_new_parent() {
   await ignoreChangedRoots();
 
   let { syncedGuids, unsyncedFolder } = await moveSyncedBookmarksToUnsyncedParent();
 
   info("Unsynced parent and synced items should be tracked");
   let changes = await PlacesSyncUtils.bookmarks.pullChanges();
   deepEqual(Object.keys(changes).sort(),
@@ -2307,36 +2021,16 @@ add_task(async function test_touch() {
     deepEqual(Object.keys(changes).sort(), [bmk.recordId, "menu"].sort(),
       "Should return change records for revived bookmark and parent");
     equal(changes[bmk.recordId].counter, 1,
       "Change counter for revived bookmark should be 1");
 
     await setChangesSynced(changes);
   }
 
-  // Livemarks are stored as folders, but their kinds are different, so we
-  // should still bump their change counters.
-  let { site, stopServer } = makeLivemarkServer();
-  try {
-    let livemark = await PlacesSyncUtils.bookmarks.insert({
-      kind: "livemark",
-      recordId: makeGuid(),
-      feed: site + "/feed/1",
-      parentRecordId: "unfiled",
-    });
-
-    let changes = await PlacesSyncUtils.bookmarks.touch(livemark.recordId);
-    deepEqual(Object.keys(changes).sort(), [livemark.recordId, "unfiled"].sort(),
-      "Should return change records for revived livemark and parent");
-    equal(changes[livemark.recordId].counter, 1,
-      "Change counter for revived livemark should be 1");
-  } finally {
-    await stopServer();
-  }
-
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
 add_task(async function test_separator() {
   await ignoreChangedRoots();
 
   await PlacesSyncUtils.bookmarks.insert({
--- a/toolkit/components/places/tests/sync/xpcshell.ini
+++ b/toolkit/components/places/tests/sync/xpcshell.ini
@@ -1,12 +1,11 @@
 [DEFAULT]
 head = head_sync.js
 support-files =
-  livemark.xml
   sync_utils_bookmarks.html
   sync_utils_bookmarks.json
   mirror_corrupt.sqlite
   mirror_v1.sqlite
 
 [test_bookmark_corruption.js]
 [test_bookmark_deduping.js]
 [test_bookmark_deletion.js]