author | Marco Bonardo <mbonardo@mozilla.com> |
Tue, 04 Feb 2014 14:43:20 +0100 | |
changeset 167567 | af5cdb31f131d9d3fcda9c4d95f4adb50346c2d7 |
parent 167566 | 3a10c46057955b7000a0ad917094d7fb668bb3db |
child 167568 | 9fb9effa74a6ddf68d15c02dca1ca7c21476a150 |
push id | 26174 |
push user | kwierso@gmail.com |
push date | Sat, 08 Feb 2014 00:55:48 +0000 |
treeherder | mozilla-central@2c873eff7dc2 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | mano |
bugs | 824433 |
milestone | 30.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -474,17 +474,17 @@ pref("browser.ctrlTab.recentlyUsedLimit" // If true, at shutdown the bookmarks in your menu and toolbar will // be exported as HTML to the bookmarks.html file. pref("browser.bookmarks.autoExportHTML", false); // The maximum number of daily bookmark backups to // keep in {PROFILEDIR}/bookmarkbackups. Special values: // -1: unlimited // 0: no backups created (and deletes all existing backups) -pref("browser.bookmarks.max_backups", 10); +pref("browser.bookmarks.max_backups", 15); // Scripts & Windows prefs pref("dom.disable_open_during_load", true); pref("javascript.options.showInConsole", true); #ifdef DEBUG pref("general.warnOnAboutConfig", false); #endif
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -77,26 +77,30 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", + "resource:///modules/AsyncShutdown.jsm"); + const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser"; const PREF_PLUGINS_UPDATEURL = "plugins.update.url"; -// We try to backup bookmarks at idle times, to avoid doing that at shutdown. -// Number of idle seconds before trying to backup bookmarks. 10 minutes. -const BOOKMARKS_BACKUP_IDLE_TIME = 10 * 60; -// Minimum interval in milliseconds between backups. -const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000; -// Maximum number of backups to create. Old ones will be purged. -const BOOKMARKS_BACKUP_MAX_BACKUPS = 10; +// Seconds of idle before trying to create a bookmarks backup. +const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60; +// Minimum interval between backups. We try to not create more than one backup +// per interval. +const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1; +// Maximum interval between backups. If the last backup is older than these +// days we will try to create a new one more aggressively. +const BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS = 5; // Factory object const BrowserGlueServiceFactory = { _instance: null, createInstance: function BGSF_createInstance(outer, iid) { if (outer != null) throw Components.results.NS_ERROR_NO_AGGREGATION; return this._instance == null ? @@ -129,17 +133,16 @@ function BrowserGlue() { #ifndef XP_MACOSX # OS X has the concept of zero-window sessions and therefore ignores the # browser-lastwindow-close-* topics. #define OBSERVE_LASTWINDOW_CLOSE_TOPICS 1 #endif BrowserGlue.prototype = { _saveSession: false, - _isIdleObserver: false, _isPlacesInitObserver: false, _isPlacesLockedObserver: false, _isPlacesShutdownObserver: false, _isPlacesDatabaseLocked: false, _migrationImportsDefaultBookmarks: false, _setPrefToSaveSession: function BG__setPrefToSaveSession(aForce) { if (!this._saveSession && !aForce) @@ -257,18 +260,17 @@ BrowserGlue.prototype = { if (this._isPlacesShutdownObserver) { Services.obs.removeObserver(this, "places-shutdown"); this._isPlacesShutdownObserver = false; } // places-shutdown is fired when the profile is about to disappear. this._onPlacesShutdown(); break; case "idle": - if (this._idleService.idleTime > BOOKMARKS_BACKUP_IDLE_TIME * 1000) - this._backupBookmarks(); + this._backupBookmarks(); break; case "distribution-customization-complete": Services.obs.removeObserver(this, "distribution-customization-complete"); // Customization has finished, we don't need the customizer anymore. delete this._distributionCustomizer; break; case "browser-glue-test": // used by tests if (data == "post-update-notification") { @@ -417,18 +419,20 @@ BrowserGlue.prototype = { os.removeObserver(this, "browser-lastwindow-close-requested"); os.removeObserver(this, "browser-lastwindow-close-granted"); #endif #ifdef MOZ_SERVICES_SYNC os.removeObserver(this, "weave:service:ready"); os.removeObserver(this, "weave:engine:clients:display-uri"); #endif os.removeObserver(this, "session-save"); - if (this._isIdleObserver) - this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); + if (this._bookmarksBackupIdleTime) { + this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime); + delete this._bookmarksBackupIdleTime; + } if (this._isPlacesInitObserver) os.removeObserver(this, "places-init-complete"); if (this._isPlacesLockedObserver) os.removeObserver(this, "places-database-locked"); if (this._isPlacesShutdownObserver) os.removeObserver(this, "places-shutdown"); os.removeObserver(this, "handle-xul-text-link"); os.removeObserver(this, "profile-before-change"); @@ -1055,24 +1059,28 @@ BrowserGlue.prototype = { Services.prefs.getBoolPref("browser.bookmarks.restore_default_bookmarks"); if (restoreDefaultBookmarks) { // Ensure that we already have a bookmarks backup for today. yield this._backupBookmarks(); importBookmarks = true; } } catch(ex) {} + // This may be reused later, check for "=== undefined" to see if it has + // been populated already. + let lastBackupFile; + // If the user did not require to restore default bookmarks, or import // from bookmarks.html, we will try to restore from JSON if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) { // get latest JSON backup - var bookmarksBackupFile = yield PlacesBackups.getMostRecent("json"); - if (bookmarksBackupFile) { + lastBackupFile = yield PlacesBackups.getMostRecentBackup("json"); + if (lastBackupFile) { // restore from JSON backup - yield BookmarkJSONUtils.importFromFile(bookmarksBackupFile, true); + yield BookmarkJSONUtils.importFromFile(lastBackupFile, true); importBookmarks = false; } else { // We have created a new database but we don't have any backup available importBookmarks = true; var dirService = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); var bookmarksHTMLFile = dirService.get("BMarks", Ci.nsILocalFile); @@ -1157,100 +1165,95 @@ BrowserGlue.prototype = { if (importBookmarksHTML) Services.prefs.setBoolPref("browser.places.importBookmarksHTML", false); if (restoreDefaultBookmarks) Services.prefs.setBoolPref("browser.bookmarks.restore_default_bookmarks", false); } // Initialize bookmark archiving on idle. - // Once a day, either on idle or shutdown, bookmarks are backed up. - if (!this._isIdleObserver) { - this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); - this._isIdleObserver = true; + if (!this._bookmarksBackupIdleTime) { + this._bookmarksBackupIdleTime = BOOKMARKS_BACKUP_IDLE_TIME_SEC; + + // If there is no backup, or the last bookmarks backup is too old, use + // a more aggressive idle observer. + if (lastBackupFile === undefined) + lastBackupFile = yield PlacesBackups.getMostRecentBackup(); + if (!lastBackupFile) { + this._bookmarksBackupIdleTime /= 2; + } + else { + let lastBackupTime = PlacesBackups.getDateForFile(lastBackupFile); + let profileLastUse = Services.appinfo.replacedLockTime || Date.now(); + + // If there is a backup after the last profile usage date it's fine, + // regardless its age. Otherwise check how old is the last + // available backup compared to that session. + if (profileLastUse > lastBackupTime) { + let backupAge = Math.round((profileLastUse - lastBackupTime) / 86400000); + // Report the age of the last available backup. + try { + Services.telemetry + .getHistogramById("PLACES_BACKUPS_DAYSFROMLAST") + .add(backupAge); + } catch (ex) { + Components.utils.reportError("Unable to report telemetry."); + } + + if (backupAge > BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS) + this._bookmarksBackupIdleTime /= 2; + } + } + this._idleService.addIdleObserver(this, this._bookmarksBackupIdleTime); } Services.obs.notifyObservers(null, "places-browser-init-complete", ""); }.bind(this)); }, /** * Places shut-down tasks - * - back up bookmarks if needed. + * - finalize components depending on Places. * - export bookmarks as HTML, if so configured. - * - finalize components depending on Places. */ _onPlacesShutdown: function BG__onPlacesShutdown() { this._sanitizer.onShutdown(); PageThumbs.uninit(); - if (this._isIdleObserver) { - this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); - this._isIdleObserver = false; + if (this._bookmarksBackupIdleTime) { + this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime); + delete this._bookmarksBackupIdleTime; } - let waitingForBackupToComplete = true; - this._backupBookmarks().then( - function onSuccess() { - waitingForBackupToComplete = false; - }, - function onFailure() { - Cu.reportError("Unable to backup bookmarks."); - waitingForBackupToComplete = false; + // Support legacy bookmarks.html format for apps that depend on that format. + try { + if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) { + // places-shutdown happens at profile-change-teardown, so here we + // can safely add a profile-before-change blocker. + AsyncShutdown.profileBeforeChange.addBlocker( + "Places: bookmarks.html", + () => BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks", Ci.nsIFile)) + .then(null, Cu.reportError) + ); } - ); - - // Backup bookmarks to bookmarks.html to support apps that depend - // on the legacy format. - let waitingForHTMLExportToComplete = false; - // If this fails to get the preference value, we don't export. - if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) { - // Exceptionally, since this is a non-default setting and HTML format is - // discouraged in favor of the JSON backups, we spin the event loop on - // shutdown, to wait for the export to finish. We cannot safely spin - // the event loop on shutdown until we include a watchdog to prevent - // potential hangs (bug 518683). The asynchronous shutdown operations - // will then be handled by a shutdown service (bug 435058). - waitingForHTMLExportToComplete = true; - BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks", Ci.nsIFile)).then( - function onSuccess() { - waitingForHTMLExportToComplete = false; - }, - function onFailure() { - Cu.reportError("Unable to auto export html."); - waitingForHTMLExportToComplete = false; - } - ); - } - - // The events loop should spin at least once because waitingForBackupToComplete - // is true before checking whether backup should be made. - let thread = Services.tm.currentThread; - while (waitingForBackupToComplete || waitingForHTMLExportToComplete) { - thread.processNextEvent(true); - } + } catch (ex) {} // Do not export. }, /** - * Backup bookmarks. + * If a backup for today doesn't exist, this creates one. */ _backupBookmarks: function BG__backupBookmarks() { return Task.spawn(function() { let lastBackupFile = yield PlacesBackups.getMostRecentBackup(); // Should backup bookmarks if there are no backups or the maximum // interval between backups elapsed. if (!lastBackupFile || - new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_INTERVAL) { - let maxBackups = BOOKMARKS_BACKUP_MAX_BACKUPS; - try { - maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups"); - } - catch(ex) { /* Use default. */ } - - yield PlacesBackups.create(maxBackups); // Don't force creation. + new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000) { + let maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups"); + yield PlacesBackups.create(maxBackups); } }); }, /** * Show the notificationBox for a locked places database. */ _showPlacesLockedNotificationBox: function BG__showPlacesLockedNotificationBox() {
--- a/browser/components/places/content/places.js +++ b/browser/components/places/content/places.js @@ -535,17 +535,18 @@ var PlacesOrganizer = { */ backupBookmarks: function PO_backupBookmarks() { let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); let fpCallback = function fpCallback_done(aResult) { if (aResult != Ci.nsIFilePicker.returnCancel) { - PlacesBackups.saveBookmarksToJSONFile(fp.file); + // There is no OS.File version of the filepicker yet (Bug 937812). + PlacesBackups.saveBookmarksToJSONFile(fp.file.path); } }; fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"), Ci.nsIFilePicker.modeSave); fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); fp.defaultString = PlacesBackups.getFilenameForDate();
--- a/browser/components/places/tests/unit/test_421483.js +++ b/browser/components/places/tests/unit/test_421483.js @@ -1,94 +1,81 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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/. */ -// Get bookmarks service -try { - var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); -} catch(ex) { - do_throw("Could not get Bookmarks service\n"); -} - -// Get annotation service -try { - var annosvc = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); -} catch(ex) { - do_throw("Could not get Annotation service\n"); -} - -// Get browser glue -try { - var gluesvc = Cc["@mozilla.org/browser/browserglue;1"]. - getService(Ci.nsIBrowserGlue). - QueryInterface(Ci.nsIObserver); - // Avoid default bookmarks import. - gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", ""); -// gluesvc.observe(null, "initial-migration-did-import-default-bookmarks", ""); -} catch(ex) { - do_throw("Could not get BrowserGlue service\n"); -} - -// Get pref service -try { - var pref = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); -} catch(ex) { - do_throw("Could not get Preferences service\n"); -} const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion"; -// main +let gluesvc = Cc["@mozilla.org/browser/browserglue;1"]. + getService(Ci.nsIBrowserGlue). + QueryInterface(Ci.nsIObserver); +// Avoid default bookmarks import. +gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", ""); + function run_test() { - // TEST 1: smart bookmarks disabled - pref.setIntPref("browser.places.smartBookmarksVersion", -1); - gluesvc.ensurePlacesDefaultQueriesInitialized(); - var smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_eq(smartBookmarkItemIds.length, 0); - // check that pref has not been bumped up - do_check_eq(pref.getIntPref("browser.places.smartBookmarksVersion"), -1); + run_next_test(); +} - // TEST 2: create smart bookmarks - pref.setIntPref("browser.places.smartBookmarksVersion", 0); +add_task(function smart_bookmarks_disabled() { + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1); gluesvc.ensurePlacesDefaultQueriesInitialized(); - smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_neq(smartBookmarkItemIds.length, 0); - // check that pref has been bumped up - do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0); + let smartBookmarkItemIds = + PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + do_check_eq(smartBookmarkItemIds.length, 0); + do_log_info("check that pref has not been bumped up"); + do_check_eq(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1); +}); - var smartBookmarksCount = smartBookmarkItemIds.length; - - // TEST 3: smart bookmarks restore - // remove one smart bookmark and restore - bmsvc.removeItem(smartBookmarkItemIds[0]); - pref.setIntPref("browser.places.smartBookmarksVersion", 0); +add_task(function create_smart_bookmarks() { + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); gluesvc.ensurePlacesDefaultQueriesInitialized(); - smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount); - // check that pref has been bumped up - do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0); + let smartBookmarkItemIds = + PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + do_check_neq(smartBookmarkItemIds.length, 0); + do_log_info("check that pref has been bumped up"); + do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); +}); - // TEST 4: move a smart bookmark, change its title, then restore - // smart bookmark should be restored in place - var parent = bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]); - var oldTitle = bmsvc.getItemTitle(smartBookmarkItemIds[0]); +add_task(function remove_smart_bookmark_and_restore() { + let smartBookmarkItemIds = + PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + let smartBookmarksCount = smartBookmarkItemIds.length; + do_log_info("remove one smart bookmark and restore"); + PlacesUtils.bookmarks.removeItem(smartBookmarkItemIds[0]); + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); + gluesvc.ensurePlacesDefaultQueriesInitialized(); + let smartBookmarkItemIds = + PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount); + do_log_info("check that pref has been bumped up"); + do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); +}); + +add_task(function move_smart_bookmark_rename_and_restore() { + let smartBookmarkItemIds = + PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + let smartBookmarksCount = smartBookmarkItemIds.length; + do_log_info("smart bookmark should be restored in place"); + let parent = PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]); + let oldTitle = PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]); // create a subfolder and move inside it - var newParent = bmsvc.createFolder(parent, "test", bmsvc.DEFAULT_INDEX); - bmsvc.moveItem(smartBookmarkItemIds[0], newParent, bmsvc.DEFAULT_INDEX); + let newParent = + PlacesUtils.bookmarks.createFolder(parent, "test", + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.moveItem(smartBookmarkItemIds[0], newParent, + PlacesUtils.bookmarks.DEFAULT_INDEX); // change title - bmsvc.setItemTitle(smartBookmarkItemIds[0], "new title"); + PlacesUtils.bookmarks.setItemTitle(smartBookmarkItemIds[0], "new title"); // restore - pref.setIntPref("browser.places.smartBookmarksVersion", 0); + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); gluesvc.ensurePlacesDefaultQueriesInitialized(); - smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + smartBookmarkItemIds = + PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount); - do_check_eq(bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]), newParent); - do_check_eq(bmsvc.getItemTitle(smartBookmarkItemIds[0]), oldTitle); - // check that pref has been bumped up - do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0); -} + do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]), newParent); + do_check_eq(PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]), oldTitle); + do_log_info("check that pref has been bumped up"); + do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); +});
deleted file mode 100644 --- a/browser/components/places/tests/unit/test_browserGlue_shutdown.js +++ /dev/null @@ -1,153 +0,0 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* 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/. */ - -/** - * Tests that nsBrowserGlue is correctly exporting based on preferences values, - * and creating bookmarks backup if one does not exist for today. - */ - -// Initialize nsBrowserGlue after Places. -let bg = Cc["@mozilla.org/browser/browserglue;1"]. - getService(Ci.nsIBrowserGlue); - -// Initialize Places through Bookmarks Service. -let bs = PlacesUtils.bookmarks; - -// Get other services. -let ps = Services.prefs; -let os = Services.obs; - -const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML"; - -let tests = []; - -//------------------------------------------------------------------------------ - -tests.push({ - description: "Export to bookmarks.html if autoExportHTML is true.", - exec: function() { - remove_all_JSON_backups(); - - // Sanity check: we should have bookmarks on the toolbar. - do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0); - - // Set preferences. - ps.setBoolPref(PREF_AUTO_EXPORT_HTML, true); - - // Force nsBrowserGlue::_shutdownPlaces(). - bg.QueryInterface(Ci.nsIObserver).observe(null, - PlacesUtils.TOPIC_SHUTDOWN, - null); - - // Check bookmarks.html has been created. - check_bookmarks_html(); - // Check JSON backup has been created. - check_JSON_backup(true); - - // Check preferences have not been reverted. - do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML)); - // Reset preferences. - ps.setBoolPref(PREF_AUTO_EXPORT_HTML, false); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "Export to bookmarks.html if autoExportHTML is true and a bookmarks.html exists.", - exec: function() { - // Sanity check: we should have bookmarks on the toolbar. - do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0); - - // Set preferences. - ps.setBoolPref(PREF_AUTO_EXPORT_HTML, true); - - // Create a bookmarks.html in the profile. - let profileBookmarksHTMLFile = create_bookmarks_html("bookmarks.glue.html"); - - // set the file's lastModifiedTime to one minute ago and get its size. - let lastMod = Date.now() - 60*1000; - profileBookmarksHTMLFile.lastModifiedTime = lastMod; - - let fileSize = profileBookmarksHTMLFile.fileSize; - - // Force nsBrowserGlue::_shutdownPlaces(). - bg.QueryInterface(Ci.nsIObserver).observe(null, - PlacesUtils.TOPIC_SHUTDOWN, - null); - - // Check a new bookmarks.html has been created. - let profileBookmarksHTMLFile = check_bookmarks_html(); - do_check_true(profileBookmarksHTMLFile.lastModifiedTime > lastMod); - do_check_neq(profileBookmarksHTMLFile.fileSize, fileSize); - - // Check preferences have not been reverted. - do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML)); - // Reset preferences. - ps.setBoolPref(PREF_AUTO_EXPORT_HTML, false); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "Backup to JSON should be a no-op if a backup for today already exists.", - exec: function() { - // Sanity check: we should have bookmarks on the toolbar. - do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0); - - // Create a JSON backup in the profile. - let profileBookmarksJSONFile = create_JSON_backup("bookmarks.glue.json"); - // Get file lastModified and size. - let lastMod = profileBookmarksJSONFile.lastModifiedTime; - let fileSize = profileBookmarksJSONFile.fileSize; - - // Force nsBrowserGlue::_shutdownPlaces(). - bg.QueryInterface(Ci.nsIObserver).observe(null, - PlacesUtils.TOPIC_SHUTDOWN, - null); - - // Check a new JSON backup has not been created. - do_check_true(profileBookmarksJSONFile.exists()); - do_check_eq(profileBookmarksJSONFile.lastModifiedTime, lastMod); - do_check_eq(profileBookmarksJSONFile.fileSize, fileSize); - - do_test_finished(); - } -}); - -//------------------------------------------------------------------------------ - -var testIndex = 0; -function next_test() { - // Remove bookmarks.html from profile. - remove_bookmarks_html(); - - // Execute next test. - let test = tests.shift(); - dump("\nTEST " + (++testIndex) + ": " + test.description); - test.exec(); -} - -function run_test() { - do_test_pending(); - - // Clean up bookmarks. - remove_all_bookmarks(); - - // Create some bookmarks. - bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://mozilla.org/"), - bs.DEFAULT_INDEX, "bookmark-on-menu"); - bs.insertBookmark(bs.toolbarFolder, uri("http://mozilla.org/"), - bs.DEFAULT_INDEX, "bookmark-on-toolbar"); - - // Kick-off tests. - next_test(); -}
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js +++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js @@ -14,18 +14,18 @@ const URIS = [ , "http://b.example2.com/" , "http://c.example3.com/" ]; const TOPIC_CONNECTION_CLOSED = "places-connection-closed"; let EXPECTED_NOTIFICATIONS = [ "places-shutdown" +, "places-will-close-connection" , "places-expiration-finished" -, "places-will-close-connection" , "places-connection-closed" ]; const UNEXPECTED_NOTIFICATIONS = [ "xpcom-shutdown" ]; const URL = "ftp://localhost/clearHistoryOnShutdown/";
--- a/browser/components/places/tests/unit/xpcshell.ini +++ b/browser/components/places/tests/unit/xpcshell.ini @@ -11,13 +11,12 @@ support-files = [test_421483.js] [test_browserGlue_corrupt.js] [test_browserGlue_corrupt_nobackup.js] [test_browserGlue_corrupt_nobackup_default.js] [test_browserGlue_distribution.js] [test_browserGlue_migrate.js] [test_browserGlue_prefs.js] [test_browserGlue_restore.js] -[test_browserGlue_shutdown.js] [test_browserGlue_smartBookmarks.js] [test_clearHistory_shutdown.js] [test_leftpane_corruption_handling.js] [test_PUIU_makeTransaction.js]
--- a/toolkit/components/places/BookmarkJSONUtils.jsm +++ b/toolkit/components/places/BookmarkJSONUtils.jsm @@ -4,24 +4,34 @@ this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ]; const Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; const Cr = Components.results; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); -Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); -Cu.import("resource://gre/modules/Sqlite.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", + "resource://gre/modules/PlacesBackups.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder()); +XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder()); +XPCOMUtils.defineLazyGetter(this, "localFileCtor", + () => Components.Constructor("@mozilla.org/file/local;1", + "nsILocalFile", "initWithPath")); this.BookmarkJSONUtils = Object.freeze({ /** * Import bookmarks from a url. * * @param aURL * url of the bookmark data. * @param aReplace @@ -36,43 +46,70 @@ this.BookmarkJSONUtils = Object.freeze({ return importer.importFromURL(aURL, aReplace); }, /** * Restores bookmarks and tags from a JSON file. * @note any item annotated with "places/excludeFromBackup" won't be removed * before executing the restore. * - * @param aFile - * nsIFile of bookmarks in JSON format to be restored. + * @param aFilePath + * OS.File path or nsIFile of bookmarks in JSON format to be restored. * @param aReplace * Boolean if true, replace existing bookmarks, else merge. * * @return {Promise} * @resolves When the new bookmarks have been created. * @rejects JavaScript exception. */ - importFromFile: function BJU_importFromFile(aFile, aReplace) { + importFromFile: function BJU_importFromFile(aFilePath, aReplace) { let importer = new BookmarkImporter(); - return importer.importFromFile(aFile, aReplace); + // TODO (bug 967192): convert to pure OS.File + let file = aFilePath instanceof Ci.nsIFile ? aFilePath + : new localFileCtor(aFilePath); + return importer.importFromFile(file, aReplace); }, /** - * Serializes bookmarks using JSON, and writes to the supplied file. + * Serializes bookmarks using JSON, and writes to the supplied file path. * - * @param aLocalFile - * nsIFile for the "bookmarks.json" file to be created. + * @param aFilePath + * OS.File path for the "bookmarks.json" file to be created. * * @return {Promise} - * @resolves When the file has been created. + * @resolves To the exported bookmarks count when the file has been created. * @rejects JavaScript exception. + * @deprecated passing an nsIFile is deprecated */ - exportToFile: function BJU_exportToFile(aLocalFile) { - let exporter = new BookmarkExporter(); - return exporter.exportToFile(aLocalFile); + exportToFile: function BJU_exportToFile(aFilePath) { + if (aFilePath instanceof Ci.nsIFile) { + Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " + + "is deprecated. Please use an OS.File path instead.", + "https://developer.mozilla.org/docs/JavaScript_OS.File"); + aFilePath = aFilePath.path; + } + return Task.spawn(function* () { + let [bookmarks, count] = yield PlacesBackups.getBookmarksTree(); + let startTime = Date.now(); + let jsonString = JSON.stringify(bookmarks); + // Report the time taken to convert the tree to JSON. + try { + Services.telemetry + .getHistogramById("PLACES_BACKUPS_TOJSON_MS") + .add(Date.now() - startTime); + } catch (ex) { + Components.utils.reportError("Unable to report telemetry."); + } + + // Write to the temp folder first, to avoid leaving back partial files. + let tmpPath = OS.Path.join(OS.Constants.Path.tmpDir, + OS.Path.basename(aFilePath) + ".tmp"); + yield OS.File.writeAtomic(aFilePath, jsonString, { tmpPath: tmpPath }); + return count; + }); }, /** * Takes a JSON-serialized node and inserts it into the db. * * @param aData * The unwrapped data blob of dropped or pasted data. * @param aContainer @@ -357,17 +394,18 @@ BookmarkImporter.prototype = { PlacesUtils.tagging.tagURI( NetUtil.newURI(aChild.uri), [aData.title]); } catch (ex) { // Invalid tag child, skip it } }); return [folderIdMap, searchIds]; } - } else if (aData.livemark && aData.annos) { + } else if (aData.annos && + aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) { // Node is a livemark let feedURI = null; let siteURI = null; aData.annos = aData.annos.filter(function(aAnno) { switch (aAnno.name) { case PlacesUtils.LMANNO_FEEDURI: feedURI = NetUtil.newURI(aAnno.value); return false; @@ -417,17 +455,18 @@ BookmarkImporter.prototype = { } break; case PlacesUtils.TYPE_X_MOZ_PLACE: id = PlacesUtils.bookmarks.insertBookmark( aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title); if (aData.keyword) PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword); if (aData.tags) { - let tags = aData.tags.split(", "); + // TODO (bug 967196) the tagging service should trim by itself. + let tags = aData.tags.split(",").map(tag => tag.trim()); if (tags.length) PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags); } if (aData.charset) { PlacesUtils.annotations.setPageAnnotation( NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset, 0, Ci.nsIAnnotationService.EXPIRE_NEVER); } @@ -497,316 +536,16 @@ function fixupQuery(aQueryURI, aFolderId let convert = function(str, p1, offset, s) { return "folder=" + aFolderIdMap[p1]; } let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert); return NetUtil.newURI(stringURI); } -function BookmarkExporter() {} -BookmarkExporter.prototype = { - exportToFile: function BE_exportToFile(aLocalFile) { - return Task.spawn(this._writeToFile(aLocalFile)); - }, - - _converterOut: null, - - _writeToFile: function BE__writeToFile(aLocalFile) { - // Create a file that can be accessed by the current user only. - let safeFileOut = Cc["@mozilla.org/network/safe-file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - safeFileOut.init(aLocalFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | - FileUtils.MODE_TRUNCATE, parseInt("0600", 8), 0); - let nodeCount; - - try { - // We need a buffered output stream for performance. See bug 202477. - let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]. - createInstance(Ci.nsIBufferedOutputStream); - bufferedOut.init(safeFileOut, 4096); - try { - // Write bookmarks in UTF-8. - this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]. - createInstance(Ci.nsIConverterOutputStream); - this._converterOut.init(bufferedOut, "utf-8", 0, 0); - try { - nodeCount = yield this._writeContentToFile(); - - // Flush the buffer and retain the target file on success only. - bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish(); - } finally { - this._converterOut.close(); - this._converterOut = null; - } - } finally { - bufferedOut.close(); - } - } finally { - safeFileOut.close(); - } - throw new Task.Result(nodeCount); - }, - - _writeContentToFile: function BE__writeContentToFile() { - return Task.spawn(function() { - // Weep over stream interface variance. - let streamProxy = { - converter: this._converterOut, - write: function(aData, aLen) { - this.converter.writeString(aData); - } - }; - - // Get list of itemIds that must be excluded from the backup. - let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation( - PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO); - // Serialize to JSON and write to stream. - let nodeCount = yield BookmarkRow.serializeJSONToOutputStream(streamProxy, - excludeItems); - throw new Task.Result(nodeCount); - }.bind(this)); - } -} - -let BookmarkRow = { - /** - * Serializes the SQL results as JSON with async SQL call and writes the - * serialization to the given output stream. - * - * @param aStream - * An nsIOutputStream. NOTE: it only uses the write(str, len) - * method of nsIOutputStream. The caller is responsible for - * closing the stream. - * @param aExcludeItems - * An array of item ids that should not be written to the backup. - * @return {Promise} - * @resolves the number of serialized uri nodes. - */ - serializeJSONToOutputStream: function(aStream, aExcludeItems) { - return Task.spawn(function() { - let nodes = []; - let nodeCount = 0; - - let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, - "places.sqlite"); - let conn = yield Sqlite.openConnection({ path: dbFilePath, - sharedMemoryCache: false }); - try { - let rows = yield conn.execute( - "SELECT b.id, h.url, b.position, b.title, b.parent, " + - "b.type, b.dateAdded, b.lastModified, b.guid, t.parent AS grandParent " + - "FROM moz_bookmarks b " + - "LEFT JOIN moz_bookmarks t ON t.id = b.parent " + - "LEFT JOIN moz_places h ON h.id = b.fk " + - "ORDER BY b.parent, b.position, b.id"); - - // Create a Map for lookup. - let rowMap = new Map(); - for (let row of rows) { - let parent = row.getResultByName("parent"); - if (rowMap.has(parent)) { - let data = rowMap.get(parent); - data.children.push(row); - } else { - rowMap.set(parent, { children: [row] }); - } - } - - let root = rowMap.get(0); - if (!root) { - throw new Error("Root does not exist."); - } - let result = yield BookmarkRow._appendConvertedNode(root.children[0], - rowMap, - nodes, - aExcludeItems); - if (result.appendedNode) { - nodeCount = result.nodeCount; - let json = JSON.stringify(nodes[0]); - aStream.write(json, json.length); - } - } catch(e) { - Cu.reportError("serializeJSONToOutputStream error " + e); - } finally { - yield conn.close(); - } - throw new Task.Result(nodeCount); - }); - }, - - _appendConvertedNode: function BR__appendConvertedNode( - aRow, aRowMap, aNodes, aExcludeItems) { - return Task.spawn(function() { - let node = {}; - let nodeCount = 0; - - this._addGenericProperties(aRow, node); - - let parent = aRow.getResultByName("parent"); - let grandParent = parent ? aRow.getResultByName("grandParent") : null; - let type = aRow.getResultByName("type"); - - if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { - // Tag root accept only folder nodes - if (parent == PlacesUtils.tagsFolderId) - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); - - // Check for url validity, since we can't halt while writing a backup. - // This will throw if we try to serialize an invalid url and it does - // not make sense saving a wrong or corrupt uri node. - try { - NetUtil.newURI(aRow.getResultByName("url")); - } catch (ex) { - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); - } - yield this._addURIProperties(aRow, node); - nodeCount++; - } else if (type == Ci.nsINavBookmarksService.TYPE_FOLDER) { - // Tag containers accept only uri nodes - if (grandParent && grandParent == PlacesUtils.tagsFolderId) { - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); - } - this._addContainerProperties(aRow, node); - } else if (type == Ci.nsINavBookmarksService.TYPE_SEPARATOR) { - // Tag root accept only folder nodes - // Tag containers accept only uri nodes - if ((parent == PlacesUtils.tagsFolderId) || - (grandParent == PlacesUtils.tagsFolderId)) { - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); - } - this._addSeparatorProperties(aRow, node); - } - - if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { - nodeCount += yield this._appendConvertedComplexNode(node, - aNodes, - aRowMap, - aExcludeItems); - throw new Task.Result({ appendedNode: true, nodeCount: nodeCount }); - } - - aNodes.push(node); - throw new Task.Result({ appendedNode: true, nodeCount: nodeCount }); - }.bind(this)); - }, - - _addGenericProperties: function BR__addGenericProperties(aRow, aJSNode) { - let title = aRow.getResultByName("title") - aJSNode.title = title ? title : ""; - aJSNode.guid = aRow.getResultByName("guid"); - aJSNode.id = aRow.getResultByName("id"); - aJSNode.index = aRow.getResultByName("position"); - if (aJSNode.id != -1) { - let parent = aRow.getResultByName("parent"); - if (parent) - aJSNode.parent = parent; - let dateAdded = aRow.getResultByName("dateAdded");; - if (dateAdded) - aJSNode.dateAdded = dateAdded; - let lastModified = aRow.getResultByName("lastModified"); - if (lastModified) - aJSNode.lastModified = lastModified; - - // XXX need a hasAnnos api - let annos = []; - try { - annos = - PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) { - // XXX should whitelist this instead, w/ a pref for - // backup/restore of non-whitelisted annos - // XXX causes JSON encoding errors, so utf-8 encode - // anno.value = unescape(encodeURIComponent(anno.value)); - if (anno.name == PlacesUtils.LMANNO_FEEDURI) - aJSNode.livemark = 1; - return true; - }); - } catch(ex) {} - if (annos.length != 0) - aJSNode.annos = annos; - } - // XXXdietrich - store annos for non-bookmark items - }, - - _addURIProperties: function BR__addURIProperties(aRow, aJSNode) { - return Task.spawn(function() { - aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE; - aJSNode.uri = aRow.getResultByName("url"); - if (aJSNode.id && aJSNode.id != -1) { - // Harvest bookmark-specific properties - let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id); - if (keyword) - aJSNode.keyword = keyword; - } - - // Last character-set - let uri = NetUtil.newURI(aRow.getResultByName("url")); - let lastCharset = yield PlacesUtils.getCharsetForURI(uri) - if (lastCharset) - aJSNode.charset = lastCharset; - }); - }, - - _addSeparatorProperties: function BR__addSeparatorProperties(aRow, aJSNode) { - aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR; - }, - - _addContainerProperties: function BR__addContainerProperties(aRow, aJSNode) { - // This is a bookmark or a tag container. - // Bookmark folder or a shortcut we should convert to folder. - aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER; - - // Mark root folders - let itemId = aRow.getResultByName("id"); - if (itemId == PlacesUtils.placesRootId) - aJSNode.root = "placesRoot"; - else if (itemId == PlacesUtils.bookmarksMenuFolderId) - aJSNode.root = "bookmarksMenuFolder"; - else if (itemId == PlacesUtils.tagsFolderId) - aJSNode.root = "tagsFolder"; - else if (itemId == PlacesUtils.unfiledBookmarksFolderId) - aJSNode.root = "unfiledBookmarksFolder"; - else if (itemId == PlacesUtils.toolbarFolderId) - aJSNode.root = "toolbarFolder"; - }, - - _appendConvertedComplexNode: function BR__appendConvertedComplexNode( - aNode, aNodes, aRowMap, aExcludeItems) { - return Task.spawn(function() { - let repr = {}; - let nodeCount = 0; - - for (let [name, value] in Iterator(aNode)) - repr[name] = value; - repr.children = []; - - let data = aRowMap.get(aNode.id); - if (data) { - for (let row of data.children) { - let id = row.getResultByName("id"); - // ignore exclude items - if (aExcludeItems && aExcludeItems.indexOf(id) != -1) { - continue; - } - let result = yield this._appendConvertedNode(row, - aRowMap, - repr.children, - aExcludeItems); - nodeCount += result.nodeCount; - } - } else { - Cu.reportError("_appendConvertedComplexNode error: Unable to find node"); - } - - aNodes.push(repr); - throw new Task.Result(nodeCount); - }.bind(this)); - } -} - let BookmarkNode = { /** * Serializes the given node (and all its descendents) as JSON * and writes the serialization to the given output stream. * * @param aNode * An nsINavHistoryResultNode * @param aStream @@ -821,36 +560,36 @@ let BookmarkNode = { * @param aExcludeItems * An array of item ids that should not be written to the backup. * @returns Task promise * @resolves the number of serialized uri nodes. */ serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream( aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) { - return Task.spawn(function() { + return Task.spawn(function* () { // Serialize to stream let array = []; let result = yield this._appendConvertedNode(aNode, null, array, aIsUICommand, aResolveShortcuts, aExcludeItems); if (result.appendedNode) { - let json = JSON.stringify(array[0]); - aStream.write(json, json.length); + let jsonString = JSON.stringify(array[0]); + aStream.write(jsonString, jsonString.length); } else { throw Cr.NS_ERROR_UNEXPECTED; } - throw new Task.Result(result.nodeCount); + return result.nodeCount; }.bind(this)); }, _appendConvertedNode: function BN__appendConvertedNode( bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) { - return Task.spawn(function() { + return Task.spawn(function* () { let node = {}; let nodeCount = 0; // Set index in order received // XXX handy shortcut, but are there cases where we don't want // to export using the sorting provided by the query? if (aIndex) node.index = aIndex; @@ -858,58 +597,58 @@ let BookmarkNode = { this._addGenericProperties(bNode, node, aResolveShortcuts); let parent = bNode.parent; let grandParent = parent ? parent.parent : null; if (PlacesUtils.nodeIsURI(bNode)) { // Tag root accept only folder nodes if (parent && parent.itemId == PlacesUtils.tagsFolderId) - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); + return { appendedNode: false, nodeCount: nodeCount }; // Check for url validity, since we can't halt while writing a backup. // This will throw if we try to serialize an invalid url and it does // not make sense saving a wrong or corrupt uri node. try { NetUtil.newURI(bNode.uri); } catch (ex) { - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); + return { appendedNode: false, nodeCount: nodeCount }; } yield this._addURIProperties(bNode, node, aIsUICommand); nodeCount++; } else if (PlacesUtils.nodeIsContainer(bNode)) { // Tag containers accept only uri nodes if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId) - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); + return { appendedNode: false, nodeCount: nodeCount }; this._addContainerProperties(bNode, node, aIsUICommand, aResolveShortcuts); } else if (PlacesUtils.nodeIsSeparator(bNode)) { // Tag root accept only folder nodes // Tag containers accept only uri nodes if ((parent && parent.itemId == PlacesUtils.tagsFolderId) || (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)) - throw new Task.Result({ appendedNode: false, nodeCount: nodeCount }); + return { appendedNode: false, nodeCount: nodeCount }; this._addSeparatorProperties(bNode, node); } if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { nodeCount += yield this._appendConvertedComplexNode(node, bNode, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) - throw new Task.Result({ appendedNode: true, nodeCount: nodeCount }); + return { appendedNode: true, nodeCount: nodeCount }; } aArray.push(node); - throw new Task.Result({ appendedNode: true, nodeCount: nodeCount }); + return { appendedNode: true, nodeCount: nodeCount }; }.bind(this)); }, _addGenericProperties: function BN__addGenericProperties( aPlacesNode, aJSNode, aResolveShortcuts) { aJSNode.title = aPlacesNode.title; aJSNode.id = aPlacesNode.itemId; if (aJSNode.id != -1) { @@ -927,18 +666,16 @@ let BookmarkNode = { let annos = []; try { annos = PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) { // XXX should whitelist this instead, w/ a pref for // backup/restore of non-whitelisted annos // XXX causes JSON encoding errors, so utf-8 encode // anno.value = unescape(encodeURIComponent(anno.value)); - if (anno.name == PlacesUtils.LMANNO_FEEDURI) - aJSNode.livemark = 1; if (anno.name == PlacesUtils.READ_ONLY_ANNO && aResolveShortcuts) { // When copying a read-only node, remove the read-only annotation. return false; } return true; }); } catch(ex) {} if (annos.length != 0) @@ -1009,26 +746,27 @@ let BookmarkNode = { aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE; aJSNode.uri = aPlacesNode.uri; } }, _appendConvertedComplexNode: function BN__appendConvertedComplexNode( aNode, aSourceNode, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) { - return Task.spawn(function() { + return Task.spawn(function* () { let repr = {}; let nodeCount = 0; for (let [name, value] in Iterator(aNode)) repr[name] = value; // Write child nodes let children = repr.children = []; - if (!aNode.livemark) { + if (!aNode.annos || + !aNode.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) { PlacesUtils.asContainer(aSourceNode); let wasOpen = aSourceNode.containerOpen; if (!wasOpen) aSourceNode.containerOpen = true; let cc = aSourceNode.childCount; for (let i = 0; i < cc; ++i) { let childNode = aSourceNode.getChild(i); if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1) @@ -1038,12 +776,12 @@ let BookmarkNode = { aExcludeItems); nodeCount += result.nodeCount; } if (!wasOpen) aSourceNode.containerOpen = false; } aArray.push(repr); - throw new Task.Result(nodeCount); + return nodeCount; }.bind(this)); } -} \ No newline at end of file +}
--- a/toolkit/components/places/PlacesBackups.jsm +++ b/toolkit/components/places/PlacesBackups.jsm @@ -12,21 +12,26 @@ const Cc = Components.classes; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Deprecated.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", + "resource://gre/modules/Sqlite.jsm"); + +XPCOMUtils.defineLazyGetter(this, "localFileCtor", + () => Components.Constructor("@mozilla.org/file/local;1", + "nsILocalFile", "initWithPath")); this.PlacesBackups = { get _filenamesRegex() { // Get the localized backup filename, will be used to clear out // old backups with a localized name (bug 445704). let localizedFilename = PlacesUtils.getFormattedString("bookmarksArchiveFilename", [new Date()]); let localizedFilenamePrefix = @@ -35,96 +40,104 @@ this.PlacesBackups = { return this._filenamesRegex = new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)(_[0-9]+)*\.(json|html)"); }, get folder() { Deprecated.warning( "PlacesBackups.folder is deprecated and will be removed in a future version", "https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); + return this._folder; + }, + /** + * This exists just to avoid spamming deprecate warnings from internal calls + * needed to support deprecated methods themselves. + */ + get _folder() { let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile); bookmarksBackupDir.append(this.profileRelativeFolderPath); if (!bookmarksBackupDir.exists()) { bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8)); if (!bookmarksBackupDir.exists()) throw("Unable to create bookmarks backup folder"); } - delete this.folder; - return this.folder = bookmarksBackupDir; + delete this._folder; + return this._folder = bookmarksBackupDir; }, /** * Gets backup folder asynchronously. * @return {Promise} * @resolve the folder (the folder string path). */ getBackupFolder: function PB_getBackupFolder() { - return Task.spawn(function() { - if (this._folder) { - throw new Task.Result(this._folder); + return Task.spawn(function* () { + if (this._backupFolder) { + return this._backupFolder; } let profileDir = OS.Constants.Path.profileDir; let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath); - yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true }).then( - function onSuccess() { - this._folder = backupsDirPath; - }.bind(this), - function onError() { - throw("Unable to create bookmarks backup folder"); - }); - throw new Task.Result(this._folder); - }.bind(this)); + yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true }); + return this._backupFolder = backupsDirPath; + }.bind(this)); }, get profileRelativeFolderPath() "bookmarkbackups", /** * Cache current backups in a sorted (by date DESC) array. */ get entries() { Deprecated.warning( "PlacesBackups.entries is deprecated and will be removed in a future version", "https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); + return this._entries; + }, - delete this.entries; - this.entries = []; - let files = this.folder.directoryEntries; + /** + * This exists just to avoid spamming deprecate warnings from internal calls + * needed to support deprecated methods themselves. + */ + get _entries() { + delete this._entries; + this._entries = []; + let files = this._folder.directoryEntries; while (files.hasMoreElements()) { let entry = files.getNext().QueryInterface(Ci.nsIFile); // A valid backup is any file that matches either the localized or // not-localized filename (bug 445704). let matches = entry.leafName.match(this._filenamesRegex); if (!entry.isHidden() && matches) { // Remove bogus backups in future dates. if (this.getDateForFile(entry) > new Date()) { entry.remove(false); continue; } - this.entries.push(entry); + this._entries.push(entry); } } - this.entries.sort((a, b) => { + this._entries.sort((a, b) => { let aDate = this.getDateForFile(a); let bDate = this.getDateForFile(b); return aDate < bDate ? 1 : aDate > bDate ? -1 : 0; }); - return this.entries; + return this._entries; }, /** * Cache current backups in a sorted (by date DESC) array. * @return {Promise} * @resolve a sorted array of string paths. */ getBackupFiles: function PB_getBackupFiles() { - return Task.spawn(function() { - if (this._backupFiles) { - throw new Task.Result(this._backupFiles); - } + return Task.spawn(function* () { + if (this._backupFiles) + return this._backupFiles; + this._backupFiles = []; let backupFolderPath = yield this.getBackupFolder(); let iterator = new OS.File.DirectoryIterator(backupFolderPath); yield iterator.forEach(function(aEntry) { let matches = aEntry.name.match(this._filenamesRegex); if (matches) { // Remove bogus backups in future dates. @@ -133,23 +146,23 @@ this.PlacesBackups = { return OS.File.remove(filePath); } else { this._backupFiles.push(filePath); } } }.bind(this)); iterator.close(); - this._backupFiles.sort(function(a, b) { + this._backupFiles.sort((a, b) => { let aDate = this.getDateForFile(a); let bDate = this.getDateForFile(b); return aDate < bDate ? 1 : aDate > bDate ? -1 : 0; - }.bind(this)); + }); - throw new Task.Result(this._backupFiles); + return this._backupFiles; }.bind(this)); }, /** * Creates a filename for bookmarks backup files. * * @param [optional] aDateObj * Date object used to build the filename. @@ -189,97 +202,101 @@ this.PlacesBackups = { * @returns nsIFile backup file */ getMostRecent: function PB_getMostRecent(aFileExt) { Deprecated.warning( "PlacesBackups.getMostRecent is deprecated and will be removed in a future version", "https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); let fileExt = aFileExt || "(json|html)"; - for (let i = 0; i < this.entries.length; i++) { + for (let i = 0; i < this._entries.length; i++) { let rx = new RegExp("\." + fileExt + "$"); - if (this.entries[i].leafName.match(rx)) - return this.entries[i]; + if (this._entries[i].leafName.match(rx)) + return this._entries[i]; } return null; }, /** * Get the most recent backup file. * * @param [optional] aFileExt * Force file extension. Either "html" or "json". * Will check for both if not defined. * @return {Promise} * @result the path to the file. */ getMostRecentBackup: function PB_getMostRecentBackup(aFileExt) { - return Task.spawn(function() { + return Task.spawn(function* () { let fileExt = aFileExt || "(json|html)"; let entries = yield this.getBackupFiles(); for (let entry of entries) { let rx = new RegExp("\." + fileExt + "$"); if (OS.Path.basename(entry).match(rx)) { - throw new Task.Result(entry); + return entry; } } - throw new Task.Result(null); + return null; }.bind(this)); }, /** * Serializes bookmarks using JSON, and writes to the supplied file. * Note: any item that should not be backed up must be annotated with * "places/excludeFromBackup". * - * @param aFile - * nsIFile where to save JSON backup. + * @param aFilePath + * OS.File path for the "bookmarks.json" file to be created. * @return {Promise} * @resolves the number of serialized uri nodes. + * @deprecated passing an nsIFile is deprecated */ - saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFile) { - return Task.spawn(function() { - let nodeCount = yield BookmarkJSONUtils.exportToFile(aFile); + saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) { + if (aFilePath instanceof Ci.nsIFile) { + Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " + + "is deprecated. Please use an OS.File path instead.", + "https://developer.mozilla.org/docs/JavaScript_OS.File"); + aFilePath = aFilePath.path; + } + return Task.spawn(function* () { + let nodeCount = yield BookmarkJSONUtils.exportToFile(aFilePath); let backupFolderPath = yield this.getBackupFolder(); - if (aFile.parent.path == backupFolderPath) { - // Update internal cache. - this.entries.push(aFile); + if (OS.Path.dirname(aFilePath) == backupFolderPath) { + // We are creating a backup in the default backups folder, + // so just update the internal cache. + this._entries.unshift(new localFileCtor(aFilePath)); if (!this._backupFiles) { yield this.getBackupFiles(); } - this._backupFiles.push(aFile.path); + this._backupFiles.unshift(aFilePath); } else { // If we are saving to a folder different than our backups folder, then // we also want to copy this new backup to it. // This way we ensure the latest valid backup is the same saved by the // user. See bug 424389. let name = this.getFilenameForDate(); let newFilename = this._appendMetaDataToFilename(name, { nodeCount: nodeCount }); let newFilePath = OS.Path.join(backupFolderPath, newFilename); let backupFile = yield this._getBackupFileForSameDate(name); - - if (backupFile) { - yield OS.File.remove(backupFile, { ignoreAbsent: true }); - } else { - let file = new FileUtils.File(newFilePath); - + if (!backupFile) { // Update internal cache if we are not replacing an existing // backup file. - this.entries.push(file); + this._entries.unshift(new localFileCtor(newFilePath)); if (!this._backupFiles) { yield this.getBackupFiles(); } - this._backupFiles.push(file.path); + this._backupFiles.unshift(newFilePath); } - yield OS.File.copy(aFile.path, newFilePath); + + yield OS.File.copy(aFilePath, newFilePath); } - throw new Task.Result(nodeCount); + return nodeCount; }.bind(this)); }, /** * Creates a dated backup in <profile>/bookmarkbackups. * Stores the bookmarks using JSON. * Note: any item that should not be backed up must be annotated with * "places/excludeFromBackup". @@ -287,17 +304,17 @@ this.PlacesBackups = { * @param [optional] int aMaxBackups * The maximum number of backups to keep. * @param [optional] bool aForceBackup * Forces creating a backup even if one was already * created that day (overwrites). * @return {Promise} */ create: function PB_create(aMaxBackups, aForceBackup) { - return Task.spawn(function() { + return Task.spawn(function* () { // Construct the new leafname. let newBackupFilename = this.getFilenameForDate(); let mostRecentBackupFile = yield this.getMostRecentBackup(); if (!aForceBackup) { let numberOfBackupsToDelete = 0; if (aMaxBackups !== undefined && aMaxBackups > -1) { let backupFiles = yield this.getBackupFiles(); @@ -309,17 +326,17 @@ this.PlacesBackups = { // the total backups after this operation does not exceed the // number specified in the pref. if (!mostRecentBackupFile || !this._isFilenameWithSameDate(OS.Path.basename(mostRecentBackupFile), newBackupFilename)) numberOfBackupsToDelete++; while (numberOfBackupsToDelete--) { - this.entries.pop(); + this._entries.pop(); if (!this._backupFiles) { yield this.getBackupFiles(); } let oldestBackup = this._backupFiles.pop(); yield OS.File.remove(oldestBackup); } } @@ -336,35 +353,32 @@ this.PlacesBackups = { if (aForceBackup) { yield OS.File.remove(backupFile, { ignoreAbsent: true }); } else { return; } } // Save bookmarks to a backup file. - let backupFolderPath = yield this.getBackupFolder(); - let backupFolder = new FileUtils.File(backupFolderPath); - let newBackupFile = backupFolder.clone(); - newBackupFile.append(newBackupFilename); - + let backupFolder = yield this.getBackupFolder(); + let newBackupFile = OS.Path.join(backupFolder, newBackupFilename); let nodeCount = yield this.saveBookmarksToJSONFile(newBackupFile); // Rename the filename with metadata. let newFilenameWithMetaData = this._appendMetaDataToFilename( newBackupFilename, { nodeCount: nodeCount }); - newBackupFile.moveTo(backupFolder, newFilenameWithMetaData); + let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData); + yield OS.File.move(newBackupFile, newBackupFileWithMetadata); // Update internal cache. - let newFileWithMetaData = backupFolder.clone(); - newFileWithMetaData.append(newFilenameWithMetaData); - this.entries.pop(); - this.entries.push(newFileWithMetaData); + let newFileWithMetaData = new localFileCtor(newBackupFileWithMetadata); + this._entries.pop(); + this._entries.unshift(newFileWithMetaData); this._backupFiles.pop(); - this._backupFiles.push(newFileWithMetaData.path); + this._backupFiles.unshift(newBackupFileWithMetadata); }.bind(this)); }, _appendMetaDataToFilename: function PB__appendMetaDataToFilename(aFilename, aMetaData) { let matches = aFilename.match(this._filenamesRegex); let newFilename = matches[1] + "-" + matches[2] + "_" + aMetaData.nodeCount + "." + matches[4]; @@ -395,27 +409,221 @@ this.PlacesBackups = { let targetMatches = aTargetName.match(this._filenamesRegex); return (sourceMatches && targetMatches && sourceMatches[1] == targetMatches[1] && sourceMatches[2] == targetMatches[2] && sourceMatches[4] == targetMatches[4]); }, - _getBackupFileForSameDate: - function PB__getBackupFileForSameDate(aFilename) { - return Task.spawn(function() { - let backupFolderPath = yield this.getBackupFolder(); - let iterator = new OS.File.DirectoryIterator(backupFolderPath); - let backupFile; + _getBackupFileForSameDate: + function PB__getBackupFileForSameDate(aFilename) { + return Task.spawn(function* () { + let backupFolderPath = yield this.getBackupFolder(); + let iterator = new OS.File.DirectoryIterator(backupFolderPath); + let backupFile; + + yield iterator.forEach(function(aEntry) { + if (this._isFilenameWithSameDate(aEntry.name, aFilename)) { + backupFile = aEntry.path; + return iterator.close(); + } + }.bind(this)); + yield iterator.close(); + + return backupFile; + }.bind(this)); + }, + + /** + * Gets a bookmarks tree representation usable to create backups in different + * file formats. The root or the tree is PlacesUtils.placesRootId. + * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their + * descendants are excluded. + * + * @return an object representing a tree with the places root as its root. + * Each bookmark is represented by an object having these properties: + * * id: the item id (make this not enumerable after bug 824502) + * * title: the title + * * guid: unique id + * * parent: item id of the parent folder, not enumerable + * * index: the position in the parent + * * dateAdded: microseconds from the epoch + * * lastModified: microseconds from the epoch + * * type: type of the originating node as defined in PlacesUtils + * The following properties exist only for a subset of bookmarks: + * * annos: array of annotations + * * uri: url + * * keyword: associated keyword + * * charset: last known charset + * * tags: csv string of tags + * * root: string describing whether this represents a root + * * children: array of child items in a folder + */ + getBookmarksTree: function () { + return Task.spawn(function* () { + let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite"); + let conn = yield Sqlite.openConnection({ path: dbFilePath, + sharedMemoryCache: false }); + let rows = []; + try { + rows = yield conn.execute( + "SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " + + "b.position AS [index], b.type, b.dateAdded, b.lastModified, b.guid, " + + "( SELECT GROUP_CONCAT(t.title, ',') " + + "FROM moz_bookmarks b2 " + + "JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " + + "WHERE b2.fk = h.id " + + ") AS tags, " + + "EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " + + "( SELECT a.content FROM moz_annos a " + + "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " + + "WHERE place_id = h.id AND n.name = :charset_anno " + + ") AS charset " + + "FROM moz_bookmarks b " + + "LEFT JOIN moz_bookmarks p ON p.id = b.parent " + + "LEFT JOIN moz_places h ON h.id = b.fk " + + "WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " + + "ORDER BY b.parent, b.position", + { tags_folder: PlacesUtils.tagsFolderId, + charset_anno: PlacesUtils.CHARSET_ANNO }); + } catch(e) { + Cu.reportError("Unable to query the database " + e); + } finally { + yield conn.close(); + } + + let startTime = Date.now(); + // Create a Map for lookup and recursive building of the tree. + let itemsMap = new Map(); + for (let row of rows) { + let id = row.getResultByName("id"); + try { + let bookmark = sqliteRowToBookmarkObject(row); + if (itemsMap.has(id)) { + // Since children may be added before parents, we should merge with + // the existing object. + let original = itemsMap.get(id); + for (prop in bookmark) { + original[prop] = bookmark[prop]; + } + bookmark = original; + } + else { + itemsMap.set(id, bookmark); + } - yield iterator.forEach(function(aEntry) { - if (this._isFilenameWithSameDate(aEntry.name, aFilename)) { - backupFile = aEntry.path; - return iterator.close(); - } - }.bind(this)); - yield iterator.close(); + // Append bookmark to its parent. + if (!itemsMap.has(bookmark.parent)) + itemsMap.set(bookmark.parent, {}); + let parent = itemsMap.get(bookmark.parent); + if (!("children" in parent)) + parent.children = []; + parent.children.push(bookmark); + } catch (e) { + Cu.reportError("Error while reading node " + id + " " + e); + } + } + + // Handle excluded items, by removing entire subtrees pointed by them. + function removeFromMap(id) { + // Could have been removed by a previous call, since we can't + // predict order of items in EXCLUDE_FROM_BACKUP_ANNO. + if (itemsMap.has(id)) { + let excludedItem = itemsMap.get(id); + if (excludedItem.children) { + for (let child of excludedItem.children) { + removeFromMap(child.id); + } + } + // Remove the excluded item from its parent's children... + let parentItem = itemsMap.get(excludedItem.parent); + parentItem.children = parentItem.children.filter(aChild => aChild.id != id); + // ...then remove it from the map. + itemsMap.delete(id); + } + } + + for (let id of PlacesUtils.annotations.getItemsWithAnnotation( + PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) { + removeFromMap(id); + } + + // Report the time taken to build the tree. This doesn't take into + // account the time spent in the query since that's off the main-thread. + try { + Services.telemetry + .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS") + .add(Date.now() - startTime); + } catch (ex) { + Components.utils.reportError("Unable to report telemetry."); + } + + return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size]; + }); + } +} - throw new Task.Result(backupFile); - }.bind(this)); - } +/** + * Helper function to convert a Sqlite.jsm row to a bookmark object + * representation. + * + * @param aRow The Sqlite.jsm result row. + */ +function sqliteRowToBookmarkObject(aRow) { + let bookmark = {}; + for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) { + bookmark[p] = aRow.getResultByName(p); + } + Object.defineProperty(bookmark, "parent", + { value: aRow.getResultByName("parent") }); + + let type = aRow.getResultByName("type"); + + // Add annotations. + if (aRow.getResultByName("has_annos")) { + try { + bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id); + } catch (e) { + Cu.reportError("Unexpected error while reading annotations " + e); + } + } + + switch (type) { + case Ci.nsINavBookmarksService.TYPE_BOOKMARK: + // TODO: What about shortcuts? + bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE; + // This will throw if we try to serialize an invalid url and the node will + // just be skipped. + bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec; + // Keywords are cached, so this should be decently fast. + let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id); + if (keyword) + bookmark.keyword = keyword; + let charset = aRow.getResultByName("charset"); + if (charset) + bookmark.charset = charset; + let tags = aRow.getResultByName("tags"); + if (tags) + bookmark.tags = tags; + break; + case Ci.nsINavBookmarksService.TYPE_FOLDER: + bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER; + + // Mark root folders. + if (bookmark.id == PlacesUtils.placesRootId) + bookmark.root = "placesRoot"; + else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId) + bookmark.root = "bookmarksMenuFolder"; + else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId) + bookmark.root = "unfiledBookmarksFolder"; + else if (bookmark.id == PlacesUtils.toolbarFolderId) + bookmark.root = "toolbarFolder"; + break; + case Ci.nsINavBookmarksService.TYPE_SEPARATOR: + bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR; + break; + default: + Cu.reportError("Unexpected bookmark type"); + break; + } + return bookmark; }
--- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -2839,16 +2839,40 @@ "PLACES_KEYWORDS_COUNT": { "expires_in_version": "never", "kind": "exponential", "high": "200", "n_buckets": 10, "extended_statistics_ok": true, "description": "PLACES: Number of keywords" }, + "PLACES_BACKUPS_DAYSFROMLAST": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 15, + "description": "PLACES: Days from last backup" + }, + "PLACES_BACKUPS_BOOKMARKSTREE_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 10, + "extended_statistics_ok": true, + "description": "PLACES: Time to build the bookmarks tree" + }, + "PLACES_BACKUPS_TOJSON_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 50, + "high": 2000, + "n_buckets": 10, + "extended_statistics_ok": true, + "description": "PLACES: Time to convert and write the backup" + }, "FENNEC_FAVICONS_COUNT": { "expires_in_version": "never", "kind": "exponential", "high": "2000", "n_buckets": 10, "cpp_guard": "ANDROID", "extended_statistics_ok": true, "description": "FENNEC: (Places) Number of favicons stored"