Browse Source

places updates plus latest comm patches

Frank-Rainer Grahl 14 hours ago
parent
commit
62eef75ea5

+ 47 - 0
comm-release/patches/1925849-domi-cmdline-25320.patch

@@ -0,0 +1,47 @@
+# HG changeset patch
+# User Ian Neal <iann_cvs@blueyonder.co.uk>
+# Date 1729433055 -3600
+# Parent  fc65844c30eb93fc7ce9cedaf5016fe0bfadf97c
+Bug 1925849 - Remove use of NSGetModule in DOMi. r=frg a=frg
+
+diff --git a/suite/extensions/inspector/base/js/inspector-cmdline.js b/suite/extensions/inspector/base/js/inspector-cmdline.js
+--- a/suite/extensions/inspector/base/js/inspector-cmdline.js
++++ b/suite/extensions/inspector/base/js/inspector-cmdline.js
+@@ -9,19 +9,16 @@ const nsISupportsString     = Components
+ const nsIWindowWatcher      = Components.interfaces.nsIWindowWatcher;
+ 
+ function InspectorCmdLineHandler() {}
+ InspectorCmdLineHandler.prototype =
+ {
+   classDescription: "DOM Inspector Command Line Handler",
+   classID: Components.ID("{38293526-6b13-4d4f-a075-71939435b408}"),
+   contractID: "@mozilla.org/commandlinehandler/general-startup;1?type=inspector",
+-  /* Needed for XPCOMUtils NSGetModule */
+-  _xpcom_categories: [{category: "command-line-handler",
+-                       entry: "m-inspector"}],
+ 
+   /* nsISupports */
+   QueryInterface: XPCOMUtils.generateQI([nsICommandLineHandler]),
+ 
+   /* nsICommandLineHandler */
+   handle : function handler_handle(cmdLine) {
+     var args = Components.classes["@mozilla.org/supports-string;1"]
+                          .createInstance(nsISupportsString);
+@@ -45,16 +42,9 @@ InspectorCmdLineHandler.prototype =
+     wwatch.openWindow(null, "chrome://inspector/content/", "_blank",
+                       "chrome,dialog=no,all", args);
+   },
+ 
+   helpInfo : "  -inspector <url>     Open the DOM inspector.\n"
+ };
+ 
+ 
+-/**
+- * XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4).
+- * XPCOMUtils.generateNSGetModule is for Mozilla 1.9.0 (Firefox 3.0).
+- */
+-if (XPCOMUtils.generateNSGetFactory)
+-  var NSGetFactory = XPCOMUtils.generateNSGetFactory([InspectorCmdLineHandler]);
+-else
+-  var NSGetModule = XPCOMUtils.generateNSGetModule([InspectorCmdLineHandler]);
++var NSGetFactory = XPCOMUtils.generateNSGetFactory([InspectorCmdLineHandler]);

+ 243 - 0
comm-release/patches/1925871-irc-remove-NSGetModule-v1_1-25320.patch

@@ -0,0 +1,243 @@
+# HG changeset patch
+# User Ian Neal <iann_cvs@blueyonder.co.uk>
+# Date 1729460986 -3600
+# Parent  1b656f6b40181f3be74040d5378b6fc6068fedc8
+Bug 1925871 - Remove use of NSGetModule in cZ. r=frg a=frg
+
+diff --git a/suite/extensions/irc/js/lib/chatzilla-service.js b/suite/extensions/irc/js/lib/chatzilla-service.js
+--- a/suite/extensions/irc/js/lib/chatzilla-service.js
++++ b/suite/extensions/irc/js/lib/chatzilla-service.js
+@@ -1,48 +1,40 @@
+ /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+  * This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+-
+-const MEDIATOR_CONTRACTID =
+-    "@mozilla.org/appshell/window-mediator;1";
+-const ASS_CONTRACTID =
+-    "@mozilla.org/appshell/appShellService;1";
++ChromeUtils.import("resource://gre/modules/Services.jsm");
++ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
++
++
+ const RDFS_CONTRACTID =
+     "@mozilla.org/rdf/rdf-service;1";
+-const CATMAN_CONTRACTID =
+-    "@mozilla.org/categorymanager;1";
+ const PPMM_CONTRACTID =
+     "@mozilla.org/parentprocessmessagemanager;1";
+ 
+-const CLINE_SERVICE_CONTRACTID =
+-    "@mozilla.org/commandlinehandler/general-startup;1?type=chat";
+ const CLINE_SERVICE_CID =
+     Components.ID("{38a95514-1dd2-11b2-97e7-9da958640f2c}");
+ const STARTUP_CID =
+     Components.ID("{ae6ad015-433b-42ab-9afc-1636af5a7fc4}");
+ 
+ 
+ Cu.import("chrome://chatzilla/content/lib/js/protocol-handlers.jsm");
+ /* global ChatZillaProtocols */
+ /* global IRCProtocolHandlerFactory, IRCSProtocolHandlerFactory */
+ /* global IRCPROT_HANDLER_CID, IRCSPROT_HANDLER_CID */
+ 
+ 
+ function spawnChatZilla(uri, count)
+ {
+-    const wm = Cc[MEDIATOR_CONTRACTID].getService(Ci.nsIWindowMediator);
+-    const ass = Cc[ASS_CONTRACTID].getService(Ci.nsIAppShellService);
+-    const hiddenWin = ass.hiddenDOMWindow;
++    const hiddenWin = Services.appShell.hiddenDOMWindow;
+ 
+     // Ok, not starting currently, so check if we've got existing windows.
+-    const w = wm.getMostRecentWindow("irc:chatzilla");
++    const w = Services.wm.getMostRecentWindow("irc:chatzilla");
+ 
+     // Claiming that a ChatZilla window is loading.
+     if ("ChatZillaStarting" in hiddenWin)
+     {
+         dump("cz-service: ChatZilla claiming to be starting.\n");
+         if (w && ("client" in w) && ("initialized" in w.client) &&
+             w.client.initialized)
+         {
+@@ -98,26 +90,17 @@ function spawnChatZilla(uri, count)
+ 
+ function CommandLineService()
+ {
+ }
+ 
+ CommandLineService.prototype =
+ {
+     /* nsISupports */
+-    QueryInterface(iid)
+-    {
+-        if (iid.equals(Ci.nsISupports))
+-            return this;
+-
+-        if (Ci.nsICommandLineHandler && iid.equals(Ci.nsICommandLineHandler))
+-            return this;
+-
+-        throw Cr.NS_ERROR_NO_INTERFACE;
+-    },
++    QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+ 
+     /* nsICommandLineHandler */
+     handle(cmdLine)
+     {
+         var uri;
+         try
+         {
+             uri = cmdLine.handleFlagWithParam("chat", false);
+@@ -152,27 +135,18 @@ const CommandLineFactory =
+ 
+ function ProcessHandler()
+ {
+ }
+ 
+ ProcessHandler.prototype =
+ {
+     /* nsISupports */
+-    QueryInterface(iid)
+-    {
+-        if (iid.equals(Ci.nsISupports) ||
+-            iid.equals(Ci.nsIObserver) ||
+-            iid.equals(Ci.nsIMessageListener))
+-        {
+-            return this;
+-        }
+-
+-        throw Cr.NS_ERROR_NO_INTERFACE;
+-    },
++    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
++                                           Ci.nsIMessageListener]),
+ 
+     /* nsIObserver */
+     observe(subject, topic, data)
+     {
+         if (topic !== "profile-after-change")
+             return;
+ 
+         var ppmm;
+@@ -211,88 +185,41 @@ const StartupFactory =
+             throw Cr.NS_ERROR_NO_INTERFACE;
+ 
+         // startup:
+         return new ProcessHandler();
+     },
+ };
+ 
+ 
+-const ChatZillaModule =
+-{
+-    registerSelf(compMgr, fileSpec, location, type)
+-    {
+-        compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
+-        const catman = Cc[CATMAN_CONTRACTID].getService(Ci.nsICategoryManager);
+-
+-        debug("*** Registering -chat handler.\n");
+-        catman.addCategoryEntry("command-line-argument-handlers",
+-                                "chatzilla command line handler",
+-                                CLINE_SERVICE_CONTRACTID, true, true);
+-        catman.addCategoryEntry("command-line-handler",
+-                                "m-irc",
+-                                CLINE_SERVICE_CONTRACTID, true, true);
+-        debug("*** Registering done.\n");
+-    },
+-
+-    unregisterSelf(compMgr, fileSpec, location)
+-    {
+-        compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
+-        const catman = Cc[CATMAN_CONTRACTID].getService(Ci.nsICategoryManager);
+-        catman.deleteCategoryEntry("command-line-argument-handlers",
+-                                   "chatzilla command line handler", true);
+-        catman.deleteCategoryEntry("command-line-handler",
+-                                   "m-irc", true);
+-    },
+-
+-    getClassObject(compMgr, cid, iid)
+-    {
+-        // Checking if we're disabled in the Chrome Registry.
+-        var rv;
+-        try
+-        {
+-            const rdfSvc = Cc[RDFS_CONTRACTID].getService(Ci.nsIRDFService);
+-            const rdfDS = rdfSvc.GetDataSource("rdf:chrome");
+-            const resSelf = rdfSvc.GetResource("urn:mozilla:package:chatzilla");
+-            const resDisabled = rdfSvc.GetResource("http://www.mozilla.org/rdf/chrome#disabled");
+-            rv = rdfDS.GetTarget(resSelf, resDisabled, true);
+-        }
+-        catch (e)
+-        {
+-        }
+-        if (rv)
+-            throw Cr.NS_ERROR_NO_INTERFACE;
+-
+-        if (cid.equals(CLINE_SERVICE_CID))
+-            return CommandLineFactory;
+-
+-        if (cid.equals(IRCPROT_HANDLER_CID))
+-            return IRCProtocolHandlerFactory;
+-
+-        if (cid.equals(IRCSPROT_HANDLER_CID))
+-            return IRCSProtocolHandlerFactory;
+-
+-        if (cid.equals(STARTUP_CID))
+-            return StartupFactory;
+-
+-        if (!iid.equals(Ci.nsIFactory))
+-            throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+-
++/* entrypoint */
++function NSGetFactory(cid)
++{
++    // Checking if we're disabled in the Chrome Registry.
++    var rv;
++    try
++    {
++        const rdfSvc = Cc[RDFS_CONTRACTID].getService(Ci.nsIRDFService);
++        const rdfDS = rdfSvc.GetDataSource("rdf:chrome");
++        const resSelf = rdfSvc.GetResource("urn:mozilla:package:chatzilla");
++        const resDisabled = rdfSvc.GetResource("http://www.mozilla.org/rdf/chrome#disabled");
++        rv = rdfDS.GetTarget(resSelf, resDisabled, true);
++    }
++    catch (e)
++    {
++    }
++    if (rv)
+         throw Cr.NS_ERROR_NO_INTERFACE;
+-    },
+-
+-    canUnload(compMgr)
+-    {
+-        return true;
+-    },
+-};
+-
+-
+-/* entrypoint */
+-function NSGetModule(compMgr, fileSpec)
+-{
+-    return ChatZillaModule;
+-}
+-
+-function NSGetFactory(cid)
+-{
+-    return ChatZillaModule.getClassObject(null, cid, null);
+-}
++
++    if (cid.equals(CLINE_SERVICE_CID))
++        return CommandLineFactory;
++
++    if (cid.equals(STARTUP_CID))
++        return StartupFactory;
++
++    if (cid.equals(IRCPROT_HANDLER_CID))
++        return IRCProtocolHandlerFactory;
++
++    if (cid.equals(IRCSPROT_HANDLER_CID))
++        return IRCSProtocolHandlerFactory;
++
++    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
++}

+ 479 - 44
comm-release/patches/TOP-1378089-4-WIP-bookmarks-25320.patch

@@ -1,7 +1,7 @@
 # HG changeset patch
 # User Frank-Rainer Grahl <frgrahl@gmx.net>
 # Date 1525689689 -7200
-# Parent  4eb7f5758f2f19577c6ad1fed4aa27b8f704b508
+# Parent  566b84dfe7fc4bd5e8f7090dc30fbb0679f858e5
 Bug 1378089 - Part 4. Replace the Bookmarks Manager and the History viewer with the Firefox library. r=IanN a=IanN
 From Fx 56 up to Bug 1383138 in m-c to Fx 60 up to Bug 1423896 "Make the new All Bookmarks folder query only..."
 
@@ -622,7 +622,7 @@ diff --git a/suite/browser/navigatorOverlay.xul b/suite/browser/navigatorOverlay
 diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places/PlacesUIUtils.jsm
 --- a/suite/components/places/PlacesUIUtils.jsm
 +++ b/suite/components/places/PlacesUIUtils.jsm
-@@ -3,37 +3,42 @@
+@@ -3,77 +3,45 @@
   * 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/. */
  
@@ -664,12 +664,52 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
  // copied from utilityOverlay.js
  const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
  
- // This function isn't public both because it's synchronous and because it is
- // going to be removed in bug 1072833.
- function IsLivemark(aItemId) {
-   // Since this check may be done on each dragover event, it's worth maintaining
-   // a cache.
-@@ -250,17 +255,17 @@ var PlacesUIUtils = {
+-// This function isn't public both because it's synchronous and because it is
+-// going to be removed in bug 1072833.
+-function IsLivemark(aItemId) {
+-  // Since this check may be done on each dragover event, it's worth maintaining
+-  // a cache.
+-  let self = IsLivemark;
+-  if (!("ids" in self)) {
+-    const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI;
+-
+-    let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO);
+-    self.ids = new Set(idsVec);
+-
+-    let obs = Object.freeze({
+-      QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver),
+-
+-      onItemAnnotationSet(itemId, annoName) {
+-        if (annoName == LIVEMARK_ANNO)
+-          self.ids.add(itemId);
+-      },
+-
+-      onItemAnnotationRemoved(itemId, annoName) {
+-        // If annoName is set to an empty string, the item is gone.
+-        if (annoName == LIVEMARK_ANNO || annoName == "")
+-          self.ids.delete(itemId);
+-      },
+-
+-      onPageAnnotationSet() { },
+-      onPageAnnotationRemoved() { },
+-    });
+-    PlacesUtils.annotations.addObserver(obs);
+-    PlacesUtils.registerShutdownFunction(() => {
+-      PlacesUtils.annotations.removeObserver(obs);
+-    });
+-  }
+-  return self.ids.has(aItemId);
+-}
+-
+ var InternalFaviconLoader = {
+   /**
+    * This gets called for every inner window that is destroyed.
+    * In the parent process, we process the destruction ourselves. In the child process,
+    * we notify the parent which will then process it based on that message.
+    */
+   observe(subject, topic, data) {
+     let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+@@ -250,17 +218,17 @@ var PlacesUIUtils = {
  
    /**
     * Makes a URI from a spec, and do fixup
@@ -688,7 +728,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
  
    /**
     * Get a localized plural string for the specified key name and numeric value
-@@ -332,17 +337,17 @@ var PlacesUIUtils = {
+@@ -332,17 +300,17 @@ var PlacesUIUtils = {
      let annos = [];
      if (aData.annos) {
        annos = aData.annos.filter(function(aAnno) {
@@ -707,7 +747,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
     * Gets a transaction for copying (recursively nesting to include children)
     * a folder (or container) and its contents from one folder to another.
     *
-@@ -406,17 +411,17 @@ var PlacesUIUtils = {
+@@ -406,17 +374,17 @@ var PlacesUIUtils = {
      if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder.
        let transactions = [];
        if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
@@ -726,7 +766,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
  
      if (aData.livemark && aData.annos) { // Copying a livemark.
        return this._getLivemarkCopyTransaction(aData, aContainer, aIndex);
-@@ -470,19 +475,19 @@ var PlacesUIUtils = {
+@@ -470,19 +438,19 @@ var PlacesUIUtils = {
        throw new Error("node is not a livemark");
      }
  
@@ -748,7 +788,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
      return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
                                                 aContainer, aIndex, annos);
    },
-@@ -530,17 +535,17 @@ var PlacesUIUtils = {
+@@ -530,17 +498,17 @@ var PlacesUIUtils = {
          // Otherwise move the item.
          return new PlacesMoveItemTransaction(data.id, container, index);
        default:
@@ -767,7 +807,27 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
  
    /**
     * ********* PlacesTransactions version of the function defined above ********
-@@ -623,45 +628,37 @@ var PlacesUIUtils = {
+@@ -561,17 +529,18 @@ var PlacesUIUtils = {
+    *
+    * @return  a Places Transaction that can be transacted for performing the
+    *          move/insert command.
+    */
+   getTransactionForData(aData, aType, aNewParentGuid, aIndex, aCopy) {
+     if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+       throw new Error(`Unsupported '${aData.type}' data type`);
+ 
+-    if ("itemGuid" in aData) {
++    if ("itemGuid" in aData && "instanceId" in aData &&
++        aData.instanceId == PlacesUtils.instanceId) {
+       if (!this.PLACES_FLAVORS.includes(aData.type))
+         throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
+ 
+       let info = { guid: aData.itemGuid,
+                    newParentGuid: aNewParentGuid,
+                    newIndex: aIndex };
+       if (aCopy) {
+         info.excludingAnnotation = "Places/SmartBookmark";
+@@ -623,45 +592,37 @@ var PlacesUIUtils = {
                      "chrome://communicator/content/places/bookmarkProperties2.xul" :
                      "chrome://communicator/content/places/bookmarkProperties.xul";
  
@@ -823,7 +883,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
     */
    loadFavicon(browser, principal, uri) {
      if (gInContentProcess) {
-@@ -743,23 +740,22 @@ var PlacesUIUtils = {
+@@ -743,23 +704,22 @@ var PlacesUIUtils = {
     *        a window on which a potential error alert is shown on.
     * @return true if it's safe to open the node in the browser, false otherwise.
     *
@@ -851,7 +911,120 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
      return true;
    },
  
-@@ -889,17 +885,17 @@ var PlacesUIUtils = {
+@@ -796,19 +756,21 @@ var PlacesUIUtils = {
+   },
+ 
+   /**
+    * Check whether or not the given node represents a removable entry (either in
+    * history or in bookmarks).
+    *
+    * @param aNode
+    *        a node, except the root node of a query.
++   * @param aView
++   *        The view originating the request.
+    * @return true if the aNode represents a removable entry, false otherwise.
+    */
+-  canUserRemove(aNode) {
++  canUserRemove(aNode, aView) {
+     let parentNode = aNode.parent;
+     if (!parentNode) {
+       // canUserRemove doesn't accept root nodes.
+       return false;
+     }
+ 
+     // If it's not a bookmark, we can remove it unless it's a child of a
+     // livemark.
+@@ -819,87 +781,83 @@ var PlacesUIUtils = {
+       return !PlacesUtils.nodeIsFolder(parentNode);
+     }
+ 
+     // Generally it's always possible to remove children of a query.
+     if (PlacesUtils.nodeIsQuery(parentNode))
+       return true;
+ 
+     // Otherwise it has to be a child of an editable folder.
+-    return !this.isContentsReadOnly(parentNode);
++    return !this.isFolderReadOnly(parentNode, aView);
+   },
+ 
+   /**
+    * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
+    * TO GUIDS IS COMPLETE (BUG 1071511).
+    *
+-   * Check whether or not the given node or item-id points to a folder which
++   * Check whether or not the given Places node points to a folder which
+    * should not be modified by the user (i.e. its children should be unremovable
+    * and unmovable, new children should be disallowed, etc).
+    * These semantics are not inherited, meaning that read-only folder may
+    * contain editable items (for instance, the places root is read-only, but all
+    * of its direct children aren't).
+    *
+-   * You should only pass folder item ids or folder nodes for aNodeOrItemId.
+-   * While this is only enforced for the node case (if an item id of a separator
+-   * or a bookmark is passed, false is returned), it's considered the caller's
+-   * job to ensure that it checks a folder.
+-   * Also note that folder-shortcuts should only be passed as result nodes.
+-   * Otherwise they are just treated as bookmarks (i.e. false is returned).
++   * You should only pass folder nodes.
+    *
+-   * @param aNodeOrItemId
+-   *        any item id or result node.
+-   * @throws if aNodeOrItemId is neither an item id nor a folder result node.
++   * @param placesNode
++   *        any folder result node.
++   * @param view
++   *        The view originating the request.
++   * @throws if placesNode is not a folder result node or views is invalid.
+    * @note livemark "folders" are considered read-only (but see bug 1072833).
+-   * @return true if aItemId points to a read-only folder, false otherwise.
++   * @return true if placesNode is a read-only folder, false otherwise.
+    */
+-  isContentsReadOnly(aNodeOrItemId) {
+-    let itemId;
+-    if (typeof(aNodeOrItemId) == "number") {
+-      itemId = aNodeOrItemId;
+-    } else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) {
+-      itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId);
+-    } else {
+-      throw new Error("invalid value for aNodeOrItemId");
++  isFolderReadOnly(placesNode, view) {
++    if (typeof placesNode != "object" || !PlacesUtils.nodeIsFolder(placesNode)) {
++      throw new Error("invalid value for placesNode");
+     }
+-
+-    if (itemId == PlacesUtils.placesRootId || IsLivemark(itemId))
++    if (!view || typeof view != "object") {
++      throw new Error("invalid value for aView");
++    }
++    let itemId = PlacesUtils.getConcreteItemId(placesNode);
++    if (itemId == PlacesUtils.placesRootId ||
++        view.controller.hasCachedLivemarkInfo(placesNode))
+       return true;
+ 
+     // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter
+     // performing at least a synchronous DB query (and on its very first call
+     // in a fresh profile, it also creates the entire structure).
+     // Therefore we don't want to this function, which is called very often by
+     // isCommandEnabled, to ever be the one that invokes it first, especially
+     // because isCommandEnabled may be called way before the left pane folder is
+     // even created (for example, if the user only uses the bookmarks menu or
+     // toolbar for managing bookmarks).  To do so, we avoid comparing to those
+     // special folder if the lazy getter is still in place.  This is safe merely
+     // because the only way to access the left pane contents goes through
+     // "resolving" the leftPaneFolderId getter.
+-    if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId"))
++    if (typeof Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").get == "function") {
+       return false;
+-
++    }
+     return itemId == this.leftPaneFolderId ||
+            itemId == this.allBookmarksFolderId;
+   },
+ 
+   /** aItemsToOpen needs to be an array of objects of the form:
+     * {uri: string, isBookmark: boolean}
+     */
+   _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) {
      if (!aItemsToOpen.length)
        return;
  
@@ -870,7 +1043,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
        if (skipMarking) {
          continue;
        }
-@@ -912,18 +908,18 @@ var PlacesUIUtils = {
+@@ -912,18 +870,18 @@ var PlacesUIUtils = {
  
      // whereToOpenLink doesn't return "window" when there's no browser window
      // open (Bug 630255).
@@ -891,7 +1064,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
      }
  
      var loadInBackground = where == "tabshifted";
-@@ -1028,17 +1024,17 @@ var PlacesUIUtils = {
+@@ -1028,17 +986,17 @@ var PlacesUIUtils = {
        }
  
        // Check whether the node is a bookmark which should be opened as
@@ -910,7 +1083,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
        // }
  
        aWindow.openUILinkIn(aNode.uri, aWhere, {
-@@ -1063,19 +1059,19 @@ var PlacesUIUtils = {
+@@ -1063,19 +1021,19 @@ var PlacesUIUtils = {
    guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) {
      return aUrlString.substr(0, aUrlString.indexOf(":"));
    },
@@ -932,7 +1105,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
          } else {
            title = host + (fileName ?
                             (host ? "/" + this.ellipsis + "/" : "") + fileName :
-@@ -1246,17 +1242,17 @@ var PlacesUIUtils = {
+@@ -1246,17 +1204,17 @@ var PlacesUIUtils = {
        }
      }
  
@@ -951,7 +1124,7 @@ diff --git a/suite/components/places/PlacesUIUtils.jsm b/suite/components/places
          // We should never backup this, since it changes between profiles.
          as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
                               0, as.EXPIRE_NEVER);
-@@ -1521,219 +1517,65 @@ var PlacesUIUtils = {
+@@ -1521,219 +1479,65 @@ var PlacesUIUtils = {
     *
     * @see promiseNodeLikeFromFetchInfo above and Bookmarks.fetch in Bookmarks.jsm.
     */
@@ -1514,13 +1687,17 @@ diff --git a/suite/components/places/content/bookmarkProperties.js b/suite/compo
 diff --git a/suite/components/places/content/browserPlacesViews.js b/suite/components/places/content/browserPlacesViews.js
 --- a/suite/components/places/content/browserPlacesViews.js
 +++ b/suite/components/places/content/browserPlacesViews.js
-@@ -213,18 +213,21 @@ PlacesViewBase.prototype = {
+@@ -210,21 +210,24 @@ PlacesViewBase.prototype = {
+           tagName = container.title;
+           // TODO (Bug 1160193): properly support dropping on a tag root.
+           if (!tagName)
              return null;
          }
        }
      }
  
-     if (PlacesControllerDragHelper.disallowInsertion(container))
+-    if (PlacesControllerDragHelper.disallowInsertion(container))
++    if (PlacesControllerDragHelper.disallowInsertion(container, this))
        return null;
  
 -    return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
@@ -1538,7 +1715,17 @@ diff --git a/suite/components/places/content/browserPlacesViews.js b/suite/compo
      return this.controller.buildContextMenu(aPopup);
    },
  
-@@ -1413,69 +1416,90 @@ PlacesToolbar.prototype = {
+@@ -1404,78 +1407,99 @@ PlacesToolbar.prototype = {
+ 
+     let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
+     let elt = aEvent.target;
+     if (elt._placesNode && elt != this._rootElt &&
+         elt.localName != "menupopup") {
+       let eltRect = elt.getBoundingClientRect();
+       let eltIndex = Array.prototype.indexOf.call(this._rootElt.childNodes, elt);
+       if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
+-          !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) {
++          !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this)) {
          // This is a folder.
          // If we are in the middle of it, drop inside it.
          // Otherwise, drop before it, with regards to RTL mode.
@@ -1757,7 +1944,23 @@ diff --git a/suite/components/places/content/controller.js b/suite/components/pl
          // cutting.
          if (node.itemId == -1 ||
              (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
-@@ -209,27 +205,19 @@ PlacesController.prototype = {
+@@ -194,42 +190,34 @@ PlacesController.prototype = {
+       // Livemark containers
+       let selectedNode = this._view.selectedNode;
+       return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
+     }
+     case "placesCmd_sortBy:name": {
+       let selectedNode = this._view.selectedNode;
+       return selectedNode &&
+              PlacesUtils.nodeIsFolder(selectedNode) &&
+-             !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
++             !PlacesUIUtils.isFolderReadOnly(selectedNode, this._view) &&
+              this._view.result.sortingMode ==
+                  Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+     }
+     case "placesCmd_createBookmark":
+       var node = this._view.selectedNode;
+       return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
      default:
        return false;
      }
@@ -1808,6 +2011,25 @@ diff --git a/suite/components/places/content/controller.js b/suite/components/pl
        this.showBookmarkPropertiesForSelection();
        break;
      case "placesCmd_reload":
+@@ -323,17 +311,17 @@ PlacesController.prototype = {
+ 
+     for (var j = 0; j < ranges.length; j++) {
+       var nodes = ranges[j];
+       for (var i = 0; i < nodes.length; ++i) {
+         // Disallow removing the view's root node
+         if (nodes[i] == root)
+           return false;
+ 
+-        if (!PlacesUIUtils.canUserRemove(nodes[i]))
++        if (!PlacesUIUtils.canUserRemove(nodes[i], this._view))
+           return false;
+       }
+     }
+ 
+     return true;
+   },
+ 
+   /**
 @@ -361,18 +349,18 @@ PlacesController.prototype = {
      var hasPlacesData =
        clipboard.hasDataMatchingFlavors(flavors, flavors.length,
@@ -2289,10 +2511,82 @@ diff --git a/suite/components/places/content/controller.js b/suite/components/pl
      }
  
      if (itemsToSelect.length > 0)
-@@ -1526,109 +1444,108 @@ var PlacesControllerDragHelper = {
+@@ -1482,167 +1400,185 @@ var PlacesControllerDragHelper = {
+       }
+     }
+     return true;
+   },
+ 
+   /**
+    * Determines if an unwrapped node can be moved.
+    *
+-   * @param   aUnwrappedNode
+-   *          A node unwrapped by PlacesUtils.unwrapNodes().
++   * @param unwrappedNode
++   *        A node unwrapped by PlacesUtils.unwrapNodes().
+    * @return True if the node can be moved, false otherwise.
+    */
+-  canMoveUnwrappedNode(aUnwrappedNode) {
+-    return aUnwrappedNode.id > 0 &&
+-           !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
+-           (!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
+-           aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
+-           aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
++  canMoveUnwrappedNode(unwrappedNode) {
++    if (unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
++      return false;
++    }
++    let parentId = unwrappedNode.parent;
++    if (parentId <= 0 ||
++        parentId == PlacesUtils.placesRootId ||
++        parentId == PlacesUtils.tagsFolderId ||
++        unwrappedNode.grandParentId == PlacesUtils.tagsFolderId) {
++      return false;
++    }
++    // leftPaneFolderId and allBookmarksFolderId are lazy getters running
++    // at least a synchronous DB query. Therefore we don't want to invoke
++    // them first, especially because isCommandEnabled may be called way
++    // before the left pane folder is even necessary.
++    if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
++        (parentId == PlacesUIUtils.leftPaneFolderId ||
++          parentId == PlacesUIUtils.allBookmarksFolderId)) {
++      return false;
++    }
++    return true;
+   },
+ 
+   /**
+    * Determines if a node can be moved.
+    *
+    * @param   aNode
+    *          A nsINavHistoryResultNode node.
++   * @param   aView
++   *          The view originating the request
+    * @param   [optional] aDOMNode
+    *          A XUL DOM node.
+    * @return True if the node can be moved, false otherwise.
+    */
+-  canMoveNode(aNode, aDOMNode) {
++  canMoveNode(aNode, aView, aDOMNode) {
+     // Only bookmark items are movable.
+     if (aNode.itemId == -1)
+       return false;
+ 
+     let parentNode = aNode.parent;
+     if (!parentNode) {
+       // Normally parentless places nodes can not be moved,
+       // but simulated bookmarked URI nodes are special.
+       return !!aDOMNode &&
+              aDOMNode.hasAttribute("simulated-places-node") &&
+              PlacesUtils.nodeIsBookmark(aNode);
+     }
+ 
+     // Once tags and bookmarked are divorced, the tag-query check should be
      // removed.
-     return !(PlacesUtils.nodeIsFolder(parentNode) &&
-              PlacesUIUtils.isContentsReadOnly(parentNode)) &&
+-    return !(PlacesUtils.nodeIsFolder(parentNode) &&
+-             PlacesUIUtils.isContentsReadOnly(parentNode)) &&
++    return PlacesUtils.nodeIsFolder(parentNode) &&
++           !PlacesUIUtils.isFolderReadOnly(parentNode, aView) &&
             !PlacesUtils.nodeIsTagQuery(parentNode);
    },
  
@@ -2423,8 +2717,26 @@ diff --git a/suite/components/places/content/controller.js b/suite/components/pl
     * Checks if we can insert into a container.
     * @param   aContainer
     *          The container were we are want to drop
++   * @param   aView
++   *          The view generating the request
     */
-   disallowInsertion(aContainer) {
+-  disallowInsertion(aContainer) {
++  disallowInsertion(aContainer, aView) {
+     if (!aContainer)
+       throw new Error("empty container");
+     // Allow dropping into Tag containers and editable folders.
+     return !PlacesUtils.nodeIsTagQuery(aContainer) &&
+            (!PlacesUtils.nodeIsFolder(aContainer) ||
+-            PlacesUIUtils.isContentsReadOnly(aContainer));
++            PlacesUIUtils.isFolderReadOnly(aContainer, aView));
+   }
+ };
+ 
+ 
+ XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
+                                    "@mozilla.org/widget/dragservice;1",
+                                    "nsIDragService");
+ 
 diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/components/places/content/editBookmarkOverlay.js
 --- a/suite/components/places/content/editBookmarkOverlay.js
 +++ b/suite/components/places/content/editBookmarkOverlay.js
@@ -2448,7 +2760,34 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
      let itemId = node ? node.itemId : -1;
      let itemGuid = node ? PlacesUtils.getConcreteItemGuid(node) : null;
      let isItem = itemId != -1;
-@@ -533,30 +532,16 @@ var gEditItemOverlay = {
+@@ -51,18 +50,24 @@ var gEditItemOverlay = {
+     let parentId = -1;
+     let parentGuid = null;
+ 
+     if (node && isItem) {
+       if (!node.parent || (node.parent.itemId > 0 && !node.parent.bookmarkGuid)) {
+         throw new Error("Cannot use an incomplete node to initialize the edit bookmark panel");
+       }
+       let parent = node.parent;
+-      isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) ||
+-                          PlacesUIUtils.isContentsReadOnly(parent);
++      isParentReadOnly = !PlacesUtils.nodeIsFolder(parent);
++      if (!isParentReadOnly) {
++        let folderId = PlacesUtils.getConcreteItemId(parent);
++        isParentReadOnly = folderId == PlacesUtils.placesRootId ||
++                           (!("get" in Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId")) &&
++                            (folderId == PlacesUIUtils.leftPaneFolderId ||
++                             folderId == PlacesUIUtils.allBookmarksFolderId));
++      }
+       parentId = parent.itemId;
+       parentGuid = parent.bookmarkGuid;
+     }
+ 
+     let focusedElement = aInitInfo.focusedElement;
+     let onPanelReady = aInitInfo.onPanelReady;
+ 
+     return this._paneInfo = { itemId, itemGuid, parentId, parentGuid, isItem,
+@@ -533,30 +538,16 @@ var gEditItemOverlay = {
    },
  
    // Adds and removes tags for one or more uris.
@@ -2479,7 +2818,7 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
        if (newTags.length > 0) {
          await PlacesTransactions.Tag({ urls: aURIs, tags: newTags })
                                  .transact();
-@@ -623,22 +608,16 @@ var gEditItemOverlay = {
+@@ -623,22 +614,16 @@ var gEditItemOverlay = {
  
      // Here we update either the item title or its cached static title
      let newTitle = this._namePicker.value;
@@ -2502,7 +2841,7 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
      }
    },
  
-@@ -646,22 +625,16 @@ var gEditItemOverlay = {
+@@ -646,22 +631,16 @@ var gEditItemOverlay = {
      if (this.readOnly || !this._paneInfo.isItem)
        return;
  
@@ -2525,7 +2864,7 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
  
    onLocationFieldChange() {
      if (this.readOnly || !this._paneInfo.isBookmark)
-@@ -673,62 +646,42 @@ var gEditItemOverlay = {
+@@ -673,62 +652,42 @@ var gEditItemOverlay = {
      } catch (ex) {
        // TODO: Bug 1089141 - Provide some feedback about the invalid url.
        return;
@@ -2588,7 +2927,7 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
    toggleFolderTreeVisibility() {
      var expander = this._element("foldersExpander");
      var folderTreeRow = this._element("folderTreeRow");
-@@ -806,26 +759,19 @@ var gEditItemOverlay = {
+@@ -806,26 +765,19 @@ var gEditItemOverlay = {
        setTimeout(() => this.toggleFolderTreeVisibility(), 100);
        return;
      }
@@ -2618,7 +2957,7 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
            containerId != PlacesUtils.bookmarksMenuFolderId) {
          this._markFolderAsRecentlyUsed(containerId)
              .catch(Cu.reportError);
-@@ -862,40 +808,16 @@ var gEditItemOverlay = {
+@@ -862,40 +814,16 @@ var gEditItemOverlay = {
        return;
  
      var folderItem = this._getFolderMenuItem(folderId, selectedNode.title);
@@ -2659,7 +2998,7 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
        guids.push(guid);
      }
      if (guids.length > 0) {
-@@ -1001,31 +923,27 @@ var gEditItemOverlay = {
+@@ -1001,31 +929,27 @@ var gEditItemOverlay = {
                 .filter(tag => tag.length > 0); // Kill empty tags.
    },
  
@@ -2701,7 +3040,26 @@ diff --git a/suite/components/places/content/editBookmarkOverlay.js b/suite/comp
 diff --git a/suite/components/places/content/menu.xml b/suite/components/places/content/menu.xml
 --- a/suite/components/places/content/menu.xml
 +++ b/suite/components/places/content/menu.xml
-@@ -85,20 +85,20 @@
+@@ -64,17 +64,17 @@
+            dragging over this popup insertion point -->
+       <method name="_getDropPoint">
+         <parameter name="aEvent"/>
+           <body><![CDATA[
+             // Can't drop if the menu isn't a folder
+             let resultNode = this._placesNode;
+ 
+             if (!PlacesUtils.nodeIsFolder(resultNode) ||
+-                PlacesControllerDragHelper.disallowInsertion(resultNode)) {
++                PlacesControllerDragHelper.disallowInsertion(resultNode, this._rootView)) {
+               return null;
+             }
+ 
+             var dropPoint = { ip: null, folderElt: null };
+ 
+             // The element we are dragging over
+             let elt = aEvent.target;
+             if (elt.localName == "menupopup")
+@@ -85,75 +85,78 @@
              let eventY = aEvent.layerY + (scrollbox.boxObject.y - this.boxObject.y);
              let scrollboxOffset = scrollbox.scrollBoxObject.y -
                                    (scrollbox.boxObject.y - this.boxObject.y);
@@ -2726,11 +3084,14 @@ diff --git a/suite/components/places/content/menu.xml b/suite/components/places/
                if (isMenu && elt.lastChild &&
                    elt.lastChild.hasAttribute("placespopup"))
                  dropPoint.folderElt = elt;
-@@ -108,52 +108,55 @@
+               return dropPoint;
+             }
+ 
              let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
                              elt._placesNode.title : null;
              if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
-                  !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) ||
+-                 !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) ||
++                 !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this._rootView)) ||
                  PlacesUtils.nodeIsTagQuery(elt._placesNode)) {
                // This is a folder or a tag container.
                if (eventY - eltY < eltHeight * 0.20) {
@@ -2805,6 +3166,25 @@ diff --git a/suite/components/places/content/menu.xml b/suite/components/places/
             when the mouse drags off.  The overFolder object manages opening and
             closing of folders when the mouse hovers. -->
        <field name="_overFolder"><![CDATA[({
+@@ -342,17 +345,17 @@
+         let elt = event.target;
+         if (!elt._placesNode)
+           return;
+ 
+         let draggedElt = elt._placesNode;
+ 
+         // Force a copy action if parent node is a query or we are dragging a
+         // not-removable node.
+-        if (!PlacesControllerDragHelper.canMoveNode(draggedElt, elt))
++        if (!PlacesControllerDragHelper.canMoveNode(draggedElt, this._rootView, elt))
+           event.dataTransfer.effectAllowed = "copyLink";
+ 
+         // Activate the view and cache the dragged element.
+         this._rootView._draggedElt = draggedElt;
+         this._rootView.controller.setDataTransfer(event);
+         this.setAttribute("dragstart", "true");
+         event.stopPropagation();
+       ]]></handler>
 diff --git a/suite/components/places/content/places.js b/suite/components/places/content/places.js
 --- a/suite/components/places/content/places.js
 +++ b/suite/components/places/content/places.js
@@ -2849,7 +3229,23 @@ diff --git a/suite/components/places/content/tree.xml b/suite/components/places/
              if (resultview.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
                // If the last selected item is an open container, append _into_
                // it, rather than insert adjacent to it.
-@@ -510,17 +510,17 @@
+@@ -495,54 +495,55 @@
+               container = lastSelected.parent;
+ 
+               // See comment in the treeView.js's copy of this method
+               if (!container || !container.containerOpen)
+                 return null;
+ 
+               // Avoid the potentially expensive call to getChildIndex
+               // if we know this container doesn't allow insertion
+-              if (PlacesControllerDragHelper.disallowInsertion(container))
++              if (PlacesControllerDragHelper.disallowInsertion(container, this))
+                 return null;
+ 
+               var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
+               if (queryOptions.sortingMode !=
+                     Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+                 // If we are within a sorted view, insert at the end
                  index = -1;
                } else if (queryOptions.excludeItems ||
                           queryOptions.excludeQueries ||
@@ -2867,8 +3263,10 @@ diff --git a/suite/components/places/content/tree.xml b/suite/components/places/
              }
            }
  
-           if (PlacesControllerDragHelper.disallowInsertion(container))
-@@ -529,20 +529,21 @@
+-          if (PlacesControllerDragHelper.disallowInsertion(container))
++          if (PlacesControllerDragHelper.disallowInsertion(container, this))
+             return null;
+ 
            // TODO (Bug 1160193): properly support dropping on a tag root.
            let tagName = null;
            if (PlacesUtils.nodeIsTagQuery(container)) {
@@ -2894,6 +3292,25 @@ diff --git a/suite/components/places/content/tree.xml b/suite/components/places/
          <body><![CDATA[
            this.view.selection.selectAll();
          ]]></body>
+@@ -727,17 +728,17 @@
+           if (!node.parent) {
+             event.preventDefault();
+             event.stopPropagation();
+             return;
+           }
+ 
+           // If this node is child of a readonly container (e.g. a livemark)
+           // or cannot be moved, we must force a copy.
+-          if (!PlacesControllerDragHelper.canMoveNode(node)) {
++          if (!PlacesControllerDragHelper.canMoveNode(node, this)) {
+             event.dataTransfer.effectAllowed = "copyLink";
+             break;
+           }
+         }
+ 
+         this._controller.setDataTransfer(event);
+         event.stopPropagation();
+       ]]></handler>
 diff --git a/suite/components/places/content/treeView.js b/suite/components/places/content/treeView.js
 --- a/suite/components/places/content/treeView.js
 +++ b/suite/components/places/content/treeView.js
@@ -2916,7 +3333,23 @@ diff --git a/suite/components/places/content/treeView.js b/suite/components/plac
          // If the last selected item is an open container, append _into_
          // it, rather than insert adjacent to it.
          container = lastSelected;
-@@ -1416,17 +1416,17 @@ PlacesTreeView.prototype = {
+@@ -1401,67 +1401,71 @@ PlacesTreeView.prototype = {
+         // container here.  However, we can simply bail out when this happens,
+         // because we would then be back here in less than a millisecond, when
+         // the container had been reopened.
+         if (!container || !container.containerOpen)
+           return null;
+ 
+         // Avoid the potentially expensive call to getChildIndex
+         // if we know this container doesn't allow insertion.
+-        if (PlacesControllerDragHelper.disallowInsertion(container))
++        if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+           return null;
+ 
+         let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+         if (queryOptions.sortingMode !=
+               Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+           // If we are within a sorted view, insert at the end.
            index = -1;
          } else if (queryOptions.excludeItems ||
                   queryOptions.excludeQueries ||
@@ -2934,8 +3367,10 @@ diff --git a/suite/components/places/content/treeView.js b/suite/components/plac
        }
      }
  
-     if (PlacesControllerDragHelper.disallowInsertion(container))
-@@ -1435,33 +1435,37 @@ PlacesTreeView.prototype = {
+-    if (PlacesControllerDragHelper.disallowInsertion(container))
++    if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+       return null;
+ 
      // TODO (Bug 1160193): properly support dropping on a tag root.
      let tagName = null;
      if (PlacesUtils.nodeIsTagQuery(container)) {

+ 2 - 0
comm-release/patches/series

@@ -2218,3 +2218,5 @@ TOP-1872623-cancelbookmark-25319.patch
 1925025-specificsearch-25320.patch
 1925033-focus-25320.patch
 1925037-switchToTabHavingURI-1_1-25320.patch
+1925871-irc-remove-NSGetModule-v1_1-25320.patch
+1925849-domi-cmdline-25320.patch

+ 636 - 0
mozilla-release/patches/1301622-58a1.patch

@@ -0,0 +1,636 @@
+
+# HG changeset patch
+# User Kit Cambridge <kit@yakshaving.ninja>
+# Date 1508197810 25200
+# Node ID 1126763734c00dad660e9032e3eb33fea6337dbb
+# Parent  9b16086decefc0df044098023536970cfbb225c0
+Bug 1301622 - Teach Places maintenance about Sync. r=mak
+
+This patch bumps the Sync change counter for moved children and changed
+types, extends the "GUID change" trigger to bump the change counter
+for old parents, and adds a trigger to insert tombstones for deleted
+synced items.
+
+MozReview-Commit-ID: 5jalCfy9AMk
+
+diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm
+--- a/toolkit/components/places/PlacesDBUtils.jsm
++++ b/toolkit/components/places/PlacesDBUtils.jsm
+@@ -279,16 +279,48 @@ var PlacesDBUtils = {
+         PlacesDBUtils.clearPendingTasks();
+         throw new Error("Unable to incrementally vacuum the favicons database " + ex);
+       });
+   },
+ 
+   async _getBoundCoherenceStatements() {
+     let cleanupStatements = [];
+ 
++    // Create triggers for updating Sync metadata. The "sync change" trigger
++    // bumps the parent's change counter when we update a GUID or move an item
++    // to a different folder, since Sync stores the list of child GUIDs on the
++    // parent. The "sync tombstone" trigger inserts tombstones for deleted
++    // synced bookmarks.
++    cleanupStatements.push({
++      query:
++      `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_sync_change_temp_trigger
++       AFTER UPDATE of guid, parent, position ON moz_bookmarks
++       FOR EACH ROW
++       BEGIN
++         UPDATE moz_bookmarks
++         SET syncChangeCounter = syncChangeCounter + 1
++         WHERE id = NEW.parent OR
++               (OLD.parent <> NEW.parent AND
++                id = OLD.parent);
++      END`,
++    });
++    cleanupStatements.push({
++      query:
++      `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_sync_tombstone_temp_trigger
++       AFTER DELETE ON moz_bookmarks
++       FOR EACH ROW WHEN OLD.guid NOT NULL AND
++                         OLD.syncStatus <> 1
++       BEGIN
++         INSERT INTO moz_bookmarks_deleted(guid, dateRemoved)
++         VALUES(OLD.guid, STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000);
++      END`,
++    });
++
++    // MAINTENANCE STATEMENTS SHOULD GO BELOW THIS POINT!
++
+     // MOZ_ANNO_ATTRIBUTES
+     // A.1 remove obsolete annotations from moz_annos.
+     // The 'weave0' idiom exploits character ordering (0 follows /) to
+     // efficiently select all annos with a 'weave/' prefix.
+     let deleteObsoleteAnnos = {
+       query:
+       `DELETE FROM moz_annos
+        WHERE type = 4
+@@ -503,17 +535,20 @@ var PlacesDBUtils = {
+     deleteEmptyTags.params.toolbarGuid = PlacesUtils.bookmarks.toolbarGuid;
+     deleteEmptyTags.params.unfiledGuid = PlacesUtils.bookmarks.unfiledGuid;
+     deleteEmptyTags.params.tagsGuid = PlacesUtils.bookmarks.tagsGuid;
+     cleanupStatements.push(deleteEmptyTags);
+ 
+     // D.4 move orphan items to unsorted folder
+     let fixOrphanItems = {
+       query:
+-      `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN (
++      `UPDATE moz_bookmarks SET
++         parent = :unsorted_folder,
++         syncChangeCounter = syncChangeCounter + 1
++       WHERE guid NOT IN (
+          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
+        ) AND id IN (
+          SELECT b.id FROM moz_bookmarks b
+          WHERE NOT EXISTS
+            (SELECT id FROM moz_bookmarks WHERE id = b.parent LIMIT 1)
+        )`,
+       params: {}
+     };
+@@ -526,17 +561,20 @@ var PlacesDBUtils = {
+     cleanupStatements.push(fixOrphanItems);
+ 
+     // D.6 fix wrong item types
+     //     Folders and separators should not have an fk.
+     //     If they have a valid fk convert them to bookmarks. Later in D.9 we
+     //     will move eventual children to unsorted bookmarks.
+     let fixBookmarksAsFolders = {
+       query:
+-      `UPDATE moz_bookmarks SET type = :bookmark_type WHERE guid NOT IN (
++      `UPDATE moz_bookmarks
++       SET type = :bookmark_type,
++           syncChangeCounter = syncChangeCounter + 1
++       WHERE guid NOT IN (
+          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
+        ) AND id IN (
+          SELECT id FROM moz_bookmarks b
+          WHERE type IN (:folder_type, :separator_type)
+            AND fk NOTNULL
+        )`,
+       params: {}
+     };
+@@ -550,17 +588,20 @@ var PlacesDBUtils = {
+     fixBookmarksAsFolders.params.tagsGuid = PlacesUtils.bookmarks.tagsGuid;
+     cleanupStatements.push(fixBookmarksAsFolders);
+ 
+     // D.7 fix wrong item types
+     //     Bookmarks should have an fk, if they don't have any, convert them to
+     //     folders.
+     let fixFoldersAsBookmarks = {
+       query:
+-      `UPDATE moz_bookmarks SET type = :folder_type WHERE guid NOT IN (
++      `UPDATE moz_bookmarks
++       SET type = :folder_type,
++           syncChangeCounter = syncChangeCounter + 1
++       WHERE guid NOT IN (
+          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
+        ) AND id IN (
+          SELECT id FROM moz_bookmarks b
+          WHERE type = :bookmark_type
+            AND fk IS NULL
+        )`,
+       params: {}
+     };
+@@ -573,17 +614,20 @@ var PlacesDBUtils = {
+     fixFoldersAsBookmarks.params.tagsGuid = PlacesUtils.bookmarks.tagsGuid;
+     cleanupStatements.push(fixFoldersAsBookmarks);
+ 
+     // D.9 fix wrong parents
+     //     Items cannot have separators or other bookmarks
+     //     as parent, if they have bad parent move them to unsorted bookmarks.
+     let fixInvalidParents = {
+       query:
+-      `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN (
++      `UPDATE moz_bookmarks SET
++         parent = :unsorted_folder,
++         syncChangeCounter = syncChangeCounter + 1
++       WHERE guid NOT IN (
+          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
+        ) AND id IN (
+          SELECT id FROM moz_bookmarks b
+          WHERE EXISTS
+            (SELECT id FROM moz_bookmarks WHERE id = b.parent
+              AND type IN (:bookmark_type, :separator_type)
+              LIMIT 1)
+        )`,
+@@ -834,40 +878,26 @@ var PlacesDBUtils = {
+              NOT IS_VALID_GUID(guid)`,
+       params: {
+         dateRemoved: PlacesUtils.toPRTime(new Date()),
+         syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+       },
+     });
+     cleanupStatements.push({
+       query:
+-      `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_fix_guids_temp_trigger
+-       AFTER UPDATE of guid ON moz_bookmarks
+-       FOR EACH ROW
+-       BEGIN
+-         UPDATE moz_bookmarks
+-         SET syncChangeCounter = syncChangeCounter + 1
+-         WHERE id = NEW.parent;
+-      END`,
+-    });
+-    cleanupStatements.push({
+-      query:
+       `UPDATE moz_bookmarks
+        SET guid = GENERATE_GUID(),
+            syncChangeCounter = syncChangeCounter + 1,
+            syncStatus = :syncStatus
+        WHERE guid IS NULL OR
+              NOT IS_VALID_GUID(guid)`,
+       params: {
+         syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+       },
+     });
+-    cleanupStatements.push({
+-      query: "DROP TRIGGER moz_bm_fix_guids_temp_trigger",
+-    });
+ 
+     // S.2 drop tombstones for bookmarks that aren't deleted.
+     cleanupStatements.push({
+       query:
+       `DELETE FROM moz_bookmarks_deleted
+        WHERE guid IN (SELECT guid FROM moz_bookmarks)`,
+     });
+ 
+@@ -884,16 +914,23 @@ var PlacesDBUtils = {
+              WHERE place_id = fk
+            ), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000)
+        WHERE dateAdded IS NULL OR
+              lastModified IS NULL`,
+     });
+ 
+     // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT!
+ 
++    cleanupStatements.push({
++      query: "DROP TRIGGER moz_bm_sync_change_temp_trigger",
++    });
++    cleanupStatements.push({
++      query: "DROP TRIGGER moz_bm_sync_tombstone_temp_trigger",
++    });
++
+     return cleanupStatements;
+   },
+ 
+   /**
+    * Tries to vacuum the database.
+    *
+    * Note: although this function isn't actually async, we keep it async to
+    * allow us to maintain a simple, consistent API for the tasks within this object.
+diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
++++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+@@ -32,16 +32,17 @@ function cleanDatabase() {
+   mDBConn.executeSimpleSQL("DELETE FROM moz_anno_attributes");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_annos");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_items_annos");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_inputhistory");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_keywords");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_icons");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_pages_w_icons");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE id > " + defaultBookmarksMaxId);
++  mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks_deleted");
+ }
+ 
+ function addPlace(aUrl, aFavicon, aGuid = PlacesUtils.history.makeGuid()) {
+   let href = new URL(aUrl || "http://www.mozilla.org").href;
+   let stmt = mDBConn.createStatement(
+     "INSERT INTO moz_places (url, url_hash, guid) VALUES (:url, hash(:url), :guid)");
+   stmt.params.url = href;
+   stmt.params.guid = aGuid;
+@@ -399,37 +400,48 @@ tests.push({
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "D.1",
+   desc: "Remove items without a valid place",
+ 
+   _validItemId: null,
+   _invalidItemId: null,
+-  _placeId: null,
++  _invalidSyncedItemId: null,
++  placeId: null,
+ 
+   setup() {
+     // Add a place to ensure place_id = 1 is valid
+     this.placeId = addPlace();
+     // Insert a valid bookmark
+     this._validItemId = addBookmark(this.placeId);
+     // Insert a bookmark with an invalid place
+     this._invalidItemId = addBookmark(1337);
++    // Insert a synced bookmark with an invalid place. We should write a
++    // tombstone when we remove it.
++    this._invalidSyncedItemId = addBookmark(1337, null, null, null, null, null,
++      "bookmarkAAAA", PlacesUtils.bookmarks.SYNC_STATUS.NORMAL);
+   },
+ 
+-  check() {
++  async check() {
+     // Check that valid bookmark is still there
+     let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id");
+     stmt.params.item_id = this._validItemId;
+     Assert.ok(stmt.executeStep());
+     stmt.reset();
+     // Check that invalid bookmark has been removed
+     stmt.params.item_id = this._invalidItemId;
+     Assert.ok(!stmt.executeStep());
++    stmt.reset();
++    stmt.params.item_id = this._invalidSyncedItemId;
++    Assert.ok(!stmt.executeStep());
+     stmt.finalize();
++
++    let tombstones = await PlacesTestUtils.fetchSyncTombstones();
++    Assert.deepEqual(tombstones.map(info => info.guid), ["bookmarkAAAA"]);
+   }
+ });
+ 
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "D.2",
+   desc: "Remove items that are not uri bookmarks from tag containers",
+@@ -523,48 +535,64 @@ tests.push({
+   desc: "Move orphan items to unsorted folder",
+ 
+   _orphanBookmarkId: null,
+   _orphanSeparatorId: null,
+   _orphanFolderId: null,
+   _bookmarkId: null,
+   _placeId: null,
+ 
+-  setup() {
++  async setup() {
+     // Add a place to ensure place_id = 1 is valid
+     this._placeId = addPlace();
+     // Insert an orphan bookmark
+     this._orphanBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, 8888);
+     // Insert an orphan separator
+     this._orphanSeparatorId = addBookmark(null, bs.TYPE_SEPARATOR, 8888);
+     // Insert a orphan folder
+     this._orphanFolderId = addBookmark(null, bs.TYPE_FOLDER, 8888);
+     // Create a child of the last created folder
+     this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._orphanFolderId);
+   },
+ 
+-  check() {
+-    // Check that bookmarks are now children of a real folder (unsorted)
+-    let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent");
+-    stmt.params.item_id = this._orphanBookmarkId;
+-    stmt.params.parent = bs.unfiledBookmarksFolder;
+-    Assert.ok(stmt.executeStep());
+-    stmt.reset();
+-    stmt.params.item_id = this._orphanSeparatorId;
+-    stmt.params.parent = bs.unfiledBookmarksFolder;
+-    Assert.ok(stmt.executeStep());
+-    stmt.reset();
+-    stmt.params.item_id = this._orphanFolderId;
+-    stmt.params.parent = bs.unfiledBookmarksFolder;
+-    Assert.ok(stmt.executeStep());
+-    stmt.reset();
+-    stmt.params.item_id = this._bookmarkId;
+-    stmt.params.parent = this._orphanFolderId;
+-    Assert.ok(stmt.executeStep());
+-    stmt.finalize();
++  async check() {
++    // Check that bookmarks are now children of a real folder (unfiled)
++    let expectedInfos = [{
++      id: this._orphanBookmarkId,
++      parent: bs.unfiledBookmarksFolder,
++      syncChangeCounter: 1,
++    }, {
++      id: this._orphanSeparatorId,
++      parent: bs.unfiledBookmarksFolder,
++      syncChangeCounter: 1,
++    }, {
++      id: this._orphanFolderId,
++      parent: bs.unfiledBookmarksFolder,
++      syncChangeCounter: 1,
++    }, {
++      id: this._bookmarkId,
++      parent: this._orphanFolderId,
++      syncChangeCounter: 0,
++    }, {
++      id: bs.unfiledBookmarksFolder,
++      parent: bs.placesRoot,
++      syncChangeCounter: 3,
++    }];
++    let db = await PlacesUtils.promiseDBConnection();
++    for (let { id, parent, syncChangeCounter } of expectedInfos) {
++      let rows = await db.executeCached(`
++        SELECT id, syncChangeCounter
++        FROM moz_bookmarks
++        WHERE id = :item_id AND parent = :parent`,
++        { item_id: id, parent });
++      Assert.equal(rows.length, 1);
++
++      let actualChangeCounter = rows[0].getResultByName("syncChangeCounter");
++      Assert.equal(actualChangeCounter, syncChangeCounter);
++    }
+   }
+ });
+ 
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "D.6",
+   desc: "Fix wrong item types | bookmarks",
+@@ -612,90 +640,113 @@ tests.push({
+     // Add a bookmark with a valid place id
+     this._validBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK);
+     // Add a bookmark with a null place id
+     this._invalidBookmarkId = addBookmark(null, bs.TYPE_BOOKMARK);
+   },
+ 
+   check() {
+     // Check valid bookmark
+-    let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type");
++    let stmt = mDBConn.createStatement(`
++      SELECT id, syncChangeCounter
++      FROM moz_bookmarks
++      WHERE id = :item_id AND type = :type`);
+     stmt.params.item_id = this._validBookmarkId;
+     stmt.params.type = bs.TYPE_BOOKMARK;
+     Assert.ok(stmt.executeStep());
++    Assert.equal(stmt.row.syncChangeCounter, 0);
+     stmt.reset();
+     // Check invalid bookmark has been converted to a folder
+     stmt.params.item_id = this._invalidBookmarkId;
+     stmt.params.type = bs.TYPE_FOLDER;
+     Assert.ok(stmt.executeStep());
++    Assert.equal(stmt.row.syncChangeCounter, 1);
+     stmt.finalize();
+   }
+ });
+ 
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "D.9",
+   desc: "Fix wrong parents",
+ 
+   _bookmarkId: null,
+   _separatorId: null,
+   _bookmarkId1: null,
+   _bookmarkId2: null,
+   _placeId: null,
+ 
+-  setup() {
++  async setup() {
+     // Add a place to ensure place_id = 1 is valid
+     this._placeId = addPlace();
+     // Insert a bookmark
+     this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK);
+     // Insert a separator
+     this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR);
+     // Create 3 children of these items
+     this._bookmarkId1 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._bookmarkId);
+     this._bookmarkId2 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._separatorId);
+   },
+ 
+-  check() {
+-    // Check that bookmarks are now children of a real folder (unsorted)
+-    let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent");
+-    stmt.params.item_id = this._bookmarkId1;
+-    stmt.params.parent = bs.unfiledBookmarksFolder;
+-    Assert.ok(stmt.executeStep());
+-    stmt.reset();
+-    stmt.params.item_id = this._bookmarkId2;
+-    stmt.params.parent = bs.unfiledBookmarksFolder;
+-    Assert.ok(stmt.executeStep());
+-    stmt.finalize();
++  async check() {
++    // Check that bookmarks are now children of a real folder (unfiled)
++    let expectedInfos = [{
++      id: this._bookmarkId1,
++      parent: bs.unfiledBookmarksFolder,
++      syncChangeCounter: 1,
++    }, {
++      id: this._bookmarkId2,
++      parent: bs.unfiledBookmarksFolder,
++      syncChangeCounter: 1,
++    }, {
++      id: bs.unfiledBookmarksFolder,
++      parent: bs.placesRoot,
++      syncChangeCounter: 2,
++    }];
++    let db = await PlacesUtils.promiseDBConnection();
++    for (let { id, parent, syncChangeCounter } of expectedInfos) {
++      let rows = await db.executeCached(`
++        SELECT id, syncChangeCounter
++        FROM moz_bookmarks
++        WHERE id = :item_id AND parent = :parent`,
++        { item_id: id, parent });
++      Assert.equal(rows.length, 1);
++
++      let actualChangeCounter = rows[0].getResultByName("syncChangeCounter");
++      Assert.equal(actualChangeCounter, syncChangeCounter);
++    }
+   }
+ });
+ 
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "D.10",
+   desc: "Recalculate positions",
+ 
+   _unfiledBookmarks: [],
+   _toolbarBookmarks: [],
+ 
+-  setup() {
++  async setup() {
+     const NUM_BOOKMARKS = 20;
+     bs.runInBatchMode({
+       runBatched(aUserData) {
+         // Add bookmarks to two folders to better perturbate the table.
+         for (let i = 0; i < NUM_BOOKMARKS; i++) {
+           bs.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                             NetUtil.newURI("http://example.com/"),
+-                            bs.DEFAULT_INDEX, "testbookmark");
++                            bs.DEFAULT_INDEX, "testbookmark", null,
++                            PlacesUtils.bookmarks.SOURCES.SYNC);
+         }
+         for (let i = 0; i < NUM_BOOKMARKS; i++) {
+           bs.insertBookmark(PlacesUtils.toolbarFolderId,
+                             NetUtil.newURI("http://example.com/"),
+-                            bs.DEFAULT_INDEX, "testbookmark");
++                            bs.DEFAULT_INDEX, "testbookmark", null,
++                            PlacesUtils.bookmarks.SOURCES.SYNC);
+         }
+       }
+     }, null);
+ 
+     function randomize_positions(aParent, aResultArray) {
+       let stmt = mDBConn.createStatement(
+         `UPDATE moz_bookmarks SET position = :rand
+          WHERE id IN (
+@@ -725,42 +776,54 @@ tests.push({
+       }
+       stmt.finalize();
+     }
+ 
+     // Set random positions for the added bookmarks.
+     randomize_positions(PlacesUtils.unfiledBookmarksFolderId,
+                         this._unfiledBookmarks);
+     randomize_positions(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
++
++    let syncInfos = await PlacesTestUtils.fetchBookmarkSyncFields(
++      PlacesUtils.bookmarks.unfiledGuid, PlacesUtils.bookmarks.toolbarGuid);
++    Assert.ok(syncInfos.every(info => info.syncChangeCounter === 0));
+   },
+ 
+-  check() {
+-    function check_order(aParent, aResultArray) {
++  async check() {
++    let db = await PlacesUtils.promiseDBConnection();
++
++    async function check_order(aParent, aResultArray) {
+       // Build the expected ordered list of bookmarks.
+-      let stmt = mDBConn.createStatement(
+-        `SELECT id, position FROM moz_bookmarks WHERE parent = :parent
+-         ORDER BY position ASC`
++      let childRows = await db.executeCached(
++        `SELECT id, position, syncChangeCounter FROM moz_bookmarks
++         WHERE parent = :parent
++         ORDER BY position ASC`,
++        { parent: aParent }
+       );
+-      stmt.params.parent = aParent;
+-      let pass = true;
+-      while (stmt.executeStep()) {
+-        print(stmt.row.id + "\t" + stmt.row.position);
+-        if (aResultArray.indexOf(stmt.row.id) != stmt.row.position) {
+-          pass = false;
++      for (let row of childRows) {
++        let id = row.getResultByName("id");
++        let position = row.getResultByName("position");
++        if (aResultArray.indexOf(id) != position) {
++          dump_table("moz_bookmarks");
++          do_throw("Unexpected unfiled bookmarks order.");
+         }
+       }
+-      stmt.finalize();
+-      if (!pass) {
+-        dump_table("moz_bookmarks");
+-        do_throw("Unexpected unfiled bookmarks order.");
++
++      let parentRows = await db.executeCached(
++        `SELECT syncChangeCounter FROM moz_bookmarks
++         WHERE id = :parent`,
++        { parent: aParent });
++      for (let row of parentRows) {
++        let actualChangeCounter = row.getResultByName("syncChangeCounter");
++        Assert.ok(actualChangeCounter > 0);
+       }
+     }
+ 
+-    check_order(PlacesUtils.unfiledBookmarksFolderId, this._unfiledBookmarks);
+-    check_order(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
++    await check_order(PlacesUtils.unfiledBookmarksFolderId, this._unfiledBookmarks);
++    await check_order(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
+   }
+ });
+ 
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "D.12",
+   desc: "Fix empty-named tags",
+@@ -1285,18 +1348,16 @@ tests.push({
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "S.1",
+   desc: "fix invalid GUIDs for synced bookmarks",
+   _bookmarkInfos: [],
+ 
+   async setup() {
+-    await PlacesTestUtils.markBookmarksAsSynced();
+-
+     let folderWithInvalidGuid = addBookmark(
+       null, PlacesUtils.bookmarks.TYPE_FOLDER,
+       PlacesUtils.bookmarks.bookmarksMenuFolder, /* aKeywordId */ null,
+       /* aFolderType */ null, "NORMAL folder with invalid GUID",
+       "{123456}", PlacesUtils.bookmarks.SYNC_STATUS.NORMAL);
+ 
+     let placeIdForBookmarkWithoutGuid = addPlace();
+     let bookmarkWithoutGuid = addBookmark(
+@@ -1365,18 +1426,16 @@ tests.push({
+ 
+       let syncStatus = row.getResultByName("syncStatus");
+       Assert.equal(syncStatus, expectedInfo.syncStatus);
+     }
+ 
+     let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+     Assert.deepEqual(tombstones.map(info => info.guid),
+                      ["bookmarkAAAA\n", "{123456}"]);
+-
+-    await PlacesSyncUtils.bookmarks.reset();
+   },
+ });
+ 
+ tests.push({
+   name: "S.2",
+   desc: "drop tombstones for bookmarks that aren't deleted",
+ 
+   async setup() {
+@@ -1618,16 +1677,18 @@ add_task(async function test_preventive_
+   // Get current bookmarks max ID for cleanup
+   let stmt = mDBConn.createStatement("SELECT MAX(id) FROM moz_bookmarks");
+   stmt.executeStep();
+   defaultBookmarksMaxId = stmt.getInt32(0);
+   stmt.finalize();
+   Assert.ok(defaultBookmarksMaxId > 0);
+ 
+   for (let test of tests) {
++    await PlacesTestUtils.markBookmarksAsSynced();
++
+     dump("\nExecuting test: " + test.name + "\n*** " + test.desc + "\n");
+     await test.setup();
+ 
+     Services.prefs.clearUserPref("places.database.lastMaintenance");
+     await PlacesDBUtils.maintenanceOnIdle();
+ 
+     // Check the lastMaintenance time has been saved.
+     Assert.notEqual(Services.prefs.getIntPref("places.database.lastMaintenance"), null);

+ 210 - 0
mozilla-release/patches/1386513-58a1.patch

@@ -0,0 +1,210 @@
+# HG changeset patch
+# User Mark Banner <standard8@mozilla.com>
+# Date 1504850489 -3600
+# Node ID eac9a60e4edb9f8d69248d108f2d4510c7306bf4
+# Parent  2270d1882dea3ebf2733317cc6c60009fe6d2962
+Bug 1386513 - Handle cases of pasted bookmarks not being in the database, to fix copying bookmarks across browser instances. r=mak
+
+MozReview-Commit-ID: Lv2DT0WQGhZ
+
+diff --git a/browser/components/places/PlacesUIUtils.jsm b/browser/components/places/PlacesUIUtils.jsm
+--- a/browser/components/places/PlacesUIUtils.jsm
++++ b/browser/components/places/PlacesUIUtils.jsm
+@@ -578,17 +578,18 @@ var PlacesUIUtils = {
+    *
+    * @return  a Places Transaction that can be transacted for performing the
+    *          move/insert command.
+    */
+   getTransactionForData(aData, aType, aNewParentGuid, aIndex, aCopy) {
+     if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+       throw new Error(`Unsupported '${aData.type}' data type`);
+ 
+-    if ("itemGuid" in aData) {
++    if ("itemGuid" in aData && "instanceId" in aData &&
++        aData.instanceId == PlacesUtils.instanceId) {
+       if (!this.PLACES_FLAVORS.includes(aData.type))
+         throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
+ 
+       let info = { guid: aData.itemGuid,
+                    newParentGuid: aNewParentGuid,
+                    newIndex: aIndex };
+       if (aCopy) {
+         info.excludingAnnotation = "Places/SmartBookmark";
+diff --git a/browser/components/places/tests/browser/browser.ini b/browser/components/places/tests/browser/browser.ini
+--- a/browser/components/places/tests/browser/browser.ini
++++ b/browser/components/places/tests/browser/browser.ini
+@@ -47,18 +47,20 @@ subsuite = clipboard
+ [browser_library_left_pane_select_hierarchy.js]
+ [browser_library_middleclick.js]
+ [browser_library_open_leak.js]
+ [browser_library_openFlatContainer.js]
+ [browser_library_panel_leak.js]
+ [browser_library_search.js]
+ [browser_library_views_liveupdate.js]
+ [browser_markPageAsFollowedLink.js]
++[browser_paste_bookmarks.js]
++subsuite = clipboard
+ [browser_paste_into_tags.js]
+-+subsuite = clipboard
++subsuite = clipboard
+ [browser_sidebarpanels_click.js]
+ skip-if = true # temporarily disabled for breaking the treeview - bug 658744
+ [browser_sort_in_library.js]
+ [browser_toolbar_drop_text.js]
+ [browser_toolbarbutton_menu_context.js]
+ skip-if = true # bug 1364329
+ [browser_views_iconsupdate.js]
+ support-files =
+diff --git a/browser/components/places/tests/browser/browser_paste_bookmarks.js b/browser/components/places/tests/browser/browser_paste_bookmarks.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/places/tests/browser/browser_paste_bookmarks.js
+@@ -0,0 +1,103 @@
++/* Any copyright is dedicated to the Public Domain.
++ * http://creativecommons.org/publicdomain/zero/1.0/ */
++
++"use strict";
++
++const TEST_URL = "http://example.com/";
++const TEST_URL1 = "https://example.com/otherbrowser";
++
++var PlacesOrganizer;
++var ContentTree;
++var bookmark;
++var bookmarkId;
++
++add_task(async function setup() {
++  await PlacesUtils.bookmarks.eraseEverything();
++  let organizer = await promiseLibrary();
++
++  registerCleanupFunction(async function() {
++    await promiseLibraryClosed(organizer);
++    await PlacesUtils.bookmarks.eraseEverything();
++  });
++
++  PlacesOrganizer = organizer.PlacesOrganizer;
++  ContentTree = organizer.ContentTree;
++
++  info("Selecting BookmarksToolbar in the left pane");
++  PlacesOrganizer.selectLeftPaneQuery("BookmarksToolbar");
++
++  bookmark = await PlacesUtils.bookmarks.insert({
++    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
++    url: TEST_URL,
++    title: "0"
++  });
++  bookmarkId = await PlacesUtils.promiseItemId(bookmark.guid);
++
++  ContentTree.view.selectItems([bookmarkId]);
++
++  await promiseClipboard(() => {
++    info("Copying selection");
++    ContentTree.view.controller.cut();
++  }, PlacesUtils.TYPE_X_MOZ_PLACE);
++});
++
++add_task(async function paste() {
++  info("Selecting UnfiledBookmarks in the left pane");
++  PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
++
++  info("Pasting clipboard");
++  await ContentTree.view.controller.paste();
++
++  let tree = await PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.unfiledGuid);
++
++  Assert.equal(tree.children.length, 1,
++               "Should be one bookmark in the unfiled folder.");
++  Assert.equal(tree.children[0].title, "0",
++               "Should have the correct title");
++  Assert.equal(tree.children[0].uri, TEST_URL,
++               "Should have the correct URL");
++
++  await PlacesUtils.bookmarks.remove(tree.children[0].guid);
++});
++
++add_task(async function paste_from_different_instance() {
++  let xferable = Cc["@mozilla.org/widget/transferable;1"]
++                   .createInstance(Ci.nsITransferable);
++  xferable.init(null);
++
++  // Fake data on the clipboard to pretend this is from a different instance
++  // of Firefox.
++  let data = {
++    "title": "test",
++    "id": 32,
++    "instanceId": "FAKEFAKEFAKE",
++    "itemGuid": "ZBf_TYkrYGvW",
++    "parent": 452,
++    "dateAdded": 1464866275853000,
++    "lastModified": 1507638113352000,
++    "type": "text/x-moz-place",
++    "uri": TEST_URL1
++  };
++  data = JSON.stringify(data);
++
++  xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE);
++  xferable.setTransferData(PlacesUtils.TYPE_X_MOZ_PLACE, PlacesUtils.toISupportsString(data),
++                           data.length * 2);
++
++  Services.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
++
++  info("Pasting clipboard");
++
++  await ContentTree.view.controller.paste();
++
++  let tree = await PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.unfiledGuid);
++
++  Assert.equal(tree.children.length, 1,
++               "Should be one bookmark in the unfiled folder.");
++  Assert.equal(tree.children[0].title, "test",
++               "Should have the correct title");
++  Assert.equal(tree.children[0].uri, TEST_URL1,
++               "Should have the correct URL");
++
++  await PlacesUtils.bookmarks.remove(tree.children[0].guid);
++});
+diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm
+--- a/toolkit/components/places/PlacesUtils.jsm
++++ b/toolkit/components/places/PlacesUtils.jsm
+@@ -128,16 +128,19 @@ async function notifyKeywordChange(url, 
+  *        Whether the node represents a livemark.
+  */
+ function serializeNode(aNode, aIsLivemark) {
+   let data = {};
+ 
+   data.title = aNode.title;
+   data.id = aNode.itemId;
+   data.livemark = aIsLivemark;
++  // Add an instanceId so we can tell which instance of an FF session the data
++  // is coming from.
++  data.instanceId = PlacesUtils.instanceId;
+ 
+   let guid = aNode.bookmarkGuid;
+   if (guid) {
+     data.itemGuid = guid;
+     if (aNode.parent)
+       data.parent = aNode.parent.itemId;
+     let grandParent = aNode.parent && aNode.parent.parent;
+     if (grandParent)
+@@ -2007,16 +2010,21 @@ XPCOMUtils.defineLazyGetter(PlacesUtils,
+   });
+ });
+ 
+ XPCOMUtils.defineLazyGetter(this, "bundle", function() {
+   const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
+   return Services.strings.createBundle(PLACES_STRING_BUNDLE_URI);
+ });
+ 
++// This is just used as a reasonably-random value for copy & paste / drag operations.
++XPCOMUtils.defineLazyGetter(PlacesUtils, "instanceId", () => {
++  return PlacesUtils.history.makeGuid();
++});
++
+ /**
+  * Setup internal databases for closing properly during shutdown.
+  *
+  * 1. Places initiates shutdown.
+  * 2. Before places can move to the step where it closes the low-level connection,
+  *   we need to make sure that we have closed `conn`.
+  * 3. Before we can close `conn`, we need to make sure that all external clients
+  *   have stopped using `conn`.

+ 297 - 0
mozilla-release/patches/1405563-58a1.patch

@@ -0,0 +1,297 @@
+# HG changeset patch
+# User Kit Cambridge <kit@yakshaving.ninja>
+# Date 1507148994 25200
+# Node ID a97ced54c8398a23b48e6b61d3dae4e8112acff8
+# Parent  4eea5321228f3d5027ee644f6eac57bb16dac3b5
+Bug 1405563 - Ignore and clean up tombstones for undeleted synced bookmarks. r=mak,markh
+
+MozReview-Commit-ID: KqnZFn35qId
+
+diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm
+--- a/toolkit/components/places/PlacesDBUtils.jsm
++++ b/toolkit/components/places/PlacesDBUtils.jsm
+@@ -849,16 +849,23 @@ var PlacesDBUtils = {
+       params: {
+         syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+       },
+     });
+     cleanupStatements.push({
+       query: "DROP TRIGGER moz_bm_fix_guids_temp_trigger",
+     });
+ 
++    // S.2 drop tombstones for bookmarks that aren't deleted.
++    cleanupStatements.push({
++      query:
++      `DELETE FROM moz_bookmarks_deleted
++       WHERE guid IN (SELECT guid FROM moz_bookmarks)`,
++    });
++
+     // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT!
+ 
+     return cleanupStatements;
+   },
+ 
+   /**
+    * Tries to vacuum the database.
+    *
+diff --git a/toolkit/components/places/PlacesSyncUtils.jsm b/toolkit/components/places/PlacesSyncUtils.jsm
+--- a/toolkit/components/places/PlacesSyncUtils.jsm
++++ b/toolkit/components/places/PlacesSyncUtils.jsm
+@@ -551,18 +551,17 @@ const BookmarkSyncUtils = PlacesSyncUtil
+    *        A changeset containing sync change records, as returned by
+    *        `pullChanges`.
+    * @return {Promise} resolved once all records have been updated.
+    */
+   pushChanges(changeRecords) {
+     return PlacesUtils.withConnectionWrapper(
+       "BookmarkSyncUtils: pushChanges", async function(db) {
+         let skippedCount = 0;
+-        let syncedTombstoneGuids = [];
+-        let syncedChanges = [];
++        let updateParams = [];
+ 
+         for (let syncId in changeRecords) {
+           // Validate change records to catch coding errors.
+           let changeRecord = validateChangeRecord(
+             "BookmarkSyncUtils: pushChanges",
+             changeRecords[syncId], {
+               tombstone: { required: true },
+               counter: { required: true },
+@@ -574,47 +573,49 @@ const BookmarkSyncUtils = PlacesSyncUtil
+           // uploaded items. If upload failed, ignore the change; we'll
+           // try again on the next sync.
+           if (!changeRecord.synced) {
+             skippedCount++;
+             continue;
+           }
+ 
+           let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
+-          if (changeRecord.tombstone) {
+-            syncedTombstoneGuids.push(guid);
+-          } else {
+-            syncedChanges.push([guid, changeRecord]);
+-          }
++          updateParams.push({
++            guid,
++            syncChangeDelta: changeRecord.counter,
++            syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
++          });
+         }
+ 
+-        if (syncedChanges.length || syncedTombstoneGuids.length) {
++        // Reduce the change counter and update the sync status for
++        // reconciled and uploaded items. If the bookmark was updated
++        // during the sync, its change counter will still be > 0 for the
++        // next sync.
++        if (updateParams.length) {
+           await db.executeTransaction(async function() {
+-            for (let [guid, changeRecord] of syncedChanges) {
+-              // Reduce the change counter and update the sync status for
+-              // reconciled and uploaded items. If the bookmark was updated
+-              // during the sync, its change counter will still be > 0 for the
+-              // next sync.
+-              await db.executeCached(`
+-                UPDATE moz_bookmarks
+-                SET syncChangeCounter = MAX(syncChangeCounter - :syncChangeDelta, 0),
+-                    syncStatus = :syncStatus
+-                WHERE guid = :guid`,
+-                { guid, syncChangeDelta: changeRecord.counter,
+-                  syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL });
+-            }
++            await db.executeCached(`
++              UPDATE moz_bookmarks
++              SET syncChangeCounter = MAX(syncChangeCounter - :syncChangeDelta, 0),
++                  syncStatus = :syncStatus
++              WHERE guid = :guid`,
++              updateParams);
+ 
+-            await removeTombstones(db, syncedTombstoneGuids);
++            // Unconditionally delete tombstones, in case the GUID exists in
++            // `moz_bookmarks` and `moz_bookmarks_deleted` (bug 1405563).
++            let deleteParams = updateParams.map(({ guid }) => ({ guid }));
++            await db.executeCached(`
++              DELETE FROM moz_bookmarks_deleted
++              WHERE guid = :guid`,
++              deleteParams);
+           });
+         }
+ 
+         BookmarkSyncLog.debug(`pushChanges: Processed change records`,
+                               { skipped: skippedCount,
+-                                updated: syncedChanges.length,
+-                                tombstones: syncedTombstoneGuids.length });
++                                updated: updateParams.length });
+       }
+     );
+   },
+ 
+   /**
+    * Removes items from the database. Sync buffers incoming tombstones, and
+    * calls this method to apply them at the end of each sync. Deletion
+    * happens in three steps:
+@@ -1958,32 +1959,51 @@ async function fetchQueryItem(db, bookma
+   if (query) {
+     item.query = query;
+   }
+ 
+   return item;
+ }
+ 
+ function addRowToChangeRecords(row, changeRecords) {
+-  let syncId = BookmarkSyncUtils.guidToSyncId(row.getResultByName("guid"));
++  let guid = row.getResultByName("guid");
++  if (!guid) {
++    throw new Error(`Changed item missing GUID`);
++  }
++  let isTombstone = !!row.getResultByName("tombstone");
++  let syncId = BookmarkSyncUtils.guidToSyncId(guid);
+   if (syncId in changeRecords) {
+-    throw new Error(`Duplicate entry for ${syncId} in changeset`);
++    let existingRecord = changeRecords[syncId];
++    if (existingRecord.tombstone == isTombstone) {
++      // Should never happen: `moz_bookmarks.guid` has a unique index, and
++      // `moz_bookmarks_deleted.guid` is the primary key.
++      throw new Error(`Duplicate item or tombstone ${syncId} in changeset`);
++    }
++    if (!existingRecord.tombstone && isTombstone) {
++      // Don't replace undeleted items with tombstones...
++      BookmarkSyncLog.warn("addRowToChangeRecords: Ignoring tombstone for " +
++                           "undeleted item", syncId);
++      return;
++    }
++    // ...But replace undeleted tombstones with items.
++    BookmarkSyncLog.warn("addRowToChangeRecords: Replacing tombstone for " +
++                         "undeleted item", syncId);
+   }
+   let modifiedAsPRTime = row.getResultByName("modified");
+   let modified = modifiedAsPRTime / MICROSECONDS_PER_SECOND;
+   if (Number.isNaN(modified) || modified <= 0) {
+     BookmarkSyncLog.error("addRowToChangeRecords: Invalid modified date for " +
+                           syncId, modifiedAsPRTime);
+     modified = 0;
+   }
+   changeRecords[syncId] = {
+     modified,
+     counter: row.getResultByName("syncChangeCounter"),
+     status: row.getResultByName("syncStatus"),
+-    tombstone: !!row.getResultByName("tombstone"),
++    tombstone: isTombstone,
+     synced: false,
+   };
+ }
+ 
+ /**
+  * Queries the database for synced bookmarks and tombstones, and returns a
+  * changeset for the Sync bookmarks engine.
+  *
+diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
++++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+@@ -1326,16 +1326,42 @@ tests.push({
+     let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+     Assert.deepEqual(tombstones.map(info => info.guid),
+                      ["bookmarkAAAA\n", "{123456}"]);
+ 
+     await PlacesSyncUtils.bookmarks.reset();
+   },
+ });
+ 
++tests.push({
++  name: "S.2",
++  desc: "drop tombstones for bookmarks that aren't deleted",
++
++  async setup() {
++    addBookmark(null, bs.TYPE_BOOKMARK, bs.bookmarksMenuFolder, null, null,
++                "", "bookmarkAAAA");
++
++    await PlacesUtils.withConnectionWrapper("Insert tombstones", db =>
++      db.executeTransaction(async function() {
++        for (let guid of ["bookmarkAAAA", "bookmarkBBBB"]) {
++          await db.executeCached(`
++            INSERT INTO moz_bookmarks_deleted(guid)
++            VALUES(:guid)`,
++            { guid });
++        }
++      })
++    );
++  },
++
++  async check() {
++    let tombstones = await PlacesTestUtils.fetchSyncTombstones();
++    Assert.deepEqual(tombstones.map(info => info.guid), ["bookmarkBBBB"]);
++  },
++});
++
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "Z",
+   desc: "Sanity: Preventive maintenance does not touch valid items",
+ 
+   _uri1: uri("http://www1.mozilla.org"),
+   _uri2: uri("http://www2.mozilla.org"),
+diff --git a/toolkit/components/places/tests/unit/test_sync_utils.js b/toolkit/components/places/tests/unit/test_sync_utils.js
+--- a/toolkit/components/places/tests/unit/test_sync_utils.js
++++ b/toolkit/components/places/tests/unit/test_sync_utils.js
+@@ -2334,16 +2334,61 @@ add_task(async function test_pullChanges
+       "Pulling changes should track synced sibling and parent");
+     await setChangesSynced(changes);
+   }
+ 
+   await PlacesUtils.bookmarks.eraseEverything();
+   await PlacesSyncUtils.bookmarks.reset();
+ });
+ 
++add_task(async function test_pullChanges_tombstones() {
++  await ignoreChangedRoots();
++
++  info("Insert new bookmarks");
++  await PlacesUtils.bookmarks.insertTree({
++    guid: PlacesUtils.bookmarks.menuGuid,
++    children: [{
++      guid: "bookmarkAAAA",
++      url: "http://example.com/a",
++      title: "A",
++    }, {
++      guid: "bookmarkBBBB",
++      url: "http://example.com/b",
++      title: "B",
++    }],
++  });
++
++  info("Manually insert conflicting tombstone for new bookmark");
++  await PlacesUtils.withConnectionWrapper("test_pullChanges_tombstones",
++    async function(db) {
++      await db.executeCached(`
++        INSERT INTO moz_bookmarks_deleted(guid)
++        VALUES(:guid)`,
++        { guid: "bookmarkAAAA" });
++    }
++  );
++
++  let changes = await PlacesSyncUtils.bookmarks.pullChanges();
++  deepEqual(Object.keys(changes).sort(), ["bookmarkAAAA", "bookmarkBBBB",
++    "menu"], "Should handle undeleted items when returning changes");
++  strictEqual(changes.bookmarkAAAA.tombstone, false,
++    "Should replace tombstone for A with undeleted item");
++  strictEqual(changes.bookmarkBBBB.tombstone, false,
++    "Should not report B as deleted");
++
++  await setChangesSynced(changes);
++
++  let newChanges = await PlacesSyncUtils.bookmarks.pullChanges();
++  deepEqual(newChanges, {},
++    "Should not return changes after marking undeleted items as synced");
++
++  await PlacesUtils.bookmarks.eraseEverything();
++  await PlacesSyncUtils.bookmarks.reset();
++});
++
+ add_task(async function test_pushChanges() {
+   await ignoreChangedRoots();
+ 
+   info("Populate test bookmarks");
+   let guids = await populateTree(PlacesUtils.bookmarks.menuGuid, {
+     kind: "bookmark",
+     title: "unknownBmk",
+     url: "https://example.org",

+ 1091 - 0
mozilla-release/patches/1405722-58a1.patch

@@ -0,0 +1,1091 @@
+# HG changeset patch
+# User Marco Bonardo <mbonardo@mozilla.com>
+# Date 1507629919 -7200
+# Node ID 8981c30ec849e8f09e04ef2e100328ab0e93be94
+# Parent  f66d5485553daab326b6bbf6fd51afbfe525672b
+Bug 1405722 - Remove the IsLivemark() bookmarks observer from PlacesUIUtils. r=standard8
+
+MozReview-Commit-ID: 586IR54ggbm
+
+diff --git a/browser/components/places/PlacesUIUtils.jsm b/browser/components/places/PlacesUIUtils.jsm
+--- a/browser/components/places/PlacesUIUtils.jsm
++++ b/browser/components/places/PlacesUIUtils.jsm
+@@ -33,63 +33,16 @@ let gFaviconLoadDataMap = new Map();
+ 
+ const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10;
+ 
+ // copied from utilityOverlay.js
+ const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+ const PREF_LOAD_BOOKMARKS_IN_BACKGROUND = "browser.tabs.loadBookmarksInBackground";
+ const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs";
+ 
+-// This function isn't public both because it's synchronous and because it is
+-// going to be removed in bug 1072833.
+-function IsLivemark(aItemId) {
+-  // Since this check may be done on each dragover event, it's worth maintaining
+-  // a cache.
+-  let self = IsLivemark;
+-  if (!("ids" in self)) {
+-    const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI;
+-
+-    let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO);
+-    self.ids = new Set(idsVec);
+-
+-    let obs = Object.freeze({
+-      QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarksObserver]),
+-
+-      onItemChanged(itemId, property, isAnnoProperty, newValue, lastModified,
+-                    itemType, parentId, guid) {
+-        if (isAnnoProperty && property == LIVEMARK_ANNO) {
+-          self.ids.add(itemId);
+-        }
+-      },
+-
+-      onItemRemoved(itemId) {
+-        // Since the bookmark is removed, we know we can remove any references
+-        // to it from the cache.
+-        self.ids.delete(itemId);
+-      },
+-
+-      onItemAdded() {},
+-      onBeginUpdateBatch() {},
+-      onEndUpdateBatch() {},
+-      onItemVisited() {},
+-      onItemMoved() {},
+-      onPageAnnotationSet() { },
+-      onPageAnnotationRemoved() { },
+-      skipDescendantsOnItemRemoval: false,
+-      skipTags: true,
+-    });
+-
+-    PlacesUtils.bookmarks.addObserver(obs);
+-    PlacesUtils.registerShutdownFunction(() => {
+-      PlacesUtils.bookmarks.removeObserver(obs);
+-    });
+-  }
+-  return self.ids.has(aItemId);
+-}
+-
+ let InternalFaviconLoader = {
+   /**
+    * This gets called for every inner window that is destroyed.
+    * In the parent process, we process the destruction ourselves. In the child process,
+    * we notify the parent which will then process it based on that message.
+    */
+   observe(subject, topic, data) {
+     let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+@@ -809,19 +762,21 @@ var PlacesUIUtils = {
+   },
+ 
+   /**
+    * Check whether or not the given node represents a removable entry (either in
+    * history or in bookmarks).
+    *
+    * @param aNode
+    *        a node, except the root node of a query.
++   * @param aView
++   *        The view originating the request.
+    * @return true if the aNode represents a removable entry, false otherwise.
+    */
+-  canUserRemove(aNode) {
++  canUserRemove(aNode, aView) {
+     let parentNode = aNode.parent;
+     if (!parentNode) {
+       // canUserRemove doesn't accept root nodes.
+       return false;
+     }
+ 
+     // If it's not a bookmark, we can remove it unless it's a child of a
+     // livemark.
+@@ -832,70 +787,66 @@ var PlacesUIUtils = {
+       return !PlacesUtils.nodeIsFolder(parentNode);
+     }
+ 
+     // Generally it's always possible to remove children of a query.
+     if (PlacesUtils.nodeIsQuery(parentNode))
+       return true;
+ 
+     // Otherwise it has to be a child of an editable folder.
+-    return !this.isContentsReadOnly(parentNode);
++    return !this.isFolderReadOnly(parentNode, aView);
+   },
+ 
+   /**
+    * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
+    * TO GUIDS IS COMPLETE (BUG 1071511).
+    *
+-   * Check whether or not the given node or item-id points to a folder which
++   * Check whether or not the given Places node points to a folder which
+    * should not be modified by the user (i.e. its children should be unremovable
+    * and unmovable, new children should be disallowed, etc).
+    * These semantics are not inherited, meaning that read-only folder may
+    * contain editable items (for instance, the places root is read-only, but all
+    * of its direct children aren't).
+    *
+-   * You should only pass folder item ids or folder nodes for aNodeOrItemId.
+-   * While this is only enforced for the node case (if an item id of a separator
+-   * or a bookmark is passed, false is returned), it's considered the caller's
+-   * job to ensure that it checks a folder.
+-   * Also note that folder-shortcuts should only be passed as result nodes.
+-   * Otherwise they are just treated as bookmarks (i.e. false is returned).
++   * You should only pass folder nodes.
+    *
+-   * @param aNodeOrItemId
+-   *        any item id or result node.
+-   * @throws if aNodeOrItemId is neither an item id nor a folder result node.
++   * @param placesNode
++   *        any folder result node.
++   * @param view
++   *        The view originating the request.
++   * @throws if placesNode is not a folder result node or views is invalid.
+    * @note livemark "folders" are considered read-only (but see bug 1072833).
+-   * @return true if aItemId points to a read-only folder, false otherwise.
++   * @return true if placesNode is a read-only folder, false otherwise.
+    */
+-  isContentsReadOnly(aNodeOrItemId) {
+-    let itemId;
+-    if (typeof(aNodeOrItemId) == "number") {
+-      itemId = aNodeOrItemId;
+-    } else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) {
+-      itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId);
+-    } else {
+-      throw new Error("invalid value for aNodeOrItemId");
++  isFolderReadOnly(placesNode, view) {
++    if (typeof placesNode != "object" || !PlacesUtils.nodeIsFolder(placesNode)) {
++      throw new Error("invalid value for placesNode");
+     }
+-
+-    if (itemId == PlacesUtils.placesRootId || IsLivemark(itemId))
++    if (!view || typeof view != "object") {
++      throw new Error("invalid value for aView");
++    }
++    let itemId = PlacesUtils.getConcreteItemId(placesNode);
++    if (itemId == PlacesUtils.placesRootId ||
++        view.controller.hasCachedLivemarkInfo(placesNode))
+       return true;
+ 
+     // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter
+     // performing at least a synchronous DB query (and on its very first call
+     // in a fresh profile, it also creates the entire structure).
+     // Therefore we don't want to this function, which is called very often by
+     // isCommandEnabled, to ever be the one that invokes it first, especially
+     // because isCommandEnabled may be called way before the left pane folder is
+     // even created (for example, if the user only uses the bookmarks menu or
+     // toolbar for managing bookmarks).  To do so, we avoid comparing to those
+     // special folder if the lazy getter is still in place.  This is safe merely
+     // because the only way to access the left pane contents goes through
+     // "resolving" the leftPaneFolderId getter.
+-    if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId"))
++    if (typeof Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").get == "function") {
+       return false;
+-
++    }
+     return itemId == this.leftPaneFolderId ||
+            itemId == this.allBookmarksFolderId;
+   },
+ 
+   /**
+    * Gives the user a chance to cancel loading lots of tabs at once
+    */
+   confirmOpenInTabs(numTabsToOpen, aWindow) {
+diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js
+--- a/browser/components/places/content/browserPlacesViews.js
++++ b/browser/components/places/content/browserPlacesViews.js
+@@ -210,17 +210,17 @@ PlacesViewBase.prototype = {
+           tagName = container.title;
+           // TODO (Bug 1160193): properly support dropping on a tag root.
+           if (!tagName)
+             return null;
+         }
+       }
+     }
+ 
+-    if (PlacesControllerDragHelper.disallowInsertion(container))
++    if (PlacesControllerDragHelper.disallowInsertion(container, this))
+       return null;
+ 
+     return new InsertionPoint({
+       parentId: PlacesUtils.getConcreteItemId(container),
+       parentGuid: PlacesUtils.getConcreteItemGuid(container),
+       index, orientation, tagName
+     });
+   },
+@@ -1407,17 +1407,17 @@ PlacesToolbar.prototype = {
+ 
+     let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
+     let elt = aEvent.target;
+     if (elt._placesNode && elt != this._rootElt &&
+         elt.localName != "menupopup") {
+       let eltRect = elt.getBoundingClientRect();
+       let eltIndex = Array.prototype.indexOf.call(this._rootElt.childNodes, elt);
+       if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
+-          !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) {
++          !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this)) {
+         // This is a folder.
+         // If we are in the middle of it, drop inside it.
+         // Otherwise, drop before it, with regards to RTL mode.
+         let threshold = eltRect.width * 0.25;
+         if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
+                        : (aEvent.clientX < eltRect.left + threshold)) {
+           // Drop before this folder.
+           dropPoint.ip =
+diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js
+--- a/browser/components/places/content/controller.js
++++ b/browser/components/places/content/controller.js
+@@ -194,17 +194,17 @@ PlacesController.prototype = {
+       // Livemark containers
+       let selectedNode = this._view.selectedNode;
+       return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
+     }
+     case "placesCmd_sortBy:name": {
+       let selectedNode = this._view.selectedNode;
+       return selectedNode &&
+              PlacesUtils.nodeIsFolder(selectedNode) &&
+-             !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
++             !PlacesUIUtils.isFolderReadOnly(selectedNode, this._view) &&
+              this._view.result.sortingMode ==
+                  Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+     }
+     case "placesCmd_createBookmark":
+       var node = this._view.selectedNode;
+       return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
+     default:
+       return false;
+@@ -323,17 +323,17 @@ PlacesController.prototype = {
+ 
+     for (var j = 0; j < ranges.length; j++) {
+       var nodes = ranges[j];
+       for (var i = 0; i < nodes.length; ++i) {
+         // Disallow removing the view's root node
+         if (nodes[i] == root)
+           return false;
+ 
+-        if (!PlacesUIUtils.canUserRemove(nodes[i]))
++        if (!PlacesUIUtils.canUserRemove(nodes[i], this._view))
+           return false;
+       }
+     }
+ 
+     return true;
+   },
+ 
+   /**
+@@ -1495,55 +1495,72 @@ var PlacesControllerDragHelper = {
+       }
+     }
+     return true;
+   },
+ 
+   /**
+    * Determines if an unwrapped node can be moved.
+    *
+-   * @param   aUnwrappedNode
+-   *          A node unwrapped by PlacesUtils.unwrapNodes().
++   * @param unwrappedNode
++   *        A node unwrapped by PlacesUtils.unwrapNodes().
+    * @return True if the node can be moved, false otherwise.
+    */
+-  canMoveUnwrappedNode(aUnwrappedNode) {
+-    return aUnwrappedNode.id > 0 &&
+-           !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
+-           (!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
+-           aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
+-           aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
++  canMoveUnwrappedNode(unwrappedNode) {
++    if (unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
++      return false;
++    }
++    let parentId = unwrappedNode.parent;
++    if (parentId <= 0 ||
++        parentId == PlacesUtils.placesRootId ||
++        parentId == PlacesUtils.tagsFolderId ||
++        unwrappedNode.grandParentId == PlacesUtils.tagsFolderId) {
++      return false;
++    }
++    // leftPaneFolderId and allBookmarksFolderId are lazy getters running
++    // at least a synchronous DB query. Therefore we don't want to invoke
++    // them first, especially because isCommandEnabled may be called way
++    // before the left pane folder is even necessary.
++    if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
++        (parentId == PlacesUIUtils.leftPaneFolderId ||
++          parentId == PlacesUIUtils.allBookmarksFolderId)) {
++      return false;
++    }
++    return true;
+   },
+ 
+   /**
+    * Determines if a node can be moved.
+    *
+    * @param   aNode
+    *          A nsINavHistoryResultNode node.
++   * @param   aView
++   *          The view originating the request
+    * @param   [optional] aDOMNode
+    *          A XUL DOM node.
+    * @return True if the node can be moved, false otherwise.
+    */
+-  canMoveNode(aNode, aDOMNode) {
++  canMoveNode(aNode, aView, aDOMNode) {
+     // Only bookmark items are movable.
+     if (aNode.itemId == -1)
+       return false;
+ 
+     let parentNode = aNode.parent;
+     if (!parentNode) {
+       // Normally parentless places nodes can not be moved,
+       // but simulated bookmarked URI nodes are special.
+       return !!aDOMNode &&
+              aDOMNode.hasAttribute("simulated-places-node") &&
+              PlacesUtils.nodeIsBookmark(aNode);
+     }
+ 
+     // Once tags and bookmarked are divorced, the tag-query check should be
+     // removed.
+-    return !(PlacesUtils.nodeIsFolder(parentNode) &&
+-             PlacesUIUtils.isContentsReadOnly(parentNode)) &&
++    return PlacesUtils.nodeIsFolder(parentNode) &&
++           !PlacesUIUtils.isFolderReadOnly(parentNode, aView) &&
+            !PlacesUtils.nodeIsTagQuery(parentNode);
+   },
+ 
+   /**
+    * Handles the drop of one or more items onto a view.
+    *
+    * @param {Object} insertionPoint The insertion point where the items should
+    *                                be dropped.
+@@ -1683,24 +1700,26 @@ var PlacesControllerDragHelper = {
+       PlacesUtils.transactionManager.doTransaction(txn);
+     }
+   },
+ 
+   /**
+    * Checks if we can insert into a container.
+    * @param   aContainer
+    *          The container were we are want to drop
++   * @param   aView
++   *          The view generating the request
+    */
+-  disallowInsertion(aContainer) {
++  disallowInsertion(aContainer, aView) {
+     if (!aContainer)
+       throw new Error("empty container");
+     // Allow dropping into Tag containers and editable folders.
+     return !PlacesUtils.nodeIsTagQuery(aContainer) &&
+            (!PlacesUtils.nodeIsFolder(aContainer) ||
+-            PlacesUIUtils.isContentsReadOnly(aContainer));
++            PlacesUIUtils.isFolderReadOnly(aContainer, aView));
+   }
+ };
+ 
+ 
+ XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
+                                    "@mozilla.org/widget/dragservice;1",
+                                    "nsIDragService");
+ 
+diff --git a/browser/components/places/content/editBookmarkOverlay.js b/browser/components/places/content/editBookmarkOverlay.js
+--- a/browser/components/places/content/editBookmarkOverlay.js
++++ b/browser/components/places/content/editBookmarkOverlay.js
+@@ -50,18 +50,24 @@ var gEditItemOverlay = {
+     let parentId = -1;
+     let parentGuid = null;
+ 
+     if (node && isItem) {
+       if (!node.parent || (node.parent.itemId > 0 && !node.parent.bookmarkGuid)) {
+         throw new Error("Cannot use an incomplete node to initialize the edit bookmark panel");
+       }
+       let parent = node.parent;
+-      isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) ||
+-                          PlacesUIUtils.isContentsReadOnly(parent);
++      isParentReadOnly = !PlacesUtils.nodeIsFolder(parent);
++      if (!isParentReadOnly) {
++        let folderId = PlacesUtils.getConcreteItemId(parent);
++        isParentReadOnly = folderId == PlacesUtils.placesRootId ||
++                           (!("get" in Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId")) &&
++                            (folderId == PlacesUIUtils.leftPaneFolderId ||
++                             folderId == PlacesUIUtils.allBookmarksFolderId));
++      }
+       parentId = parent.itemId;
+       parentGuid = parent.bookmarkGuid;
+     }
+ 
+     let focusedElement = aInitInfo.focusedElement;
+     let onPanelReady = aInitInfo.onPanelReady;
+ 
+     return this._paneInfo = { itemId, itemGuid, parentId, parentGuid, isItem,
+diff --git a/browser/components/places/content/menu.xml b/browser/components/places/content/menu.xml
+--- a/browser/components/places/content/menu.xml
++++ b/browser/components/places/content/menu.xml
+@@ -64,17 +64,17 @@
+            dragging over this popup insertion point -->
+       <method name="_getDropPoint">
+         <parameter name="aEvent"/>
+           <body><![CDATA[
+             // Can't drop if the menu isn't a folder
+             let resultNode = this._placesNode;
+ 
+             if (!PlacesUtils.nodeIsFolder(resultNode) ||
+-                PlacesControllerDragHelper.disallowInsertion(resultNode)) {
++                PlacesControllerDragHelper.disallowInsertion(resultNode, this._rootView)) {
+               return null;
+             }
+ 
+             var dropPoint = { ip: null, folderElt: null };
+ 
+             // The element we are dragging over
+             let elt = aEvent.target;
+             if (elt.localName == "menupopup")
+@@ -103,17 +103,17 @@
+                   elt.lastChild.hasAttribute("placespopup"))
+                 dropPoint.folderElt = elt;
+               return dropPoint;
+             }
+ 
+             let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
+                             elt._placesNode.title : null;
+             if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
+-                 !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) ||
++                 !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this._rootView)) ||
+                 PlacesUtils.nodeIsTagQuery(elt._placesNode)) {
+               // This is a folder or a tag container.
+               if (eventY - eltY < eltHeight * 0.20) {
+                 // If mouse is in the top part of the element, drop above folder.
+                 dropPoint.ip = new InsertionPoint({
+                   parentId: PlacesUtils.getConcreteItemId(resultNode),
+                   parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
+                   orientation: Ci.nsITreeView.DROP_BEFORE,
+@@ -345,17 +345,17 @@
+         let elt = event.target;
+         if (!elt._placesNode)
+           return;
+ 
+         let draggedElt = elt._placesNode;
+ 
+         // Force a copy action if parent node is a query or we are dragging a
+         // not-removable node.
+-        if (!PlacesControllerDragHelper.canMoveNode(draggedElt, elt))
++        if (!PlacesControllerDragHelper.canMoveNode(draggedElt, this._rootView, elt))
+           event.dataTransfer.effectAllowed = "copyLink";
+ 
+         // Activate the view and cache the dragged element.
+         this._rootView._draggedElt = draggedElt;
+         this._rootView.controller.setDataTransfer(event);
+         this.setAttribute("dragstart", "true");
+         event.stopPropagation();
+       ]]></handler>
+diff --git a/browser/components/places/content/tree.xml b/browser/components/places/content/tree.xml
+--- a/browser/components/places/content/tree.xml
++++ b/browser/components/places/content/tree.xml
+@@ -495,17 +495,17 @@
+               container = lastSelected.parent;
+ 
+               // See comment in the treeView.js's copy of this method
+               if (!container || !container.containerOpen)
+                 return null;
+ 
+               // Avoid the potentially expensive call to getChildIndex
+               // if we know this container doesn't allow insertion
+-              if (PlacesControllerDragHelper.disallowInsertion(container))
++              if (PlacesControllerDragHelper.disallowInsertion(container, this))
+                 return null;
+ 
+               var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
+               if (queryOptions.sortingMode !=
+                     Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+                 // If we are within a sorted view, insert at the end
+                 index = -1;
+               } else if (queryOptions.excludeItems ||
+@@ -518,17 +518,17 @@
+                 dropNearNode = lastSelected;
+               } else {
+                 var lsi = container.getChildIndex(lastSelected);
+                 index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+               }
+             }
+           }
+ 
+-          if (PlacesControllerDragHelper.disallowInsertion(container))
++          if (PlacesControllerDragHelper.disallowInsertion(container, this))
+             return null;
+ 
+           // TODO (Bug 1160193): properly support dropping on a tag root.
+           let tagName = null;
+           if (PlacesUtils.nodeIsTagQuery(container)) {
+             tagName = container.title;
+             if (!tagName)
+               return null;
+@@ -728,17 +728,17 @@
+           if (!node.parent) {
+             event.preventDefault();
+             event.stopPropagation();
+             return;
+           }
+ 
+           // If this node is child of a readonly container (e.g. a livemark)
+           // or cannot be moved, we must force a copy.
+-          if (!PlacesControllerDragHelper.canMoveNode(node)) {
++          if (!PlacesControllerDragHelper.canMoveNode(node, this)) {
+             event.dataTransfer.effectAllowed = "copyLink";
+             break;
+           }
+         }
+ 
+         this._controller.setDataTransfer(event);
+         event.stopPropagation();
+       ]]></handler>
+diff --git a/browser/components/places/content/treeView.js b/browser/components/places/content/treeView.js
+--- a/browser/components/places/content/treeView.js
++++ b/browser/components/places/content/treeView.js
+@@ -1393,17 +1393,17 @@ PlacesTreeView.prototype = {
+         // container here.  However, we can simply bail out when this happens,
+         // because we would then be back here in less than a millisecond, when
+         // the container had been reopened.
+         if (!container || !container.containerOpen)
+           return null;
+ 
+         // Avoid the potentially expensive call to getChildIndex
+         // if we know this container doesn't allow insertion.
+-        if (PlacesControllerDragHelper.disallowInsertion(container))
++        if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+           return null;
+ 
+         let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
+         if (queryOptions.sortingMode !=
+               Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
+           // If we are within a sorted view, insert at the end.
+           index = -1;
+         } else if (queryOptions.excludeItems ||
+@@ -1416,17 +1416,17 @@ PlacesTreeView.prototype = {
+           dropNearNode = lastSelected;
+         } else {
+           let lsi = container.getChildIndex(lastSelected);
+           index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+         }
+       }
+     }
+ 
+-    if (PlacesControllerDragHelper.disallowInsertion(container))
++    if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+       return null;
+ 
+     // TODO (Bug 1160193): properly support dropping on a tag root.
+     let tagName = null;
+     if (PlacesUtils.nodeIsTagQuery(container)) {
+       tagName = container.title;
+       if (!tagName)
+         return null;
+diff --git a/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
+--- a/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
++++ b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
+@@ -1,185 +1,125 @@
+ /* vim:set ts=2 sw=2 sts=2 et: */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ "use strict";
+ 
+-let rootFolder;
+-let rootNode;
+-
+-add_task(async function setup() {
+-  rootFolder = await PlacesUtils.bookmarks.insert({
+-    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
++add_task(async function() {
++  let root = await PlacesUtils.bookmarks.insert({
++    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+     title: "",
+-    type: PlacesUtils.bookmarks.TYPE_FOLDER,
++    type: PlacesUtils.bookmarks.TYPE_FOLDER
+   });
+ 
+-  let rootId = await PlacesUtils.promiseItemId(rootFolder.guid);
+-  rootNode = PlacesUtils.getFolderContents(rootId, false, true).root;
+-
+-  Assert.equal(rootNode.childCount, 0, "confirm test root is empty");
+-
+   registerCleanupFunction(async () => {
+     await PlacesUtils.bookmarks.eraseEverything();
+   });
+-});
+ 
+-add_task(async function test_regular_folder() {
+-  let regularFolder = await PlacesUtils.bookmarks.insert({
+-    parentGuid: rootFolder.guid,
+-    title: "",
+-    type: PlacesUtils.bookmarks.TYPE_FOLDER,
+-  });
++  await withSidebarTree("bookmarks", async function(tree) {
++    info("Test a regular folder");
++    let folder = await PlacesUtils.bookmarks.insert({
++      parentGuid: root.guid,
++      title: "",
++      type: PlacesUtils.bookmarks.TYPE_FOLDER,
++    });
++    let folderId = await PlacesUtils.promiseItemId(folder.guid);
++    tree.selectItems([folderId]);
++    Assert.equal(tree.selectedNode.bookmarkGuid, folder.guid,
++                 "Selected the expected node");
++    Assert.equal(tree.selectedNode.type, 6, "node is a folder");
++    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
++              "can move regular folder node");
+ 
+-  Assert.equal(rootNode.childCount, 1,
+-    "populate added data to the test root");
+-  Assert.equal(PlacesControllerDragHelper.canMoveNode(rootNode.getChild(0)),
+-     true, "can move regular folder node");
+-
+-  await PlacesUtils.bookmarks.remove(regularFolder);
+-});
+-
+-add_task(async function test_folder_shortcut() {
+-  let regularFolder = await PlacesUtils.bookmarks.insert({
+-    parentGuid: rootFolder.guid,
+-    title: "",
+-    type: PlacesUtils.bookmarks.TYPE_FOLDER,
+-  });
+-  let regularFolderId = await PlacesUtils.promiseItemId(regularFolder.guid);
++    info("Test a folder shortcut");
++    let shortcut = await PlacesUtils.bookmarks.insert({
++      parentGuid: root.guid,
++      title: "bar",
++      url: `place:folder=${folderId}`
++    });
++    let shortcutId = await PlacesUtils.promiseItemId(shortcut.guid);
++    tree.selectItems([shortcutId]);
++    Assert.equal(tree.selectedNode.bookmarkGuid, shortcut.guid,
++                 "Selected the expected node");
++    Assert.equal(tree.selectedNode.type, 9, "node is a folder shortcut");
++    Assert.equal(PlacesUtils.getConcreteItemGuid(tree.selectedNode),
++                 folder.guid, "shortcut node guid and concrete guid match");
++    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
++              "can move folder shortcut node");
+ 
+-  let shortcut = await PlacesUtils.bookmarks.insert({
+-    parentGuid: rootFolder.guid,
+-    title: "bar",
+-    url: `place:folder=${regularFolderId}`
+-  });
++    info("Test a query");
++    let bookmark = await PlacesUtils.bookmarks.insert({
++      parentGuid: root.guid,
++      title: "",
++      url: "http://foo.com",
++    });
++    let bookmarkId = await PlacesUtils.promiseItemId(bookmark.guid);
++    tree.selectItems([bookmarkId]);
++    Assert.equal(tree.selectedNode.bookmarkGuid, bookmark.guid,
++                 "Selected the expected node");
++    let query = await PlacesUtils.bookmarks.insert({
++      parentGuid: root.guid,
++      title: "bar",
++      url: `place:terms=foo`
++    });
++    let queryId = await PlacesUtils.promiseItemId(query.guid);
++    tree.selectItems([queryId]);
++    Assert.equal(tree.selectedNode.bookmarkGuid, query.guid,
++                 "Selected the expected node");
++    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
++              "can move query node");
+ 
+-  Assert.equal(rootNode.childCount, 2,
+-    "populated data to the test root");
+-
+-  let folderNode = rootNode.getChild(0);
+-  Assert.equal(folderNode.type, 6, "node is folder");
+-  Assert.equal(regularFolder.guid, folderNode.bookmarkGuid, "folder guid and folder node item guid match");
+ 
+-  let shortcutNode = rootNode.getChild(1);
+-  Assert.equal(shortcutNode.type, 9, "node is folder shortcut");
+-  Assert.equal(shortcut.guid, shortcutNode.bookmarkGuid, "shortcut guid and shortcut node item guid match");
++    info("Test a tag container");
++    PlacesUtils.tagging.tagURI(Services.io.newURI(bookmark.url.href), ["bar"]);
++    // Add the tags root query.
++    let tagsQuery = await PlacesUtils.bookmarks.insert({
++      parentGuid: root.guid,
++      title: "",
++      url: "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY,
++    });
++    let tagsQueryId = await PlacesUtils.promiseItemId(tagsQuery.guid);
++    tree.selectItems([tagsQueryId]);
++    PlacesUtils.asQuery(tree.selectedNode).containerOpen = true;
++    Assert.equal(tree.selectedNode.childCount, 1, "has tags");
++    let tagNode = tree.selectedNode.getChild(0);
++    Assert.ok(!PlacesControllerDragHelper.canMoveNode(tagNode, tree),
++              "should not be able to move tag container node");
++    tree.selectedNode.containerOpen = false;
+ 
+-  let concreteId = PlacesUtils.getConcreteItemGuid(shortcutNode);
+-  Assert.equal(concreteId, folderNode.bookmarkGuid, "shortcut node id and concrete id match");
++    info("Test that special folders and cannot be moved but other shortcuts can.");
++    let roots = [
++      PlacesUtils.bookmarksMenuFolderId,
++      PlacesUtils.unfiledBookmarksFolderId,
++      PlacesUtils.toolbarFolderId,
++    ];
+ 
+-  Assert.equal(PlacesControllerDragHelper.canMoveNode(shortcutNode), true,
+-   "can move folder shortcut node");
+-
+-  await PlacesUtils.bookmarks.remove(shortcut);
+-  await PlacesUtils.bookmarks.remove(regularFolder);
++    for (let id of roots) {
++      selectShortcutForRootId(tree, id);
++      Assert.ok(!PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
++                "shouldn't be able to move default shortcuts to roots");
++      let s = await PlacesUtils.bookmarks.insert({
++        parentGuid: root.guid,
++        title: "bar",
++        url: `place:folder=${id}`,
++      });
++      let sid = await PlacesUtils.promiseItemId(s.guid);
++      tree.selectItems([sid]);
++      Assert.equal(tree.selectedNode.bookmarkGuid, s.guid,
++                   "Selected the expected node");
++      Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
++                "should be able to move user-created shortcuts to roots");
++    }
++  });
+ });
+ 
+-add_task(async function test_regular_query() {
+-  let bookmark = await PlacesUtils.bookmarks.insert({
+-    parentGuid: rootFolder.guid,
+-    title: "",
+-    url: "http://foo.com",
+-  });
+-
+-  let query = await PlacesUtils.bookmarks.insert({
+-    parentGuid: rootFolder.guid,
+-    title: "bar",
+-    url: `place:terms=foo`
+-  });
+-
+-  Assert.equal(rootNode.childCount, 2,
+-    "populated data to the test root");
+-
+-  let bmNode = rootNode.getChild(0);
+-  Assert.equal(bmNode.bookmarkGuid, bookmark.guid, "bookmark guid and bookmark node item guid match");
+-
+-  let queryNode = rootNode.getChild(1);
+-  Assert.equal(queryNode.bookmarkGuid, query.guid, "query guid and query node item guid match");
+-
+-  Assert.equal(PlacesControllerDragHelper.canMoveNode(queryNode),
+-     true, "can move query node");
+-
+-  await PlacesUtils.bookmarks.remove(query);
+-  await PlacesUtils.bookmarks.remove(bookmark);
+-});
+-
+-add_task(async function test_special_folders() {
+-  // Test that special folders and special folder shortcuts cannot be moved.
+-  let folders = [
+-    PlacesUtils.bookmarksMenuFolderId,
+-    PlacesUtils.tagsFolderId,
+-    PlacesUtils.unfiledBookmarksFolderId,
+-    PlacesUtils.toolbarFolderId
+-  ];
+-
+-  let children = folders.map(folderId => {
+-    return {
+-      title: "",
+-      url: `place:folder=${folderId}`
+-    };
+-  });
+-
+-  let shortcuts = await PlacesUtils.bookmarks.insertTree({
+-    guid: rootFolder.guid,
+-    children
+-  });
+-
+-  // test toolbar shortcut node
+-  Assert.equal(rootNode.childCount, folders.length,
+-    "populated data to the test root");
+-
+-  function getRootChildNode(aId) {
+-    let node = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false, true).root;
+-    for (let i = 0; i < node.childCount; i++) {
+-      let child = node.getChild(i);
+-      if (child.itemId == aId) {
+-        node.containerOpen = false;
+-        return child;
+-      }
++function selectShortcutForRootId(tree, id) {
++  for (let i = 0; i < tree.result.root.childCount; ++i) {
++    let child = tree.result.root.getChild(i);
++    if (PlacesUtils.getConcreteItemId(child) == id) {
++      tree.selectItems([child.itemId]);
++      return;
+     }
+-    node.containerOpen = false;
+-    ok(false, "Unable to find child node");
+-    return null;
+   }
+-
+-  for (let i = 0; i < folders.length; i++) {
+-    let id = folders[i];
+-
+-    let node = getRootChildNode(id);
+-    isnot(node, null, "Node found");
+-    Assert.equal(PlacesControllerDragHelper.canMoveNode(node),
+-       false, "shouldn't be able to move special folder node");
+-
+-    let shortcut = shortcuts[i];
+-    let shortcutNode = rootNode.getChild(i);
+-
+-    Assert.equal(shortcutNode.bookmarkGuid, shortcut.guid,
+-      "shortcut guid and shortcut node item guid match");
+-
+-    Assert.equal(PlacesControllerDragHelper.canMoveNode(shortcutNode),
+-       true, "should be able to move special folder shortcut node");
+-  }
+-});
+-
+-add_task(async function test_tag_container() {
+-  // tag a uri
+-  this.uri = makeURI("http://foo.com");
+-  PlacesUtils.tagging.tagURI(this.uri, ["bar"]);
+-  registerCleanupFunction(() => PlacesUtils.tagging.untagURI(this.uri, ["bar"]));
+-
+-  // get tag root
+-  let query = PlacesUtils.history.getNewQuery();
+-  let options = PlacesUtils.history.getNewQueryOptions();
+-  options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY;
+-  let tagsNode = PlacesUtils.history.executeQuery(query, options).root;
+-
+-  tagsNode.containerOpen = true;
+-  Assert.equal(tagsNode.childCount, 1, "has new tag");
+-
+-  let tagNode = tagsNode.getChild(0);
+-
+-  Assert.equal(PlacesControllerDragHelper.canMoveNode(tagNode),
+-     false, "should not be able to move tag container node");
+-  tagsNode.containerOpen = false;
+-});
++  Assert.ok(false, "Cannot find shortcut to root");
++}
+diff --git a/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
+--- a/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
++++ b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
+@@ -76,13 +76,13 @@ function test() {
+     }
+     leftPaneQueries.push(query);
+     // Rename to a bad title.
+     PlacesUtils.bookmarks.setItemTitle(query.itemId, "badName");
+     if ("concreteId" in query)
+       PlacesUtils.bookmarks.setItemTitle(query.concreteId, "badName");
+   }
+ 
+-  PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
++  restoreLeftPaneGetters();
+ 
+   // Open Library, this will kick-off left pane code.
+   openLibrary(onLibraryReady);
+ }
+diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
+--- a/browser/components/places/tests/browser/head.js
++++ b/browser/components/places/tests/browser/head.js
+@@ -1,29 +1,36 @@
+ ChromeUtils.defineModuleGetter(this, "NetUtil",
+   "resource://gre/modules/NetUtil.jsm");
+ ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
+   "resource://testing-common/PlacesTestUtils.jsm");
+ ChromeUtils.defineModuleGetter(this, "TestUtils",
+   "resource://testing-common/TestUtils.jsm");
+ 
+-// We need to cache this before test runs...
+-var cachedLeftPaneFolderIdGetter;
+-var getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+-if (!cachedLeftPaneFolderIdGetter && typeof(getter) == "function") {
+-  cachedLeftPaneFolderIdGetter = getter;
++// We need to cache these before test runs...
++let leftPaneGetters = new Map([["leftPaneFolderId", null],
++                               ["allBookmarksFolderId", null]]);
++for (let [key, val] of leftPaneGetters) {
++  if (!val) {
++    let getter = Object.getOwnPropertyDescriptor(PlacesUIUtils, key).get;
++    if (typeof getter == "function") {
++      leftPaneGetters.set(key, getter);
++    }
++  }
+ }
+ 
+-// ...And restore it when test ends.
+-registerCleanupFunction(function() {
+-  let updatedGetter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+-  if (cachedLeftPaneFolderIdGetter && typeof(updatedGetter) != "function") {
+-    PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
++// ...And restore them when test ends.
++function restoreLeftPaneGetters() {
++  for (let [key, getter] of leftPaneGetters) {
++    Object.defineProperty(PlacesUIUtils, key, {
++      enumerable: true, configurable: true, get: getter
++    });
+   }
+-});
++}
++registerCleanupFunction(restoreLeftPaneGetters);
+ 
+ function openLibrary(callback, aLeftPaneRoot) {
+   let library = window.openDialog("chrome://browser/content/places/places.xul",
+                                   "", "chrome,toolbar=yes,dialog=no,resizable",
+                                   aLeftPaneRoot);
+   waitForFocus(function() {
+     callback(library);
+   }, library);
+diff --git a/browser/components/places/tests/chrome/test_0_bug510634.xul b/browser/components/places/tests/chrome/test_0_bug510634.xul
+--- a/browser/components/places/tests/chrome/test_0_bug510634.xul
++++ b/browser/components/places/tests/chrome/test_0_bug510634.xul
+@@ -39,29 +39,39 @@
+      *
+      * Ensures that properties for special queries are set on their tree nodes,
+      * even if PlacesUIUtils.leftPaneFolderId was not initialized.
+      */
+ 
+     SimpleTest.waitForExplicitFinish();
+ 
+     function runTest() {
+-      // We need to cache and restore this getter in order to simulate
+-      // Bug 510634
+-      let cachedLeftPaneFolderIdGetter =
+-        PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+-      // Must also cache and restore this getter as it is affected by
+-      // leftPaneFolderId, from bug 564900.
+-      let cachedAllBookmarksFolderIdGetter =
+-        PlacesUIUtils.__lookupGetter__("allBookmarksFolderId");
++      // We need to cache and restore the getters in order to simulate
++      // Bug 510634.
++      let leftPaneGetters = new Map([["leftPaneFolderId", null],
++                               ["allBookmarksFolderId", null]]);
++      for (let [key, val] of leftPaneGetters) {
++        if (!val) {
++          let getter = Object.getOwnPropertyDescriptor(PlacesUIUtils, key).get;
++          if (typeof getter == "function") {
++            leftPaneGetters.set(key, getter);
++          }
++        }
++      }
++
++      function restoreLeftPaneGetters() {
++        for (let [key, getter] of leftPaneGetters) {
++          Object.defineProperty(PlacesUIUtils, key, {
++            enumerable: true, configurable: true, get: getter
++          });
++        }
++      }
+ 
+       let leftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+-
+-      // restore the getter
+-      PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
++      restoreLeftPaneGetters();
+ 
+       // Setup the places tree contents.
+       let tree = document.getElementById("tree");
+       tree.place = "place:queryType=1&folder=" + leftPaneFolderId;
+ 
+       // The query-property is set on the title column for each row.
+       let titleColumn = tree.treeBoxObject.columns.getColumnAt(0);
+ 
+@@ -82,18 +92,16 @@
+           ok(found, "OrganizerQuery_" + aQueryName + " is set");
+         }
+       );
+ 
+       // Close the root node
+       tree.result.root.containerOpen = false;
+ 
+       // Restore the getters for the next test.
+-      PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+-      PlacesUIUtils.__defineGetter__("allBookmarksFolderId",
+-                                     cachedAllBookmarksFolderIdGetter);
++      restoreLeftPaneGetters();
+ 
+       SimpleTest.finish();
+     }
+ 
+   ]]>
+   </script>
+ </window>
+diff --git a/browser/components/places/tests/unit/test_PUIU_livemarksCache.js b/browser/components/places/tests/unit/test_PUIU_livemarksCache.js
+deleted file mode 100644
+--- a/browser/components/places/tests/unit/test_PUIU_livemarksCache.js
++++ /dev/null
+@@ -1,40 +0,0 @@
+-"use strict";
+-
+-let {IsLivemark} = Cu.import("resource:///modules/PlacesUIUtils.jsm", {});
+-
+-add_task(function test_livemark_cache_builtin_folder() {
+-  // This test checks a basic livemark, and also initializes the observer for
+-  // updates to the bookmarks.
+-  Assert.ok(!IsLivemark(PlacesUtils.unfiledBookmarksFolderId),
+-    "unfiled bookmarks should not be seen as a livemark");
+-});
+-
+-add_task(async function test_livemark_add_and_remove_items() {
+-  let bookmark = await PlacesUtils.bookmarks.insert({
+-    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+-    title: "Grandsire",
+-    url: "http://example.com",
+-  });
+-
+-  let bookmarkId = await PlacesUtils.promiseItemId(bookmark.guid);
+-
+-  Assert.ok(!IsLivemark(bookmarkId),
+-    "a simple bookmark should not be seen as a livemark");
+-
+-  let livemark = await PlacesUtils.livemarks.addLivemark({
+-    title: "Stedman",
+-    feedURI: Services.io.newURI("http://livemark.com/"),
+-    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+-  });
+-
+-  let livemarkId = await PlacesUtils.promiseItemId(livemark.guid);
+-
+-  Assert.ok(IsLivemark(livemarkId),
+-    "a livemark should be reported as a livemark");
+-
+-  // Now remove the livemark.
+-  await PlacesUtils.livemarks.removeLivemark(livemark);
+-
+-  Assert.ok(!IsLivemark(livemarkId),
+-    "the livemark should have been removed from the cache");
+-});
+diff --git a/browser/components/places/tests/unit/xpcshell.ini b/browser/components/places/tests/unit/xpcshell.ini
+--- a/browser/components/places/tests/unit/xpcshell.ini
++++ b/browser/components/places/tests/unit/xpcshell.ini
+@@ -17,10 +17,9 @@ support-files =
+ [test_browserGlue_migrate.js]
+ [test_browserGlue_prefs.js]
+ [test_browserGlue_restore.js]
+ [test_browserGlue_smartBookmarks.js]
+ [test_browserGlue_urlbar_defaultbehavior_migration.js]
+ [test_clearHistory_shutdown.js]
+ [test_leftpane_corruption_handling.js]
+ [test_PUIU_batchUpdatesForNode.js]
+-[test_PUIU_livemarksCache.js]
+ [test_PUIU_makeTransaction.js]
+diff --git a/toolkit/components/places/nsLivemarkService.js b/toolkit/components/places/nsLivemarkService.js
+--- a/toolkit/components/places/nsLivemarkService.js
++++ b/toolkit/components/places/nsLivemarkService.js
+@@ -347,16 +347,19 @@ LivemarkService.prototype = {
+       if (livemarksMap.has(guid)) {
+         let livemark = livemarksMap.get(guid);
+         livemark.terminate();
+         livemarksMap.delete(guid);
+       }
+     });
+   },
+ 
++  skipDescendantsOnItemRemoval: false,
++  skipTags: true,
++
+   // nsINavHistoryObserver
+ 
+   onPageChanged() {},
+   onTitleChanged() {},
+   onDeleteVisits() {},
+ 
+   onClearHistory() {
+     this._promiseLivemarksMap().then(livemarksMap => {

+ 203 - 0
mozilla-release/patches/1408686-58a1.patch

@@ -0,0 +1,203 @@
+# HG changeset patch
+# User Kit Cambridge <kit@yakshaving.ninja>
+# Date 1508186723 25200
+# Node ID 0a9f04d67bd0c4bb76933ed13eb55e7828be48a4
+# Parent  768e902b52694c85e23e3bf3353627b866698454
+Bug 1408686 - Add a maintenance task to set missing last modified date and date added for bookmarks. r=mak
+
+For bookmarks without an added date, we'll fall back to the last
+modified date, earliest visit date, and current time, in that
+order. For missing last modified dates, we'll try the date added,
+latest visit date, and current time.
+
+MozReview-Commit-ID: 9sCs1y20S3r
+
+diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm
+--- a/toolkit/components/places/PlacesDBUtils.jsm
++++ b/toolkit/components/places/PlacesDBUtils.jsm
+@@ -866,16 +866,32 @@ var PlacesDBUtils = {
+ 
+     // S.2 drop tombstones for bookmarks that aren't deleted.
+     cleanupStatements.push({
+       query:
+       `DELETE FROM moz_bookmarks_deleted
+        WHERE guid IN (SELECT guid FROM moz_bookmarks)`,
+     });
+ 
++    // S.3 set missing added and last modified dates.
++    cleanupStatements.push({
++      query:
++      `UPDATE moz_bookmarks
++       SET dateAdded = COALESCE(dateAdded, lastModified, (
++             SELECT MIN(visit_date) FROM moz_historyvisits
++             WHERE place_id = fk
++           ), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000),
++           lastModified = COALESCE(lastModified, dateAdded, (
++             SELECT MAX(visit_date) FROM moz_historyvisits
++             WHERE place_id = fk
++           ), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000)
++       WHERE dateAdded IS NULL OR
++             lastModified IS NULL`,
++    });
++
+     // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT!
+ 
+     return cleanupStatements;
+   },
+ 
+   /**
+    * Tries to vacuum the database.
+    *
+diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
++++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+@@ -1396,16 +1396,149 @@ tests.push({
+   },
+ 
+   async check() {
+     let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+     Assert.deepEqual(tombstones.map(info => info.guid), ["bookmarkBBBB"]);
+   },
+ });
+ 
++tests.push({
++  name: "S.3",
++  desc: "set missing added and last modified dates",
++  _placeVisits: [],
++  _bookmarksWithDates: [],
++
++  async setup() {
++    let placeIdWithVisits = addPlace();
++    this._placeVisits.push({
++      placeId: placeIdWithVisits,
++      visitDate: PlacesUtils.toPRTime(new Date(2017, 9, 4)),
++    }, {
++      placeId: placeIdWithVisits,
++      visitDate: PlacesUtils.toPRTime(new Date(2017, 9, 8)),
++    });
++
++    this._bookmarksWithDates.push({
++      guid: "bookmarkAAAA",
++      placeId: null,
++      parentId: bs.bookmarksMenuFolder,
++      dateAdded: null,
++      lastModified: PlacesUtils.toPRTime(new Date(2017, 9, 1)),
++    }, {
++      guid: "bookmarkBBBB",
++      placeId: null,
++      parentId: bs.bookmarksMenuFolder,
++      dateAdded: PlacesUtils.toPRTime(new Date(2017, 9, 2)),
++      lastModified: null,
++    }, {
++      guid: "bookmarkCCCC",
++      placeId: null,
++      parentId: bs.unfiledBookmarksFolder,
++      dateAdded: null,
++      lastModified: null,
++    }, {
++      guid: "bookmarkDDDD",
++      placeId: placeIdWithVisits,
++      parentId: bs.mobileFolder,
++      dateAdded: null,
++      lastModified: null,
++    }, {
++      guid: "bookmarkEEEE",
++      placeId: placeIdWithVisits,
++      parentId: bs.unfiledBookmarksFolder,
++      dateAdded: PlacesUtils.toPRTime(new Date(2017, 9, 3)),
++      lastModified: PlacesUtils.toPRTime(new Date(2017, 9, 6)),
++    });
++
++    await PlacesUtils.withConnectionWrapper(
++      "Insert bookmarks and visits with dates",
++      db => db.executeTransaction(async () => {
++        await db.executeCached(`
++          INSERT INTO moz_historyvisits(place_id, visit_date)
++          VALUES(:placeId, :visitDate)`,
++          this._placeVisits);
++
++        await db.executeCached(`
++          INSERT INTO moz_bookmarks(fk, type, parent, guid, dateAdded,
++                                    lastModified)
++          VALUES(:placeId, 1, :parentId, :guid, :dateAdded,
++                 :lastModified)`,
++          this._bookmarksWithDates);
++      })
++    );
++  },
++
++  async check() {
++    let db = await PlacesUtils.promiseDBConnection();
++    let updatedRows = await db.executeCached(`
++      SELECT guid, dateAdded, lastModified
++      FROM moz_bookmarks
++      WHERE guid IN (?, ?, ?, ?, ?)`,
++      this._bookmarksWithDates.map(info => info.guid));
++
++    for (let row of updatedRows) {
++      let guid = row.getResultByName("guid");
++
++      let dateAdded = row.getResultByName("dateAdded");
++      Assert.ok(Number.isInteger(dateAdded));
++
++      let lastModified = row.getResultByName("lastModified");
++      Assert.ok(Number.isInteger(lastModified));
++
++      switch (guid) {
++        // Last modified date exists, so we should use it for date added.
++        case "bookmarkAAAA": {
++          let expectedInfo = this._bookmarksWithDates[0];
++          Assert.equal(dateAdded, expectedInfo.lastModified);
++          Assert.equal(lastModified, expectedInfo.lastModified);
++          break;
++        }
++
++        // Date added exists, so we should use it for last modified date.
++        case "bookmarkBBBB": {
++          let expectedInfo = this._bookmarksWithDates[1];
++          Assert.equal(dateAdded, expectedInfo.dateAdded);
++          Assert.equal(lastModified, expectedInfo.dateAdded);
++          break;
++        }
++
++        // Neither date added nor last modified exists, and no visits, so we
++        // should fall back to the current time for both.
++        case "bookmarkCCCC": {
++          let nowAsPRTime = PlacesUtils.toPRTime(new Date());
++          Assert.equal(dateAdded, lastModified);
++          Assert.ok(dateAdded <= nowAsPRTime);
++          break;
++        }
++
++        // Neither date added nor last modified exists, but we have two
++        // visits, so we should fall back to the earliest and latest visit
++        // dates.
++        case "bookmarkDDDD": {
++          let oldestVisit = this._placeVisits[0];
++          Assert.equal(dateAdded, oldestVisit.visitDate);
++          let newestVisit = this._placeVisits[1];
++          Assert.equal(lastModified, newestVisit.visitDate);
++          break;
++        }
++
++        // We have two visits, but both date added and last modified exist,
++        // so we shouldn't update them.
++        case "bookmarkEEEE": {
++          let expectedInfo = this._bookmarksWithDates[4];
++          Assert.equal(dateAdded, expectedInfo.dateAdded);
++          Assert.equal(lastModified, expectedInfo.lastModified);
++          break;
++        }
++      }
++    }
++  },
++});
++
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
+   name: "Z",
+   desc: "Sanity: Preventive maintenance does not touch valid items",
+ 
+   _uri1: uri("http://www1.mozilla.org"),
+   _uri2: uri("http://www2.mozilla.org"),

+ 128 - 0
mozilla-release/patches/1408687-58a1.patch

@@ -0,0 +1,128 @@
+# HG changeset patch
+# User Kit Cambridge <kit@yakshaving.ninja>
+# Date 1508188527 25200
+# Node ID 30b79f7b92917a0ec18b0575239c3cfbd1904074
+# Parent  27bc43d5d11ced40c6014d9055825993935d185e
+Bug 1408687 - Add a maintenance task to reset missing or invalid Place GUIDs. r=mak
+
+MozReview-Commit-ID: JPqXnDsg07q
+
+diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm
+--- a/toolkit/components/places/PlacesDBUtils.jsm
++++ b/toolkit/components/places/PlacesDBUtils.jsm
+@@ -780,16 +780,26 @@ this.PlacesDBUtils = {
+     cleanupStatements.push(fixForeignCount);
+ 
+     // L.5 recalculate missing hashes.
+     let fixMissingHashes = {
+       query: `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`
+     };
+     cleanupStatements.push(fixMissingHashes);
+ 
++    // L.6 fix invalid Place GUIDs.
++    let fixInvalidPlaceGuids = {
++      query:
++      `UPDATE moz_places
++       SET guid = GENERATE_GUID()
++       WHERE guid IS NULL OR
++             NOT IS_VALID_GUID(guid)`
++    };
++    cleanupStatements.push(fixInvalidPlaceGuids);
++
+     // MOZ_BOOKMARKS
+     // S.1 fix invalid GUIDs for synced bookmarks.
+     //     This requires multiple related statements.
+     //     First, we insert tombstones for all synced bookmarks with invalid
+     //     GUIDs, so that we can delete them on the server. Second, we add a
+     //     temporary trigger to bump the change counter for the parents of any
+     //     items we update, since Sync stores the list of child GUIDs on the
+     //     parent. Finally, we assign new GUIDs for all items with missing and
+diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
++++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+@@ -34,21 +34,22 @@ function cleanDatabase() {
+   mDBConn.executeSimpleSQL("DELETE FROM moz_items_annos");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_inputhistory");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_keywords");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_icons");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_pages_w_icons");
+   mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE id > " + defaultBookmarksMaxId);
+ }
+ 
+-function addPlace(aUrl, aFavicon) {
++function addPlace(aUrl, aFavicon, aGuid = PlacesUtils.history.makeGuid()) {
+   let href = new URL(aUrl || "http://www.mozilla.org").href;
+   let stmt = mDBConn.createStatement(
+-    "INSERT INTO moz_places (url, url_hash) VALUES (:url, hash(:url))");
++    "INSERT INTO moz_places (url, url_hash, guid) VALUES (:url, hash(:url), :guid)");
+   stmt.params.url = href;
++  stmt.params.guid = aGuid;
+   stmt.execute();
+   stmt.finalize();
+   let id = mDBConn.lastInsertRowID;
+   if (aFavicon) {
+     stmt = mDBConn.createStatement(
+       "INSERT INTO moz_pages_w_icons (page_url, page_url_hash) VALUES (:url, hash(:url))");
+     stmt.params.url = href;
+     stmt.execute();
+@@ -1236,16 +1237,59 @@ tests.push({
+   async check() {
+     Assert.ok((await this._getHash()) > 0);
+   }
+ });
+ 
+ // ------------------------------------------------------------------------------
+ 
+ tests.push({
++  name: "L.6",
++  desc: "fix invalid Place GUIDs",
++  _placeIds: [],
++
++  async setup() {
++    let placeWithValidGuid = addPlace("http://example.com/a", null,
++                                      "placeAAAAAAA");
++    this._placeIds.push(placeWithValidGuid);
++
++    let placeWithEmptyGuid = addPlace("http://example.com/b", null, "");
++    this._placeIds.push(placeWithEmptyGuid);
++
++    let placeWithoutGuid = addPlace("http://example.com/c", null, null);
++    this._placeIds.push(placeWithoutGuid);
++
++    let placeWithInvalidGuid = addPlace("http://example.com/c", null,
++                                        "{123456}");
++    this._placeIds.push(placeWithInvalidGuid);
++  },
++
++  async check() {
++    let db = await PlacesUtils.promiseDBConnection();
++    let updatedRows = await db.execute(`
++      SELECT id, guid
++      FROM moz_places
++      WHERE id IN (?, ?, ?, ?)`,
++      this._placeIds);
++
++    for (let row of updatedRows) {
++      let id = row.getResultByName("id");
++      let guid = row.getResultByName("guid");
++      if (id == this._placeIds[0]) {
++        Assert.equal(guid, "placeAAAAAAA");
++      } else {
++        Assert.ok(PlacesUtils.isValidGuid(guid));
++      }
++    }
++  },
++});
++
++// ------------------------------------------------------------------------------
++
++tests.push({
+   name: "S.1",
+   desc: "fix invalid GUIDs for synced bookmarks",
+   _bookmarkInfos: [],
+ 
+   async setup() {
+     await PlacesTestUtils.markBookmarksAsSynced();
+ 
+     let folderWithInvalidGuid = addBookmark(
+

+ 6 - 0
mozilla-release/patches/series

@@ -7701,3 +7701,9 @@ TOP-NOBUG-1712804fix-25320.patch
 1737436-use-mozilla-compat-version-define-25320.patch
 1925041-removebinary-25320.patch
 1476333-fix-25320.patch
+1386513-58a1.patch
+1405563-58a1.patch
+1405722-58a1.patch
+1408687-58a1.patch
+1408686-58a1.patch
+1301622-58a1.patch