Files
tubestation/browser/components/places/PlacesBrowserStartup.sys.mjs
Gijs Kruitbosch 78ebe33ed8 Bug 1962056 - move places initialization out of BrowserGlue, r=places-reviewers,migration-reviewers,firefox-desktop-core-reviewers ,mossop,mak,mconley
Places frontend initialization is surprisingly complex, and disentangling it
from the rest of startup by moving it to its own file helps make some of the
logic a little more obvious, and makes unit-testing a bit easier.

This also removes BrowserGlue from the indirection mechanism used between
MigatorBase instances and Places, by switching to category-manager-based
invocation - this way, migrator code does not need to directly
call places code to tell it it's done, but we don't need BrowserGlue to play
messenger between them.

It would be nice to do the same thing for `places-init-complete` but
that is notified from C++ code so unfortunately that is not easily possible.

Differential Revision: https://phabricator.services.mozilla.com/D244428
2025-05-21 13:54:58 +00:00

382 lines
14 KiB
JavaScript

/* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
DistributionManagement: "resource:///modules/distribution.sys.mjs",
PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetters(lazy, {
BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
UserIdleService: [
"@mozilla.org/widget/useridleservice;1",
"nsIUserIdleService",
],
});
// Seconds of idle before trying to create a bookmarks backup.
const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
// Minimum interval between backups. We try to not create more than one backup
// per interval.
const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
export let PlacesBrowserStartup = {
_migrationImportsDefaultBookmarks: false,
_placesInitialized: false,
_placesBrowserInitComplete: false,
_isObservingIdle: false,
_bookmarksBackupIdleTime: null,
_firstWindowReady: Promise.withResolvers(),
onFirstWindowReady(window) {
this._firstWindowReady.resolve();
// Set the default favicon size for UI views that use the page-icon protocol.
lazy.PlacesUtils.favicons.setDefaultIconURIPreferredSize(
16 * window.devicePixelRatio
);
},
backendInitComplete() {
if (!this._migrationImportsDefaultBookmarks) {
this.initPlaces();
}
},
willImportDefaultBookmarks() {
this._migrationImportsDefaultBookmarks = true;
},
didImportDefaultBookmarks() {
this.initPlaces({ initialMigrationPerformed: true });
},
/**
* Initialize Places
* - imports the bookmarks html file if bookmarks database is empty, try to
* restore bookmarks from a JSON backup if the backend indicates that the
* database was corrupt.
*
* These prefs can be set up by the frontend:
*
* WARNING: setting these preferences to true will overwite existing bookmarks
*
* - browser.places.importBookmarksHTML
* Set to true will import the bookmarks.html file from the profile folder.
* - browser.bookmarks.restore_default_bookmarks
* Set to true by safe-mode dialog to indicate we must restore default
* bookmarks.
*
* @param {object} [options={}]
* @param {boolean} [options.initialMigrationPerformed=false]
* Whether we performed an initial migration from another browser or via
* Firefox Refresh.
*/
initPlaces({ initialMigrationPerformed = false } = {}) {
if (this._placesInitialized) {
throw new Error("Cannot initialize Places more than once");
}
this._placesInitialized = true;
// We must instantiate the history service since it will tell us if we
// need to import or restore bookmarks due to first-run, corruption or
// forced migration (due to a major schema change).
// If the database is corrupt or has been newly created we should
// import bookmarks.
let dbStatus = lazy.PlacesUtils.history.databaseStatus;
// Show a notification with a "more info" link for a locked places.sqlite.
if (dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_LOCKED) {
// Note: initPlaces should always happen when the first window is ready,
// in any case, better safe than sorry.
this._firstWindowReady.promise.then(() => {
this._showPlacesLockedNotificationBox();
this._placesBrowserInitComplete = true;
Services.obs.notifyObservers(null, "places-browser-init-complete");
});
return;
}
let importBookmarks =
!initialMigrationPerformed &&
(dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_CREATE ||
dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_CORRUPT);
// Check if user or an extension has required to import bookmarks.html
let importBookmarksHTML = false;
try {
importBookmarksHTML = Services.prefs.getBoolPref(
"browser.places.importBookmarksHTML"
);
if (importBookmarksHTML) {
importBookmarks = true;
}
} catch (ex) {}
// Support legacy bookmarks.html format for apps that depend on that format.
let autoExportHTML = Services.prefs.getBoolPref(
"browser.bookmarks.autoExportHTML",
false
); // Do not export.
if (autoExportHTML) {
// Sqlite.sys.mjs and Places shutdown happen at profile-before-change, thus,
// to be on the safe side, this should run earlier.
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
"Places: export bookmarks.html",
() =>
lazy.BookmarkHTMLUtils.exportToFile(
lazy.BookmarkHTMLUtils.defaultPath
)
);
}
(async () => {
// Check if Safe Mode or the user has required to restore bookmarks from
// default profile's bookmarks.html
let restoreDefaultBookmarks = false;
try {
restoreDefaultBookmarks = Services.prefs.getBoolPref(
"browser.bookmarks.restore_default_bookmarks"
);
if (restoreDefaultBookmarks) {
// Ensure that we already have a bookmarks backup for today.
await this._backupBookmarks();
importBookmarks = true;
}
} catch (ex) {}
// 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
let lastBackupFile = await lazy.PlacesBackups.getMostRecentBackup();
if (lastBackupFile) {
// restore from JSON backup
await lazy.BookmarkJSONUtils.importFromFile(lastBackupFile, {
replace: true,
source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
});
importBookmarks = false;
} else {
// We have created a new database but we don't have any backup available
importBookmarks = true;
if (await IOUtils.exists(lazy.BookmarkHTMLUtils.defaultPath)) {
// If bookmarks.html is available in current profile import it...
importBookmarksHTML = true;
} else {
// ...otherwise we will restore defaults
restoreDefaultBookmarks = true;
}
}
}
// Import default bookmarks when necessary.
// Otherwise, if any kind of import runs, default bookmarks creation should be
// delayed till the import operations has finished. Not doing so would
// cause them to be overwritten by the newly imported bookmarks.
if (!importBookmarks) {
// Now apply distribution customized bookmarks.
// This should always run after Places initialization.
try {
await lazy.DistributionManagement.applyBookmarks();
} catch (e) {
console.error(e);
}
} else {
// An import operation is about to run.
let bookmarksUrl = null;
if (restoreDefaultBookmarks) {
// User wants to restore the default set of bookmarks shipped with the
// browser, those that new profiles start with.
bookmarksUrl = "chrome://browser/content/default-bookmarks.html";
} else if (await IOUtils.exists(lazy.BookmarkHTMLUtils.defaultPath)) {
bookmarksUrl = PathUtils.toFileURI(
lazy.BookmarkHTMLUtils.defaultPath
);
}
if (bookmarksUrl) {
// Import from bookmarks.html file.
try {
if (
Services.policies.isAllowed("defaultBookmarks") &&
// Default bookmarks are imported after startup, and they may
// influence the outcome of tests, thus it's possible to use
// this test-only pref to skip the import.
!(
Cu.isInAutomation &&
Services.prefs.getBoolPref(
"browser.bookmarks.testing.skipDefaultBookmarksImport",
false
)
)
) {
await lazy.BookmarkHTMLUtils.importFromURL(bookmarksUrl, {
replace: true,
source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
});
}
} catch (e) {
console.error("Bookmarks.html file could be corrupt. ", e);
}
try {
// Now apply distribution customized bookmarks.
// This should always run after Places initialization.
await lazy.DistributionManagement.applyBookmarks();
} catch (e) {
console.error(e);
}
} else {
console.error(new Error("Unable to find bookmarks.html file."));
}
// Reset preferences, so we won't try to import again at next run
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.
// If the last backup has been created before the last browser session,
// and is days old, be more aggressive with the idle timer.
let idleTime = BOOKMARKS_BACKUP_IDLE_TIME_SEC;
if (!(await lazy.PlacesBackups.hasRecentBackup())) {
idleTime /= 2;
}
if (!this._isObservingIdle) {
lazy.UserIdleService.addIdleObserver(this._backupBookmarks, idleTime);
Services.obs.addObserver(this, "profile-before-change");
this._isObservingIdle = true;
}
this._bookmarksBackupIdleTime = idleTime;
if (this._isNewProfile) {
// New profiles may have existing bookmarks (imported from another browser or
// copied into the profile) and we want to show the bookmark toolbar for them
// in some cases.
await lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility();
}
})()
.catch(ex => {
console.error(ex);
})
.then(() => {
// NB: deliberately after the catch so that we always do this, even if
// we threw halfway through initializing in the Task above.
this._placesBrowserInitComplete = true;
Services.obs.notifyObservers(null, "places-browser-init-complete");
});
},
/**
* If a backup for today doesn't exist, this creates one.
*/
async _backupBookmarks() {
let lastBackupFile = await lazy.PlacesBackups.getMostRecentBackup();
// Should backup bookmarks if there are no backups or the maximum
// interval between backups elapsed.
if (
!lastBackupFile ||
new Date() - lazy.PlacesBackups.getDateForFile(lastBackupFile) >
BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000
) {
let maxBackups = Services.prefs.getIntPref(
"browser.bookmarks.max_backups"
);
await lazy.PlacesBackups.create(maxBackups);
}
},
/**
* Show the notificationBox for a locked places database.
*/
async _showPlacesLockedNotificationBox() {
var win = lazy.BrowserWindowTracker.getTopWindow();
var buttons = [{ supportPage: "places-locked" }];
var notifyBox = win.gBrowser.getNotificationBox();
var notification = await notifyBox.appendNotification(
"places-locked",
{
label: { "l10n-id": "places-locked-prompt" },
priority: win.gNotificationBox.PRIORITY_CRITICAL_MEDIUM,
},
buttons
);
notification.persistence = -1; // Until user closes it
},
notifyIfInitializationComplete() {
if (this._placesBrowserInitComplete) {
Services.obs.notifyObservers(null, "places-browser-init-complete");
}
},
async maybeAddImportButton() {
// First check if we've already added the import button, in which
// case we should check for events indicating we can remove it.
if (
Services.prefs.getBoolPref("browser.bookmarks.addedImportButton", false)
) {
lazy.PlacesUIUtils.removeImportButtonWhenImportSucceeds();
return;
}
// Otherwise, check if this is a new profile where we need to add it.
// `maybeAddImportButton` will call
// `removeImportButtonWhenImportSucceeds`itself if/when it adds the
// button. Doing things in this order avoids listening for removal
// more than once.
if (
lazy.BrowserHandler.firstRunProfile &&
// Not in automation: the button changes CUI state, breaking tests
!Cu.isInAutomation
) {
await lazy.PlacesUIUtils.maybeAddImportButton();
}
},
handleShutdown() {
if (this._bookmarksBackupIdleTime) {
lazy.UserIdleService.removeIdleObserver(
this._backupBookmarks,
this._bookmarksBackupIdleTime
);
this._bookmarksBackupIdleTime = null;
}
},
observe(subject, topic, _data) {
switch (topic) {
case "profile-before-change":
this.handleShutdown();
break;
}
},
};
PlacesBrowserStartup._backupBookmarks =
PlacesBrowserStartup._backupBookmarks.bind(PlacesBrowserStartup);
PlacesBrowserStartup.QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);