Bug 484019 - Fix corrupt or wrong roots titles in the database and in the Library, r=dietrich
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 24 Apr 2009 15:11:06 +0200
changeset 27715 b52c15e23bd274463bfab7f94b450bb564a27fc5
parent 27714 0498e448db6460ae33a671b4cc7281649adb2888
child 27716 8f5f0cf6c611618f198b3868b2285a58bf09af66
push id6718
push usermak77@bonardo.net
push dateFri, 24 Apr 2009 14:45:56 +0000
treeherdermozilla-central@b52c15e23bd2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdietrich
bugs484019
milestone1.9.2a1pre
Bug 484019 - Fix corrupt or wrong roots titles in the database and in the Library, r=dietrich
browser/components/places/content/utils.js
browser/components/places/tests/browser/Makefile.in
browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
browser/components/places/tests/browser/browser_library_multiple_left_panes.js
toolkit/components/places/src/PlacesDBUtils.jsm
toolkit/components/places/src/nsNavBookmarks.cpp
toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
toolkit/components/places/tests/unit/test_preventive_maintenance.js
--- a/browser/components/places/content/utils.js
+++ b/browser/components/places/content/utils.js
@@ -1143,154 +1143,163 @@ var PlacesUIUtils = {
   },
 
   get leftPaneQueries() {    
     // build the map
     this.leftPaneFolderId;
     return this.leftPaneQueries;
   },
 
-  // get the folder id for the organizer left-pane folder
+  // Get the folder id for the organizer left-pane folder.
   get leftPaneFolderId() {
     var leftPaneRoot = -1;
     var allBookmarksId;
-    var items = PlacesUtils.annotations
-                           .getItemsWithAnnotation(ORGANIZER_FOLDER_ANNO, {});
+
+    // Shortcuts to services.
+    var bs = PlacesUtils.bookmarks;
+    var as = PlacesUtils.annotations;
+
+    // Get all items marked as being the left pane folder.  We should only have
+    // one of them.
+    var items = as.getItemsWithAnnotation(ORGANIZER_FOLDER_ANNO, {});
     if (items.length > 1) {
       // Something went wrong, we cannot have more than one left pane folder,
-      // remove all left pane folders and generate a correct new one.
-      items.forEach(function(aItem) {
-        PlacesUtils.bookmarks.removeItem(aItem);
-      });
+      // remove all left pane folders and continue.  We will create a new one.
+      items.forEach(bs.removeItem);
     }
     else if (items.length == 1 && items[0] != -1) {
       leftPaneRoot = items[0];
-      // check organizer left pane version
-      var version = PlacesUtils.annotations
-                               .getItemAnnotation(leftPaneRoot, ORGANIZER_FOLDER_ANNO);
+      // Check organizer left pane version.
+      var version = as.getItemAnnotation(leftPaneRoot, ORGANIZER_FOLDER_ANNO);
       if (version != ORGANIZER_LEFTPANE_VERSION) {
         // If version is not valid we must rebuild the left pane.
-        PlacesUtils.bookmarks.removeItem(leftPaneRoot);
+        bs.removeItem(leftPaneRoot);
         leftPaneRoot = -1;
       }
     }
 
+    var queriesTitles = {
+      "PlacesRoot": "",
+      "History": this.getString("OrganizerQueryHistory"),
+      // TODO: Bug 489681, Tags needs its own string in places.properties
+      "Tags": bs.getItemTitle(PlacesUtils.tagsFolderId),
+      "AllBookmarks": this.getString("OrganizerQueryAllBookmarks"),
+      "Downloads": this.getString("OrganizerQueryDownloads"),
+      "BookmarksToolbar": null,
+      "BookmarksMenu": null,
+      "UnfiledBookmarks": null
+    };
+
     if (leftPaneRoot != -1) {
-      // Build the leftPaneQueries Map
+      // A valid left pane folder has been found.
+      // Build the leftPaneQueries Map.  This is used to quickly access them
+      // associating a mnemonic name to the real item ids.
       delete this.leftPaneQueries;
       this.leftPaneQueries = {};
-      var items = PlacesUtils.annotations
-                             .getItemsWithAnnotation(ORGANIZER_QUERY_ANNO, {});
-      for (var i=0; i < items.length; i++) {
-        var queryName = PlacesUtils.annotations
-                                   .getItemAnnotation(items[i], ORGANIZER_QUERY_ANNO);
+      var items = as.getItemsWithAnnotation(ORGANIZER_QUERY_ANNO, {});
+      // While looping through queries we will also check for titles validity.
+      for (var i = 0; i < items.length; i++) {
+        var queryName = as.getItemAnnotation(items[i], ORGANIZER_QUERY_ANNO);
         this.leftPaneQueries[queryName] = items[i];
+        // Titles could have been corrupted or the user could have changed his
+        // locale.  Check title is correctly set and eventually fix it.
+        if (bs.getItemTitle(items[i]) != queriesTitles[queryName])
+          bs.setItemTitle(items[i], queriesTitles[queryName]);
       }
       delete this.leftPaneFolderId;
       return this.leftPaneFolderId = leftPaneRoot;
     }
 
     var self = this;
-    const EXPIRE_NEVER = PlacesUtils.annotations.EXPIRE_NEVER;
     var callback = {
-      runBatched: function(aUserData) {
+      // Helper to create an organizer special query.
+      create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) {
+        let itemId = bs.insertBookmark(aParentId,
+                                       PlacesUtils._uri(aQueryUrl),
+                                       bs.DEFAULT_INDEX,
+                                       queriesTitles[aQueryName]);
+        // Mark as special organizer query.
+        as.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO, aQueryName,
+                             0, as.EXPIRE_NEVER);
+        // We should never backup this, since it changes between profiles.
+        as.setItemAnnotation(itemId, EXCLUDE_FROM_BACKUP_ANNO, 1,
+                             0, as.EXPIRE_NEVER);
+        // Add to the queries map.
+        self.leftPaneQueries[aQueryName] = itemId;
+        return itemId;
+      },
+
+      // Helper to create an organizer special folder.
+      create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) {
+              // Left Pane Root Folder.
+        let folderId = bs.createFolder(aParentId,
+                                       queriesTitles[aFolderName],
+                                       bs.DEFAULT_INDEX);
+        // We should never backup this, since it changes between profiles.
+        as.setItemAnnotation(folderId, 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, ORGANIZER_FOLDER_ANNO,
+                               ORGANIZER_LEFTPANE_VERSION,
+                               0, as.EXPIRE_NEVER);
+        }
+        else {
+          // Mark as special organizer folder.
+          as.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO, aFolderName,
+                           0, as.EXPIRE_NEVER);
+          self.leftPaneQueries[aFolderName] = folderId;
+        }
+        return folderId;
+      },
+
+      runBatched: function CB_runBatched(aUserData) {
         delete self.leftPaneQueries;
         self.leftPaneQueries = { };
 
-        // Left Pane Root Folder
-        leftPaneRoot = PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId, "", -1);
-        // ensure immediate children can't be removed
-        PlacesUtils.bookmarks.setFolderReadonly(leftPaneRoot, true);
+        // Left Pane Root Folder.
+        leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true);
 
-        // History Query
-        let uri = PlacesUtils._uri("place:type=" +
-                                   Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY +
-                                   "&sort=4");
-        let title = self.getString("OrganizerQueryHistory");
-        let itemId = PlacesUtils.bookmarks.insertBookmark(leftPaneRoot, uri, -1, title);
-        PlacesUtils.annotations.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO,
-                                                  "History", 0, EXPIRE_NEVER);
-        PlacesUtils.annotations.setItemAnnotation(itemId,
-                                                  EXCLUDE_FROM_BACKUP_ANNO,
-                                                  1, 0, EXPIRE_NEVER);
-        self.leftPaneQueries["History"] = itemId;
+        // History Query.
+        this.create_query("History", leftPaneRoot,
+                          "place:type=" +
+                          Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY +
+                          "&sort=" +
+                          Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
 
-        // XXX: Downloads
+        // XXX: Downloads.
 
-        // Tags Query
-        uri = PlacesUtils._uri("place:type=" +
+        // Tags Query.
+        this.create_query("Tags", leftPaneRoot,
+                          "place:type=" +
                           Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY +
                           "&sort=" +
                           Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
-        title = PlacesUtils.bookmarks.getItemTitle(PlacesUtils.tagsFolderId);
-        itemId = PlacesUtils.bookmarks.insertBookmark(leftPaneRoot, uri, -1, title);
-        PlacesUtils.annotations.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO,
-                                                  "Tags", 0, EXPIRE_NEVER);
-        PlacesUtils.annotations.setItemAnnotation(itemId,
-                                                  EXCLUDE_FROM_BACKUP_ANNO,
-                                                  1, 0, EXPIRE_NEVER);
-        self.leftPaneQueries["Tags"] = itemId;
+
+        // All Bookmarks Folder.
+        allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false);
 
-        // All Bookmarks Folder
-        title = self.getString("OrganizerQueryAllBookmarks");
-        itemId = PlacesUtils.bookmarks.createFolder(leftPaneRoot, title, -1);
-        allBookmarksId = itemId;
-        PlacesUtils.annotations.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO,
-                                                  "AllBookmarks", 0, EXPIRE_NEVER);
-        PlacesUtils.annotations.setItemAnnotation(itemId,
-                                                  EXCLUDE_FROM_BACKUP_ANNO,
-                                                  1, 0, EXPIRE_NEVER);
-        self.leftPaneQueries["AllBookmarks"] = itemId;
-
-        // disallow manipulating this folder within the organizer UI
-        PlacesUtils.bookmarks.setFolderReadonly(allBookmarksId, true);
+        // All Bookmarks->Bookmarks Toolbar Query.
+        this.create_query("BookmarksToolbar", allBookmarksId,
+                          "place:folder=TOOLBAR");
 
-        // All Bookmarks->Bookmarks Toolbar Query
-        uri = PlacesUtils._uri("place:folder=TOOLBAR");
-        itemId = PlacesUtils.bookmarks.insertBookmark(allBookmarksId, uri, -1, null);
-        PlacesUtils.annotations.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO,
-                                                  "BookmarksToolbar", 0, EXPIRE_NEVER);
-        PlacesUtils.annotations.setItemAnnotation(itemId,
-                                                  EXCLUDE_FROM_BACKUP_ANNO,
-                                                  1, 0, EXPIRE_NEVER);
-        self.leftPaneQueries["BookmarksToolbar"] = itemId;
+        // All Bookmarks->Bookmarks Menu Query.
+        this.create_query("BookmarksMenu", allBookmarksId,
+                          "place:folder=BOOKMARKS_MENU");
 
-        // All Bookmarks->Bookmarks Menu Query
-        uri = PlacesUtils._uri("place:folder=BOOKMARKS_MENU");
-        itemId = PlacesUtils.bookmarks.insertBookmark(allBookmarksId, uri, -1, null);
-        PlacesUtils.annotations.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO,
-                                                  "BookmarksMenu", 0, EXPIRE_NEVER);
-        PlacesUtils.annotations.setItemAnnotation(itemId,
-                                                  EXCLUDE_FROM_BACKUP_ANNO,
-                                                  1, 0, EXPIRE_NEVER);
-        self.leftPaneQueries["BookmarksMenu"] = itemId;
-
-        // All Bookmarks->Unfiled bookmarks
-        uri = PlacesUtils._uri("place:folder=UNFILED_BOOKMARKS");
-        itemId = PlacesUtils.bookmarks.insertBookmark(allBookmarksId, uri, -1, null);
-        PlacesUtils.annotations.setItemAnnotation(itemId, ORGANIZER_QUERY_ANNO,
-                                                  "UnfiledBookmarks", 0,
-                                                  EXPIRE_NEVER);
-        PlacesUtils.annotations.setItemAnnotation(itemId,
-                                                  EXCLUDE_FROM_BACKUP_ANNO,
-                                                  1, 0, EXPIRE_NEVER);
-        self.leftPaneQueries["UnfiledBookmarks"] = itemId;
-
-        // disallow manipulating this folder within the organizer UI
-        PlacesUtils.bookmarks.setFolderReadonly(leftPaneRoot, true);
+        // All Bookmarks->Unfiled Bookmarks Query.
+        this.create_query("UnfiledBookmarks", allBookmarksId,
+                          "place:folder=UNFILED_BOOKMARKS");
       }
     };
-    PlacesUtils.bookmarks.runInBatchMode(callback, null);
-    PlacesUtils.annotations.setItemAnnotation(leftPaneRoot,
-                                              ORGANIZER_FOLDER_ANNO,
-                                              ORGANIZER_LEFTPANE_VERSION,
-                                              0, EXPIRE_NEVER);
-    PlacesUtils.annotations.setItemAnnotation(leftPaneRoot,
-                                              EXCLUDE_FROM_BACKUP_ANNO,
-                                              1, 0, EXPIRE_NEVER);
+    bs.runInBatchMode(callback, null);
+
     delete this.leftPaneFolderId;
     return this.leftPaneFolderId = leftPaneRoot;
   },
 
   get allBookmarksFolderId() {
     // ensure the left-pane root is initialized;
     this.leftPaneFolderId;
     delete this.allBookmarksFolderId;
--- a/browser/components/places/tests/browser/Makefile.in
+++ b/browser/components/places/tests/browser/Makefile.in
@@ -40,24 +40,26 @@ srcdir		= @srcdir@
 VPATH			= @srcdir@
 relativesrcdir  = browser/components/places/tests/browser
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
 	browser_0_library_left_pane_migration.js \
+	browser_library_left_pane_fixnames.js \
 	browser_425884.js \
 	browser_423515.js \
 	browser_410196_paste_into_tags.js \
 	browser_457473_no_copy_guid.js \
 	browser_sort_in_library.js \
 	browser_library_open_leak.js \
 	browser_library_panel_leak.js \
 	browser_library_search.js \
 	browser_history_sidebar_search.js \
 	browser_bookmarksProperties.js \
 	browser_forgetthissite_single.js \
 	browser_library_left_pane_commands.js \
+	browser_library_multiple_left_panes.js \
 	$(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
@@ -0,0 +1,123 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Places test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Corp.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Marco Bonardo <mak77@bonardo.net> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by devaring the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not devare
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ *  Test we correctly fix broken Library left pane queries names.
+ */
+
+var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+         getService(Ci.nsIWindowWatcher);
+
+// Array of left pane queries objects, each one has the following properties:
+// name: query's identifier got from annotations,
+// itemId: query's itemId,
+// correctTitle: original and correct query's title.
+var leftPaneQueries = [];
+
+var windowObserver = {
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic === "domwindowopened") {
+      ww.unregisterNotification(this);
+      var organizer = aSubject.QueryInterface(Ci.nsIDOMWindow);
+      organizer.addEventListener("load", function onLoad(event) {
+        organizer.removeEventListener("load", onLoad, false);
+        executeSoon(function () {
+          // Check titles have been fixed.
+          for (var i = 0; i < leftPaneQueries.length; i++) {
+            var query = leftPaneQueries[i];
+            is(PlacesUtils.bookmarks.getItemTitle(query.itemId),
+               query.correctTitle, "Title is correct for query " + query.name);
+          }
+
+          // Close Library window.
+          organizer.close();
+          // No need to cleanup anything, we have a correct left pane now.
+          finish();
+        });
+      }, false);
+    }
+  }
+};
+
+function test() {
+  waitForExplicitFinish();
+  // Sanity checks.
+  ok(PlacesUtils, "PlacesUtils is running in chrome context");
+  ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+  ok(ORGANIZER_LEFTPANE_VERSION > 0,
+     "Left pane version in chrome context, current version is: " + ORGANIZER_LEFTPANE_VERSION );
+
+  // Ensure left pane is initialized.
+  ok(PlacesUIUtils.leftPaneFolderId > 0, "left pane folder is initialized");
+
+  // get the left pane folder already set, remove it eventually.
+  var leftPaneItems = PlacesUtils.annotations
+                                 .getItemsWithAnnotation(ORGANIZER_FOLDER_ANNO, {});
+
+  is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+  // Check version.
+  var version = PlacesUtils.annotations
+                           .getItemAnnotation(leftPaneItems[0],
+                                              ORGANIZER_FOLDER_ANNO);
+  is(version, ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+
+  // Get all left pane queries.
+  var items = PlacesUtils.annotations
+                         .getItemsWithAnnotation(ORGANIZER_QUERY_ANNO, {});
+  // Get current queries names.
+  for (var i = 0; i < items.length; i++) {
+    var itemId = items[i];
+    var queryName = PlacesUtils.annotations
+                               .getItemAnnotation(items[i],
+                                                  ORGANIZER_QUERY_ANNO);
+    leftPaneQueries.push({ name: queryName,
+                           itemId: itemId,
+                           correctTitle: PlacesUtils.bookmarks
+                                                    .getItemTitle(itemId) });
+    // Rename to a bad title.
+    PlacesUtils.bookmarks.setItemTitle(itemId, "badName");
+  }
+
+  // Open Library, this will kick-off left pane code.
+  ww.registerNotification(windowObserver);
+  ww.openWindow(null,
+                "chrome://browser/content/places/places.xul",
+                "",
+                "chrome,toolbar=yes,dialog=no,resizable",
+                null);
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_multiple_left_panes.js
@@ -0,0 +1,118 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Places test code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Corp.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *  Marco Bonardo <mak77@bonardo.net> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by devaring the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not devare
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ *  Test we correctly migrate Library left pane to the latest version.
+ *  Note: this test MUST be the first between browser chrome tests, or results
+ *        of next tests could be unexpected due to PlacesUIUtils getters.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+         getService(Ci.nsIWindowWatcher);
+
+var gFakeLeftPanes = [];
+
+var windowObserver = {
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic === "domwindowopened") {
+      ww.unregisterNotification(this);
+      var organizer = aSubject.QueryInterface(Ci.nsIDOMWindow);
+      organizer.addEventListener("load", function onLoad(event) {
+        organizer.removeEventListener("load", onLoad, false);
+        executeSoon(function () {
+          // Check left pane.
+          ok(PlacesUIUtils.leftPaneFolderId > 0,
+             "Left pane folder correctly created");
+          var leftPaneItems =
+            PlacesUtils.annotations
+                       .getItemsWithAnnotation(ORGANIZER_FOLDER_ANNO, {});
+          is(leftPaneItems.length, 1,
+             "We correctly have only 1 left pane folder");
+
+          // Check that all old left pane items have been removed.
+          gFakeLeftPanes.forEach(function(aItemId) {
+            try {
+              PlacesUtils.bookmarks.getItemTitle(aItemId);
+              throw("This folder should have been removed");
+            } catch (ex) {}
+          });
+
+          // Close Library window.
+          organizer.close();
+          // No need to cleanup anything, we have a correct left pane now.
+          finish();
+        });
+      }, false);
+    }
+  }
+};
+
+function test() {
+  waitForExplicitFinish();
+  // Sanity checks.
+  ok(PlacesUtils, "PlacesUtils is running in chrome context");
+  ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+  ok(ORGANIZER_LEFTPANE_VERSION > 0,
+     "Left pane version in chrome context, current version is: " + ORGANIZER_LEFTPANE_VERSION );
+
+  // We need 2 left pane folders.
+  do {
+    let leftPaneItems = PlacesUtils.annotations
+                                 .getItemsWithAnnotation(ORGANIZER_FOLDER_ANNO, {});
+    // Create a fake left pane folder.
+    let fakeLeftPaneRoot =
+      PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId, "",
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX);
+    PlacesUtils.annotations.setItemAnnotation(fakeLeftPaneRoot,
+                                              ORGANIZER_FOLDER_ANNO,
+                                              ORGANIZER_LEFTPANE_VERSION,
+                                              0,
+                                              PlacesUtils.annotations.EXPIRE_NEVER);
+    gFakeLeftPanes.push(fakeLeftPaneRoot);
+  } while (gFakeLeftPanes.length < 2);
+
+  // Open Library, this will fix the left pane.
+  ww.registerNotification(windowObserver);
+  ww.openWindow(null,
+                "chrome://browser/content/places/places.xul",
+                "",
+                "chrome,toolbar=yes,dialog=no,resizable",
+                null);
+}
--- a/toolkit/components/places/src/PlacesDBUtils.jsm
+++ b/toolkit/components/places/src/PlacesDBUtils.jsm
@@ -225,24 +225,52 @@ nsPlacesDBUtils.prototype = {
         "SELECT id FROM moz_annos a " +
         "WHERE NOT EXISTS " +
           "(SELECT id FROM moz_places_temp WHERE id = a.place_id LIMIT 1) " +
         "AND NOT EXISTS " +
           "(SELECT id FROM moz_places WHERE id = a.place_id LIMIT 1) " +
       ")");
     cleanupStatements.push(deleteOrphanAnnos);
 
-/* XXX needs test
     // MOZ_BOOKMARKS_ROOTS
-    // C.1 fix roots titles
+    // C.1 fix missing Places root
+    //     Bug 477739 shows a case where the root could be wrongly removed
+    //     due to an endianness issue.  We try to fix broken roots here.
+    let selectPlacesRoot = this._dbConn.createStatement(
+      "SELECT id FROM moz_bookmarks WHERE id = :places_root");
+    selectPlacesRoot.params["places_root"] = this._bms.placesRoot;
+    if (!selectPlacesRoot.executeStep()) {
+      // We are missing the root, try to recreate it.
+      let createPlacesRoot = this._dbConn.createStatement(
+        "INSERT INTO moz_bookmarks (id, type, fk, parent, position, title) " +
+        "VALUES (:places_root, 2, NULL, 0, 0, :title)");
+      createPlacesRoot.params["places_root"] = this._bms.placesRoot;
+      createPlacesRoot.params["title"] = "";
+      cleanupStatements.push(createPlacesRoot);
+
+      // Now ensure that other roots are children of Places root.
+      let fixPlacesRootChildren = this._dbConn.createStatement(
+        "UPDATE moz_bookmarks SET parent = :places_root WHERE id IN " +
+          "(SELECT folder_id FROM moz_bookmarks_roots " +
+            "WHERE folder_id <> :places_root)");
+      fixPlacesRootChildren.params["places_root"] = this._bms.placesRoot;
+      cleanupStatements.push(fixPlacesRootChildren);
+    }
+    selectPlacesRoot.finalize();
+
+    // C.2 fix roots titles
     //     some alpha version has wrong roots title, and this also fixes them if
     //     locale has changed.
-    //     note: we should update the library left pane folders in the frontend.
     let updateRootTitleSql = "UPDATE moz_bookmarks SET title = :title " +
                              "WHERE id = :root_id AND title <> :title";
+    // root
+    let fixPlacesRootTitle = this._dbConn.createStatement(updateRootTitleSql);
+    fixPlacesRootTitle.params["root_id"] = this._bms.placesRoot;
+    fixPlacesRootTitle.params["title"] = "";
+    cleanupStatements.push(fixPlacesRootTitle);
     // bookmarks menu
     let fixBookmarksMenuTitle = this._dbConn.createStatement(updateRootTitleSql);
     fixBookmarksMenuTitle.params["root_id"] = this._bms.bookmarksMenuFolder;
     fixBookmarksMenuTitle.params["title"] =
       this._bundle.GetStringFromName("BookmarksMenuFolderTitle");
     cleanupStatements.push(fixBookmarksMenuTitle);
     // bookmarks toolbar
     let fixBookmarksToolbarTitle = this._dbConn.createStatement(updateRootTitleSql);
@@ -257,41 +285,16 @@ nsPlacesDBUtils.prototype = {
       this._bundle.GetStringFromName("UnsortedBookmarksFolderTitle");
     cleanupStatements.push(fixUnsortedBookmarksTitle);
     // tags
     let fixTagsRootTitle = this._dbConn.createStatement(updateRootTitleSql);
     fixTagsRootTitle.params["root_id"] = this._bms.tagsFolder;
     fixTagsRootTitle.params["title"] =
       this._bundle.GetStringFromName("TagsFolderTitle");
     cleanupStatements.push(fixTagsRootTitle);
-*/
-
-    // C.2 fix missing Places root
-    //     Bug 477739 shows a case where the root could be wrongly removed
-    //     due to an endianness issue.  We try to fix broken roots here.
-    let selectPlacesRoot = this._dbConn.createStatement(
-      "SELECT id FROM moz_bookmarks WHERE id = :places_root");
-    selectPlacesRoot.params["places_root"] = this._bms.placesRoot;
-    if (!selectPlacesRoot.executeStep()) {
-      // We are missing the root, try to recreate it.
-      let createPlacesRoot = this._dbConn.createStatement(
-        "INSERT INTO moz_bookmarks (id, type, fk, parent, position, title) " +
-        "VALUES (:places_root, 2, NULL, 0, 0, NULL)");
-      createPlacesRoot.params["places_root"] = this._bms.placesRoot;
-      cleanupStatements.push(createPlacesRoot);
-
-      // Now ensure that other roots are children of Places root.
-      let fixPlacesRootChildren = this._dbConn.createStatement(
-        "UPDATE moz_bookmarks SET parent = :places_root WHERE id IN " +
-          "(SELECT folder_id FROM moz_bookmarks_roots " +
-            "WHERE folder_id <> :places_root)");
-      fixPlacesRootChildren.params["places_root"] = this._bms.placesRoot;
-      cleanupStatements.push(fixPlacesRootChildren);
-    }
-    selectPlacesRoot.finalize();
 
     // MOZ_BOOKMARKS
     // D.1 remove items without a valid place
     // if fk IS NULL we fix them in D.7
     let deleteNoPlaceItems = this._dbConn.createStatement(
       "DELETE FROM moz_bookmarks WHERE id NOT IN ( " +
         "SELECT folder_id FROM moz_bookmarks_roots " + // skip roots
       ") AND id IN (" +
--- a/toolkit/components/places/src/nsNavBookmarks.cpp
+++ b/toolkit/components/places/src/nsNavBookmarks.cpp
@@ -2248,17 +2248,21 @@ nsNavBookmarks::GetItemIdForGUID(const n
 NS_IMETHODIMP
 nsNavBookmarks::SetItemTitle(PRInt64 aItemId, const nsACString &aTitle)
 {
   nsCOMPtr<mozIStorageStatement> statement;
   nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
       "UPDATE moz_bookmarks SET title = ?1, lastModified = ?2 WHERE id = ?3"),
     getter_AddRefs(statement));
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = statement->BindUTF8StringParameter(0, aTitle);
+  // Support setting a null title, we support this in insertBookmark.
+  if (aTitle.IsVoid())
+    rv = mDBInsertBookmark->BindNullParameter(0);
+  else
+    rv = statement->BindUTF8StringParameter(0, aTitle);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->BindInt64Parameter(1, PR_Now());
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->BindInt64Parameter(2, aItemId);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = statement->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
@@ -0,0 +1,77 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Places unit test code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Drew Willcoxon <mak77@bonardo.net> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Both SetItemtitle and insertBookmark should allow for null titles.
+ */
+
+const bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+           getService(Ci.nsINavBookmarksService);
+
+const TEST_URL = "http://www.mozilla.org";
+
+function run_test() {
+  // Insert a bookmark with an empty title.
+  var itemId = bs.insertBookmark(bs.toolbarFolder,
+                                 uri(TEST_URL),
+                                 bs.DEFAULT_INDEX,
+                                 "");
+  // Check returned title is an empty string.
+  do_check_eq(bs.getItemTitle(itemId), "");
+  // Set title to null.
+  bs.setItemTitle(itemId, null);
+  // Check returned title is null.
+  do_check_eq(bs.getItemTitle(itemId), null);
+  // Cleanup.
+  bs.removeItem(itemId);
+
+  // Insert a bookmark with a null title.
+  itemId = bs.insertBookmark(bs.toolbarFolder,
+                             uri(TEST_URL),
+                             bs.DEFAULT_INDEX,
+                             null);
+  // Check returned title is null.
+  do_check_eq(bs.getItemTitle(itemId), null);
+  // Set title to an empty string.
+  bs.setItemTitle(itemId, "");
+  // Check returned title is an empty string.
+  do_check_eq(bs.getItemTitle(itemId), "");
+  // Cleanup.
+  bs.removeItem(itemId);
+}
--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
@@ -42,30 +42,35 @@
   * correct fix steps, without polluting valid data.
   */
 
 // Include PlacesDBUtils module
 Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
 
 const FINISHED_MAINTANANCE_NOTIFICATION_TOPIC = "places-maintenance-finished";
 
+const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
+
 // Get services and database connection
 let os = Cc["@mozilla.org/observer-service;1"].
          getService(Ci.nsIObserverService);
 let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
          getService(Ci.nsINavHistoryService);
 let bh = hs.QueryInterface(Ci.nsIBrowserHistory);
 let bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
          getService(Ci.nsINavBookmarksService);
 let ts = Cc["@mozilla.org/browser/tagging-service;1"].
          getService(Ci.nsITaggingService);
 let as = Cc["@mozilla.org/browser/annotation-service;1"].
          getService(Ci.nsIAnnotationService);
 let fs = Cc["@mozilla.org/browser/favicon-service;1"].
          getService(Ci.nsIFaviconService);
+let bundle = Cc["@mozilla.org/intl/stringbundle;1"].
+             getService(Ci.nsIStringBundleService).
+             createBundle(PLACES_STRING_BUNDLE_URI);
 
 let mDBConn = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
 
 //------------------------------------------------------------------------------
 // Helpers
 
 let defaultBookmarksMaxId = 0;
 function cleanDatabase() {
@@ -257,33 +262,18 @@ tests.push({
     // Check that annotation to a not existant page has been removed
     stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE place_id = 1337");
     do_check_false(stmt.executeStep());
     stmt.finalize();
   }
 });
 
 //------------------------------------------------------------------------------
-//XXX TODO
 tests.push({
   name: "C.1",
-  desc: "Fix roots titles",
-
-  setup: function() {
-
-  },
-
-  check: function() {
-
-  }
-});
-
-//------------------------------------------------------------------------------
-tests.push({
-  name: "C.2",
   desc: "fix missing Places root",
 
   setup: function() {
     // Sanity check: ensure that roots are intact.
     do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
     do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
     do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
     do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
@@ -305,16 +295,45 @@ tests.push({
     do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
     do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
     do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
     do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
   }
 });
 
 //------------------------------------------------------------------------------
+tests.push({
+  name: "C.2",
+  desc: "Fix roots titles",
+
+  setup: function() {
+    // Sanity check: ensure that roots titles are correct. We can use our check.
+    this.check();
+    // Change some roots' titles.
+    bs.setItemTitle(bs.placesRoot, "bad title");
+    do_check_eq(bs.getItemTitle(bs.placesRoot), "bad title");
+    bs.setItemTitle(bs.unfiledBookmarksFolder, "bad title");
+    do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder), "bad title");
+  },
+
+  check: function() {
+    // Ensure all roots titles are correct.
+    do_check_eq(bs.getItemTitle(bs.placesRoot), "");
+    do_check_eq(bs.getItemTitle(bs.bookmarksMenuFolder),
+                bundle.GetStringFromName("BookmarksMenuFolderTitle"));
+    do_check_eq(bs.getItemTitle(bs.tagsFolder),
+                bundle.GetStringFromName("TagsFolderTitle"));
+    do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder),
+                bundle.GetStringFromName("UnsortedBookmarksFolderTitle"));
+    do_check_eq(bs.getItemTitle(bs.toolbarFolder),
+                bundle.GetStringFromName("BookmarksToolbarFolderTitle"));
+  }
+});
+
+//------------------------------------------------------------------------------
 
 tests.push({
   name: "D.1",
   desc: "Remove items without a valid place",
 
   _validItemId: null,
   _invalidItemId: null,
   _placeId: null,