PlacesBrowserStartup.sys.mjs (13934B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", 10 BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", 11 BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs", 12 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 13 DistributionManagement: "resource:///modules/distribution.sys.mjs", 14 PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs", 15 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 16 PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs", 17 }); 18 19 XPCOMUtils.defineLazyServiceGetters(lazy, { 20 BrowserHandler: ["@mozilla.org/browser/clh;1", Ci.nsIBrowserHandler], 21 UserIdleService: [ 22 "@mozilla.org/widget/useridleservice;1", 23 Ci.nsIUserIdleService, 24 ], 25 }); 26 27 // Seconds of idle before trying to create a bookmarks backup. 28 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60; 29 // Minimum interval between backups. We try to not create more than one backup 30 // per interval. 31 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1; 32 33 export let PlacesBrowserStartup = { 34 _migrationImportsDefaultBookmarks: false, 35 _placesInitialized: false, 36 _placesBrowserInitComplete: false, 37 _isObservingIdle: false, 38 _bookmarksBackupIdleTime: null, 39 _firstWindowReady: Promise.withResolvers(), 40 41 onFirstWindowReady(window) { 42 this._firstWindowReady.resolve(); 43 // Set the default favicon size for UI views that use the page-icon protocol. 44 lazy.PlacesUtils.favicons.setDefaultIconURIPreferredSize( 45 16 * window.devicePixelRatio 46 ); 47 }, 48 49 backendInitComplete() { 50 if (!this._migrationImportsDefaultBookmarks) { 51 this.initPlaces(); 52 } 53 }, 54 55 willImportDefaultBookmarks() { 56 this._migrationImportsDefaultBookmarks = true; 57 }, 58 59 didImportDefaultBookmarks() { 60 this.initPlaces({ initialMigrationPerformed: true }); 61 }, 62 63 /** 64 * Initialize Places 65 * - imports the bookmarks html file if bookmarks database is empty, try to 66 * restore bookmarks from a JSON backup if the backend indicates that the 67 * database was corrupt. 68 * 69 * These prefs can be set up by the frontend: 70 * 71 * WARNING: setting these preferences to true will overwite existing bookmarks 72 * 73 * - browser.places.importBookmarksHTML 74 * Set to true will import the bookmarks.html file from the profile folder. 75 * - browser.bookmarks.restore_default_bookmarks 76 * Set to true by safe-mode dialog to indicate we must restore default 77 * bookmarks. 78 * 79 * @param {object} [options={}] 80 * @param {boolean} [options.initialMigrationPerformed=false] 81 * Whether we performed an initial migration from another browser or via 82 * Firefox Refresh. 83 */ 84 initPlaces({ initialMigrationPerformed = false } = {}) { 85 if (this._placesInitialized) { 86 throw new Error("Cannot initialize Places more than once"); 87 } 88 this._placesInitialized = true; 89 90 // We must instantiate the history service since it will tell us if we 91 // need to import or restore bookmarks due to first-run, corruption or 92 // forced migration (due to a major schema change). 93 // If the database is corrupt or has been newly created we should 94 // import bookmarks. 95 let dbStatus = lazy.PlacesUtils.history.databaseStatus; 96 97 // Show a notification with a "more info" link for a locked places.sqlite. 98 if (dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_LOCKED) { 99 // Note: initPlaces should always happen when the first window is ready, 100 // in any case, better safe than sorry. 101 this._firstWindowReady.promise.then(() => { 102 this._showPlacesLockedNotificationBox(); 103 this._placesBrowserInitComplete = true; 104 Services.obs.notifyObservers(null, "places-browser-init-complete"); 105 }); 106 return; 107 } 108 109 let importBookmarks = 110 !initialMigrationPerformed && 111 (dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_CREATE || 112 dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_CORRUPT); 113 114 // Check if user or an extension has required to import bookmarks.html 115 let importBookmarksHTML = false; 116 try { 117 importBookmarksHTML = Services.prefs.getBoolPref( 118 "browser.places.importBookmarksHTML" 119 ); 120 if (importBookmarksHTML) { 121 importBookmarks = true; 122 } 123 } catch (ex) {} 124 125 // Support legacy bookmarks.html format for apps that depend on that format. 126 let autoExportHTML = Services.prefs.getBoolPref( 127 "browser.bookmarks.autoExportHTML", 128 false 129 ); // Do not export. 130 if (autoExportHTML) { 131 // Sqlite.sys.mjs and Places shutdown happen at profile-before-change, thus, 132 // to be on the safe side, this should run earlier. 133 lazy.AsyncShutdown.profileChangeTeardown.addBlocker( 134 "Places: export bookmarks.html", 135 () => 136 lazy.BookmarkHTMLUtils.exportToFile( 137 lazy.BookmarkHTMLUtils.defaultPath 138 ) 139 ); 140 } 141 142 (async () => { 143 // Check if Safe Mode or the user has required to restore bookmarks from 144 // default profile's bookmarks.html 145 let restoreDefaultBookmarks = false; 146 try { 147 restoreDefaultBookmarks = Services.prefs.getBoolPref( 148 "browser.bookmarks.restore_default_bookmarks" 149 ); 150 if (restoreDefaultBookmarks) { 151 // Ensure that we already have a bookmarks backup for today. 152 await this._backupBookmarks(); 153 importBookmarks = true; 154 } 155 } catch (ex) {} 156 157 // If the user did not require to restore default bookmarks, or import 158 // from bookmarks.html, we will try to restore from JSON 159 if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) { 160 // get latest JSON backup 161 let lastBackupFile = await lazy.PlacesBackups.getMostRecentBackup(); 162 if (lastBackupFile) { 163 // restore from JSON backup 164 await lazy.BookmarkJSONUtils.importFromFile(lastBackupFile, { 165 replace: true, 166 source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP, 167 }); 168 importBookmarks = false; 169 } else { 170 // We have created a new database but we don't have any backup available 171 importBookmarks = true; 172 if (await IOUtils.exists(lazy.BookmarkHTMLUtils.defaultPath)) { 173 // If bookmarks.html is available in current profile import it... 174 importBookmarksHTML = true; 175 } else { 176 // ...otherwise we will restore defaults 177 restoreDefaultBookmarks = true; 178 } 179 } 180 } 181 182 // Import default bookmarks when necessary. 183 // Otherwise, if any kind of import runs, default bookmarks creation should be 184 // delayed till the import operations has finished. Not doing so would 185 // cause them to be overwritten by the newly imported bookmarks. 186 if (!importBookmarks) { 187 // Now apply distribution customized bookmarks. 188 // This should always run after Places initialization. 189 try { 190 await lazy.DistributionManagement.applyBookmarks(); 191 } catch (e) { 192 console.error(e); 193 } 194 } else { 195 // An import operation is about to run. 196 let bookmarksUrl = null; 197 if (restoreDefaultBookmarks) { 198 // User wants to restore the default set of bookmarks shipped with the 199 // browser, those that new profiles start with. 200 bookmarksUrl = "chrome://browser/content/default-bookmarks.html"; 201 } else if (await IOUtils.exists(lazy.BookmarkHTMLUtils.defaultPath)) { 202 bookmarksUrl = PathUtils.toFileURI( 203 lazy.BookmarkHTMLUtils.defaultPath 204 ); 205 } 206 207 if (bookmarksUrl) { 208 // Import from bookmarks.html file. 209 try { 210 if ( 211 Services.policies.isAllowed("defaultBookmarks") && 212 // Default bookmarks are imported after startup, and they may 213 // influence the outcome of tests, thus it's possible to use 214 // this test-only pref to skip the import. 215 !( 216 Cu.isInAutomation && 217 Services.prefs.getBoolPref( 218 "browser.bookmarks.testing.skipDefaultBookmarksImport", 219 false 220 ) 221 ) 222 ) { 223 await lazy.BookmarkHTMLUtils.importFromURL(bookmarksUrl, { 224 replace: true, 225 source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP, 226 }); 227 } 228 } catch (e) { 229 console.error("Bookmarks.html file could be corrupt. ", e); 230 } 231 try { 232 // Now apply distribution customized bookmarks. 233 // This should always run after Places initialization. 234 await lazy.DistributionManagement.applyBookmarks(); 235 } catch (e) { 236 console.error(e); 237 } 238 } else { 239 console.error(new Error("Unable to find bookmarks.html file.")); 240 } 241 242 // Reset preferences, so we won't try to import again at next run 243 if (importBookmarksHTML) { 244 Services.prefs.setBoolPref( 245 "browser.places.importBookmarksHTML", 246 false 247 ); 248 } 249 if (restoreDefaultBookmarks) { 250 Services.prefs.setBoolPref( 251 "browser.bookmarks.restore_default_bookmarks", 252 false 253 ); 254 } 255 } 256 257 // Initialize bookmark archiving on idle. 258 // If the last backup has been created before the last browser session, 259 // and is days old, be more aggressive with the idle timer. 260 let idleTime = BOOKMARKS_BACKUP_IDLE_TIME_SEC; 261 if (!(await lazy.PlacesBackups.hasRecentBackup())) { 262 idleTime /= 2; 263 } 264 265 if (!this._isObservingIdle) { 266 lazy.UserIdleService.addIdleObserver(this._backupBookmarks, idleTime); 267 Services.obs.addObserver(this, "profile-before-change"); 268 this._isObservingIdle = true; 269 } 270 271 this._bookmarksBackupIdleTime = idleTime; 272 273 if (this._isNewProfile) { 274 // New profiles may have existing bookmarks (imported from another browser or 275 // copied into the profile) and we want to show the bookmark toolbar for them 276 // in some cases. 277 await lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility(); 278 } 279 })() 280 .catch(ex => { 281 console.error(ex); 282 }) 283 .then(() => { 284 // NB: deliberately after the catch so that we always do this, even if 285 // we threw halfway through initializing in the Task above. 286 this._placesBrowserInitComplete = true; 287 Services.obs.notifyObservers(null, "places-browser-init-complete"); 288 }); 289 }, 290 291 /** 292 * If a backup for today doesn't exist, this creates one. 293 */ 294 async _backupBookmarks() { 295 let lastBackupFile = await lazy.PlacesBackups.getMostRecentBackup(); 296 // Should backup bookmarks if there are no backups or the maximum 297 // interval between backups elapsed. 298 if ( 299 !lastBackupFile || 300 Date.now() - lazy.PlacesBackups.getDateForFile(lastBackupFile).getTime() > 301 BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000 302 ) { 303 let maxBackups = Services.prefs.getIntPref( 304 "browser.bookmarks.max_backups" 305 ); 306 await lazy.PlacesBackups.create(maxBackups); 307 } 308 }, 309 310 /** 311 * Show the notificationBox for a locked places database. 312 */ 313 async _showPlacesLockedNotificationBox() { 314 var win = lazy.BrowserWindowTracker.getTopWindow({ 315 allowFromInactiveWorkspace: true, 316 }); 317 var buttons = [{ supportPage: "places-locked" }]; 318 319 var notifyBox = win.gBrowser.getNotificationBox(); 320 var notification = await notifyBox.appendNotification( 321 "places-locked", 322 { 323 label: { "l10n-id": "places-locked-prompt" }, 324 priority: win.gNotificationBox.PRIORITY_CRITICAL_MEDIUM, 325 }, 326 buttons 327 ); 328 notification.persistence = -1; // Until user closes it 329 }, 330 331 notifyIfInitializationComplete() { 332 if (this._placesBrowserInitComplete) { 333 Services.obs.notifyObservers(null, "places-browser-init-complete"); 334 } 335 }, 336 337 async maybeAddImportButton() { 338 // First check if we've already added the import button, in which 339 // case we should check for events indicating we can remove it. 340 if ( 341 Services.prefs.getBoolPref("browser.bookmarks.addedImportButton", false) 342 ) { 343 lazy.PlacesUIUtils.removeImportButtonWhenImportSucceeds(); 344 return; 345 } 346 347 // Otherwise, check if this is a new profile where we need to add it. 348 // `maybeAddImportButton` will call 349 // `removeImportButtonWhenImportSucceeds`itself if/when it adds the 350 // button. Doing things in this order avoids listening for removal 351 // more than once. 352 if ( 353 lazy.BrowserHandler.firstRunProfile && 354 // Not in automation: the button changes CUI state, breaking tests 355 !Cu.isInAutomation 356 ) { 357 await lazy.PlacesUIUtils.maybeAddImportButton(); 358 } 359 }, 360 361 handleShutdown() { 362 if (this._bookmarksBackupIdleTime) { 363 lazy.UserIdleService.removeIdleObserver( 364 this._backupBookmarks, 365 this._bookmarksBackupIdleTime 366 ); 367 this._bookmarksBackupIdleTime = null; 368 } 369 }, 370 371 observe(subject, topic, _data) { 372 switch (topic) { 373 case "profile-before-change": 374 this.handleShutdown(); 375 break; 376 } 377 }, 378 }; 379 380 PlacesBrowserStartup._backupBookmarks = 381 PlacesBrowserStartup._backupBookmarks.bind(PlacesBrowserStartup); 382 383 PlacesBrowserStartup.QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);