/* 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]);