Bug 1084203 Fix totally empty bookmark manager (Port Bug 1068671 Remove 'read-only' folders support from places) r=Neil
authorstefanh@inbox.com
Sun, 26 Oct 2014 06:00:18 +0800
changeset 16955 072eb34fcd1f8de4c0c76b6509e3e2beb5741480
parent 16954 5f2cc63708bc940f0408b9e532739d6d92ab7bd3
child 16956 209d0c9e55e61bf200d59f781da12912f8ac7a09
push id10534
push userphilip.chee@gmail.com
push dateSat, 25 Oct 2014 22:01:33 +0000
treeherdercomm-central@072eb34fcd1f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersNeil
bugs1084203, 1068671
Bug 1084203 Fix totally empty bookmark manager (Port Bug 1068671 Remove 'read-only' folders support from places) r=Neil a=Callek for CLOSED TREE
suite/common/places/browserPlacesViews.js
suite/common/places/controller.js
suite/common/places/menu.xml
suite/common/places/tests/browser/browser_423515.js
suite/common/places/treeView.js
suite/common/src/PlacesUIUtils.jsm
--- a/suite/common/places/browserPlacesViews.js
+++ b/suite/common/places/browserPlacesViews.js
@@ -1307,17 +1307,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.indexOf(this._rootElt.childNodes, elt);
       if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
-          !PlacesUtils.nodeIsReadOnly(elt._placesNode)) {
+          !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) {
         // 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 =
--- a/suite/common/places/controller.js
+++ b/suite/common/places/controller.js
@@ -183,17 +183,17 @@ PlacesController.prototype = {
     case "placesCmd_reload":
       // Livemark containers
       var selectedNode = this._view.selectedNode;
       return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
     case "placesCmd_sortBy:name":
       var selectedNode = this._view.selectedNode;
       return selectedNode &&
              PlacesUtils.nodeIsFolder(selectedNode) &&
-             !PlacesUtils.nodeIsReadOnly(selectedNode) &&
+             !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
              this._view.result.sortingMode ==
                  Components.interfaces.nsINavHistoryQueryOptions.SORT_BY_NONE;
     case "placesCmd_createBookmark":
       var node = this._view.selectedNode;
       return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
     default:
       return false;
     }
@@ -304,31 +304,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 (PlacesUtils.nodeIsFolder(nodes[i]) &&
-            !PlacesControllerDragHelper.canMoveNode(nodes[i]))
-          return false;
-
-        // We don't call nodeIsReadOnly here, because nodeIsReadOnly means that
-        // a node has children that cannot be edited, reordered or removed. Here,
-        // we don't care if a node's children can't be reordered or edited, just
-        // that they're removable. All history results have removable children
-        // (based on the principle that any URL in the history table should be
-        // removable), but some special bookmark folders may have non-removable
-        // children, e.g. live bookmark folder children. It doesn't make sense
-        // to delete a child of a live bookmark folder, since when the folder
-        // refreshes, the child will return.
-        var parent = nodes[i].parent || root;
-        if (PlacesUtils.isReadonlyFolder(parent))
+        if (!PlacesUIUtils.canUserRemove(nodes[i]))
           return false;
       }
     }
 
     return true;
   },
 
   /**
@@ -1440,69 +1426,27 @@ let PlacesControllerDragHelper = {
    * Determines if a node can be moved.
    *
    * @param   aNode
    *          A nsINavHistoryResultNode node.
    * @returns True if the node can be moved, false otherwise.
    */
   canMoveNode:
   function PCDH_canMoveNode(aNode) {
-    // Can't move query root.
-    if (!aNode.parent)
-      return false;
-
-    let parentId = PlacesUtils.getConcreteItemId(aNode.parent);
-    let concreteId = PlacesUtils.getConcreteItemId(aNode);
-
-    // Can't move children of tag containers.
-    if (PlacesUtils.nodeIsTagQuery(aNode.parent))
-      return false;
-
-    // Can't move children of read-only containers.
-    if (PlacesUtils.nodeIsReadOnly(aNode.parent))
-      return false;
-
-    // Check for special folders, etc.
-    if (PlacesUtils.nodeIsContainer(aNode) &&
-        !this.canMoveContainer(aNode.itemId, parentId))
+    // Only bookmark items are movable.
+    if (aNode.itemId == -1)
       return false;
-
-    return true;
-  },
-
-  /**
-   * Determines if a container node can be moved.
-   *
-   * @param   aId
-   *          A bookmark folder id.
-   * @param   [optional] aParentId
-   *          The parent id of the folder.
-   * @returns True if the container can be moved to the target.
-   */
-  canMoveContainer:
-  function PCDH_canMoveContainer(aId, aParentId) {
-    if (aId == -1)
-      return false;
-
-    // Disallow moving of roots and special folders.
-    const ROOTS = [PlacesUtils.placesRootId, PlacesUtils.bookmarksMenuFolderId,
-                   PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId,
-                   PlacesUtils.toolbarFolderId];
-    if (ROOTS.indexOf(aId) != -1)
-      return false;
-
-    // Get parent id if necessary.
-    if (aParentId == null || aParentId == -1)
-      aParentId = PlacesUtils.bookmarks.getFolderIdForItem(aId);
-
-    if (PlacesUtils.bookmarks.getFolderReadonly(aParentId))
-      return false;
-
-    return true;
-  },
+    // Once tags and bookmarked are divorced, the tag-query check should be
+    // removed.
+    let parentNode = aNode.parent;
+    return parentNode &&
+           !(PlacesUtils.nodeIsFolder(parentNode) &&
+             PlacesUIUtils.isContentsReadOnly(parentNode)) &&
+           !PlacesUtils.nodeIsTagQuery(parentNode);
+    },
 
   /**
    * Handles the drop of one or more items onto a view.
    * @param   insertionPoint
    *          The insertion point where the items should be dropped
    */
   onDrop: function PCDH_onDrop(insertionPoint, dt) {
     let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1;
@@ -1564,22 +1508,20 @@ let PlacesControllerDragHelper = {
 
   /**
    * Checks if we can insert into a container.
    * @param   aContainer
    *          The container were we are want to drop
    */
   disallowInsertion: function(aContainer) {
     NS_ASSERT(aContainer, "empty container");
-    // Allow dropping into Tag containers.
-    if (PlacesUtils.nodeIsTagQuery(aContainer))
-      return false;
-    // Disallow insertion of items under readonly folders.
-    return (!PlacesUtils.nodeIsFolder(aContainer) ||
-             PlacesUtils.nodeIsReadOnly(aContainer));
+    // Allow dropping into Tag containers and editable folders.
+    return !PlacesUtils.nodeIsTagQuery(aContainer) &&
+           (!PlacesUtils.nodeIsFolder(aContainer) ||
+            PlacesUIUtils.isContentsReadOnly(aContainer));
   },
 
   placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
                   PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
                   PlacesUtils.TYPE_X_MOZ_PLACE],
 
   // The order matters.
   GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
--- a/suite/common/places/menu.xml
+++ b/suite/common/places/menu.xml
@@ -103,19 +103,19 @@
               let isMenu = elt.localName == "menu" ||
                  (elt.localName == "toolbarbutton" &&
                   elt.getAttribute("type") == "menu");
               if (isMenu && elt.lastChild &&
                   elt.lastChild.hasAttribute("placespopup"))
                 dropPoint.folderElt = elt;
               return dropPoint;
             }
-            else if ((PlacesUtils.nodeIsFolder(elt._placesNode) ||
-                      PlacesUtils.nodeIsTagQuery(elt._placesNode)) &&
-                     !PlacesUtils.nodeIsReadOnly(elt._placesNode)) {
+            else if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
+                      !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) ||
+                     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(
                                     PlacesUtils.getConcreteItemId(resultNode),
                                     -1,
                                     Components.interfaces.nsITreeView.DROP_BEFORE,
                                     PlacesUtils.nodeIsTagQuery(elt._placesNode),
--- a/suite/common/places/tests/browser/browser_423515.js
+++ b/suite/common/places/tests/browser/browser_423515.js
@@ -22,18 +22,16 @@ function test() {
   tests.push({
     populate: function() {
       this.id =
         PlacesUtils.bookmarks.createFolder(rootId, "", IDX);
     },
     validate: function() {
       is(rootNode.childCount, 1,
         "populate added data to the test root");
-      is(PlacesControllerDragHelper.canMoveContainer(this.id),
-         true, "can move regular folder id");
       is(PlacesControllerDragHelper.canMoveNode(rootNode.getChild(0)),
          true, "can move regular folder node");
     }
   });
 
   // add a regular folder shortcut, should be moveable
   tests.push({
     populate: function() {
@@ -52,19 +50,16 @@ function test() {
 
       var shortcutNode = rootNode.getChild(1);
       is(shortcutNode.type, 9, "node is folder shortcut");
       is(this.shortcutId, shortcutNode.itemId, "shortcut id and shortcut node item id match");
 
       var concreteId = PlacesUtils.getConcreteItemId(shortcutNode);
       is(concreteId, folderNode.itemId, "shortcut node id and concrete id match");
 
-      is(PlacesControllerDragHelper.canMoveContainer(this.shortcutId),
-         true, "can move folder shortcut id");
-
       is(PlacesControllerDragHelper.canMoveNode(shortcutNode),
          true, "can move folder shortcut node");
     }
   });
 
   // add a regular query, should be moveable
   tests.push({
     populate: function() {
@@ -78,19 +73,16 @@ function test() {
         "populated data to the test root");
 
       var bmNode = rootNode.getChild(0);
       is(bmNode.itemId, this.bookmarkId, "bookmark id and bookmark node item id match");
 
       var queryNode = rootNode.getChild(1);
       is(queryNode.itemId, this.queryId, "query id and query node item id match");
 
-      is(PlacesControllerDragHelper.canMoveContainer(this.queryId),
-         true, "can move query id");
-
       is(PlacesControllerDragHelper.canMoveNode(queryNode),
          true, "can move query node");
     }
   });
 
   // test that special folders cannot be moved
   // test that special folders shortcuts can be moved
   tests.push({
@@ -122,33 +114,26 @@ function test() {
         node.containerOpen = false;
         ok(false, "Unable to find child node");
         return null;
       }
 
       for (var i = 0; i < this.folders.length; i++) {
         var id = this.folders[i];
 
-        is(PlacesControllerDragHelper.canMoveContainer(id),
-           false, "shouldn't be able to move special folder id");
-
         var node = getRootChildNode(id);
         isnot(node, null, "Node found");
         is(PlacesControllerDragHelper.canMoveNode(node),
            false, "shouldn't be able to move special folder node");
 
         var shortcutId = this.shortcuts[id];
         var shortcutNode = rootNode.getChild(i);
 
         is(shortcutNode.itemId, shortcutId, "shortcut id and shortcut node item id match");
 
-        dump("can move shortcut id?\n");
-        is(PlacesControllerDragHelper.canMoveContainer(shortcutId),
-           true, "should be able to move special folder shortcut id");
-
         dump("can move shortcut node?\n");
         is(PlacesControllerDragHelper.canMoveNode(shortcutNode),
            true, "should be able to move special folder shortcut node");
       }
     }
   });
 
   // test that a tag container cannot be moved
@@ -172,48 +157,16 @@ function test() {
 
       is(PlacesControllerDragHelper.canMoveNode(tagNode),
          false, "should not be able to move tag container node");
 
       tagsNode.containerOpen = false;
     }
   });
 
-  // test that any child of a read-only node cannot be moved
-  tests.push({
-    populate: function() {
-      this.id =
-        PlacesUtils.bookmarks.createFolder(rootId, "foo", IDX);
-      PlacesUtils.bookmarks.createFolder(this.id, "bar", IDX);
-      PlacesUtils.bookmarks.setFolderReadonly(this.id, true);
-    },
-    validate: function() {
-      is(rootNode.childCount, 1,
-        "populate added data to the test root");
-      var readOnlyFolder = rootNode.getChild(0);
-
-      // test that we can move the read-only folder
-      is(PlacesControllerDragHelper.canMoveContainer(this.id),
-         true, "can move read-only folder id");
-      is(PlacesControllerDragHelper.canMoveNode(readOnlyFolder),
-         true, "can move read-only folder node");
-
-      // test that we cannot move the child of a read-only folder
-      readOnlyFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode);
-      readOnlyFolder.containerOpen = true;
-      var childFolder = readOnlyFolder.getChild(0);
-
-      is(PlacesControllerDragHelper.canMoveContainer(childFolder.itemId),
-         false, "cannot move a child of a read-only folder");
-      is(PlacesControllerDragHelper.canMoveNode(childFolder),
-         false, "cannot move a child node of a read-only folder node");
-      readOnlyFolder.containerOpen = false;
-    }
-  });
-
   tests.forEach(function(aTest) {
     PlacesUtils.bookmarks.removeFolderChildren(rootId);
     aTest.populate();
     aTest.validate();
   });
 
   rootNode.containerOpen = false;
   PlacesUtils.bookmarks.removeItem(rootId);
--- a/suite/common/places/treeView.js
+++ b/suite/common/places/treeView.js
@@ -1601,33 +1601,49 @@ PlacesTreeView.prototype = {
     this._result.sortingMode = newSort;
   },
 
   isEditable: function PTV_isEditable(aRow, aColumn) {
     // At this point we only support editing the title field.
     if (aColumn.index != 0)
       return false;
 
-    // Only bookmark-nodes are editable, and those are never built lazily
     let node = this._rows[aRow];
-    if (!node || node.itemId == -1)
+    if (!node) {
+      Components.utils.reportError("isEditable called for an unbuilt row.");
+      return false;
+    }
+    let itemId = node.itemId;
+
+    // Only bookmark-nodes are editable.  Fortunately, this checks also takes
+    // care of livemark children.
+    if (itemId == -1)
       return false;
 
-    // The following items are never editable:
-    // * Read-only items.
+    // The following items are also not editable, even though they are bookmark
+    // items.
     // * places-roots
+    // * the left pane special folders and queries (those are place: uri
+    //   bookmarks)
     // * separators
-    if (PlacesUtils.nodeIsReadOnly(node) ||
-        PlacesUtils.nodeIsSeparator(node))
+    //
+    // Note that concrete itemIds aren't used intentionally.  For example, we
+    // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar,
+    // except for the one under All Bookmarks.
+    if (PlacesUtils.nodeIsSeparator(node) || PlacesUtils.isRootItem(itemId))
       return false;
 
-    if (PlacesUtils.nodeIsFolder(node)) {
-      let itemId = PlacesUtils.getConcreteItemId(node);
-      if (PlacesUtils.isRootItem(itemId))
-        return false;
+    let parentId = PlacesUtils.getConcreteItemId(node.parent);
+    if (parentId == PlacesUIUtils.leftPaneFolderId ||
+        parentId == PlacesUIUtils.allBookmarksFolderId) {
+      // Note that the for the time being this is the check that actually
+      // blocks renaming places "roots", and not the isRootItem check above.
+      // That's because places root are only exposed through folder shortcuts
+      // descendants of the left pane folder.
+      return false;
     }
 
     return true;
   },
 
   setCellText: function PTV_setCellText(aRow, aColumn, aText) {
     // We may only get here if the cell is editable.
     let node = this._rows[aRow];
--- a/suite/common/src/PlacesUIUtils.jsm
+++ b/suite/common/src/PlacesUIUtils.jsm
@@ -8,16 +8,53 @@ var EXPORTED_SYMBOLS = ["PlacesUIUtils"]
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
   Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
   return PlacesUtils;
 });
 
+// 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 = {
+      QueryInterface: XPCOMUtils.generateQI(Components.interfaces.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 PlacesUIUtils = {
   ORGANIZER_LEFTPANE_VERSION: 6,
   ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder",
   ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
 
   LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
   DESCRIPTION_ANNO: "bookmarkProperties/description",
 
@@ -731,16 +768,99 @@ var PlacesUIUtils = {
    * not set.
    */
   getItemDescription: function PUIU_getItemDescription(aItemId) {
     if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO))
       return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO);
     return "";
   },
 
+   /**
+   * 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.
+   * @return true if the aNode represents a removable entry, false otherwise.
+   */
+  canUserRemove: function (aNode) {
+    let parentNode = aNode.parent;
+    if (!parentNode)
+      throw new Error("canUserRemove doesn't accept root nodes");
+
+    // If it's not a bookmark, we can remove it unless it's a child of a
+    // livemark.
+    if (aNode.itemId == -1) {
+      // Rather than executing a db query, checking the existence of the feedURI
+      // annotation, detect livemark children by the fact that they are the only
+      // direct non-bookmark children of bookmark folders.
+      return !PlacesUtils.nodeIsFolder(parentNode);
+    }
+
+    // Otherwise it has to be a child of an editable folder.
+    return !this.isContentsReadOnly(parentNode);
+  },
+
+  /**
+   * 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
+   * 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).
+   *
+   * @param aNodeOrItemId
+   *        any item id or result node.
+   * @throws if aNodeOrItemId is neither an item id nor a folder result node.
+   * @note livemark "folders" are considered read-only (but see bug 1072833).
+   * @return true if aItemId points to a read-only folder, false otherwise.
+   */
+  isContentsReadOnly: function (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");
+    }
+
+    if (itemId == PlacesUtils.placesRootId || IsLivemark(itemId))
+      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"))
+      return false;
+
+    return itemId == this.leftPaneFolderId ||
+           itemId == this.allBookmarksFolderId;;
+  },
+
   /**
    * Gives the user a chance to cancel loading lots of tabs at once
    */
   _confirmOpenInTabs: function PUIU__confirmOpenInTabs(numTabsToOpen) {
     const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen";
     var reallyOpen = true;
 
     if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) {
@@ -1068,18 +1188,16 @@ var PlacesUIUtils = {
       create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) {
               // Left Pane Root Folder.
         let folderId = bs.createFolder(aParentId,
                                        queries[aFolderName].title,
                                        bs.DEFAULT_INDEX);
         // We should never backup this, since it changes between profiles.
         as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
                              0, as.EXPIRE_NEVER);
-        // Disallow manipulating this folder within the organizer UI.
-        bs.setFolderReadonly(folderId, true);
 
         if (aIsRoot) {
           // Mark as special left pane root.
           as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
                                PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
                                0, as.EXPIRE_NEVER);
         }
         else {