FirefoxProfileMigrator.sys.mjs (13717B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sw=2 ts=2 sts=2 et */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 /* 8 * Migrates from a Firefox profile in a lossy manner in order to clean up a 9 * user's profile. Data is only migrated where the benefits outweigh the 10 * potential problems caused by importing undesired/invalid configurations 11 * from the source profile. 12 */ 13 14 import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; 15 16 import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; 17 18 const lazy = {}; 19 20 ChromeUtils.defineESModuleGetters(lazy, { 21 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 22 PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs", 23 ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", 24 SessionMigration: "resource:///modules/sessionstore/SessionMigration.sys.mjs", 25 }); 26 27 /** 28 * Firefox profile migrator. Currently, this class only does "pave over" 29 * migrations, where various parts of an old profile overwrite a new 30 * profile. This is distinct from other migrators which attempt to import 31 * old profile data into the existing profile. 32 * 33 * This migrator is what powers the "Profile Refresh" mechanism. 34 */ 35 export class FirefoxProfileMigrator extends MigratorBase { 36 static get key() { 37 return "firefox"; 38 } 39 40 static get displayNameL10nID() { 41 return "migration-wizard-migrator-display-name-firefox"; 42 } 43 44 static get brandImage() { 45 return "chrome://branding/content/icon128.png"; 46 } 47 48 _getAllProfiles() { 49 let allProfiles = new Map(); 50 let profileService = Cc[ 51 "@mozilla.org/toolkit/profile-service;1" 52 ].getService(Ci.nsIToolkitProfileService); 53 for (let profile of profileService.profiles) { 54 let rootDir = profile.rootDir; 55 56 if ( 57 rootDir.exists() && 58 rootDir.isReadable() && 59 !rootDir.equals(MigrationUtils.profileStartup.directory) 60 ) { 61 allProfiles.set(profile.name, rootDir); 62 } 63 } 64 return allProfiles; 65 } 66 67 getSourceProfiles() { 68 let sorter = (a, b) => { 69 return a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase()); 70 }; 71 72 return [...this._getAllProfiles().keys()] 73 .map(x => ({ id: x, name: x })) 74 .sort(sorter); 75 } 76 77 _getFileObject(dir, fileName) { 78 let file = dir.clone(); 79 file.append(fileName); 80 81 // File resources are monolithic. We don't make partial copies since 82 // they are not expected to work alone. Return null to avoid trying to 83 // copy non-existing files. 84 return file.exists() ? file : null; 85 } 86 87 getResources(aProfile) { 88 let sourceProfileDir = aProfile 89 ? this._getAllProfiles().get(aProfile.id) 90 : Cc["@mozilla.org/toolkit/profile-service;1"].getService( 91 Ci.nsIToolkitProfileService 92 ).defaultProfile.rootDir; 93 if ( 94 !sourceProfileDir || 95 !sourceProfileDir.exists() || 96 !sourceProfileDir.isReadable() 97 ) { 98 return null; 99 } 100 101 // Being a startup-only migrator, we can rely on 102 // MigrationUtils.profileStartup being set. 103 let currentProfileDir = MigrationUtils.profileStartup.directory; 104 105 // Surely data cannot be imported from the current profile. 106 if (sourceProfileDir.equals(currentProfileDir)) { 107 return null; 108 } 109 110 return this._getResourcesInternal(sourceProfileDir, currentProfileDir); 111 } 112 113 getLastUsedDate() { 114 // We always pretend we're really old, so that we don't mess 115 // up the determination of which browser is the most 'recent' 116 // to import from. 117 return Promise.resolve(new Date(0)); 118 } 119 120 _getResourcesInternal(sourceProfileDir, currentProfileDir) { 121 let getFileResource = (aMigrationType, aFileNames) => { 122 let files = []; 123 for (let fileName of aFileNames) { 124 let file = this._getFileObject(sourceProfileDir, fileName); 125 if (file) { 126 files.push(file); 127 } 128 } 129 if (!files.length) { 130 return null; 131 } 132 return { 133 type: aMigrationType, 134 migrate(aCallback) { 135 for (let file of files) { 136 file.copyTo(currentProfileDir, ""); 137 } 138 aCallback(true); 139 }, 140 }; 141 }; 142 143 let _oldRawPrefsMemoized = null; 144 async function readOldPrefs() { 145 if (!_oldRawPrefsMemoized) { 146 let prefsPath = PathUtils.join(sourceProfileDir.path, "prefs.js"); 147 if (await IOUtils.exists(prefsPath)) { 148 _oldRawPrefsMemoized = await IOUtils.readUTF8(prefsPath, { 149 encoding: "utf-8", 150 }); 151 } 152 } 153 154 return _oldRawPrefsMemoized; 155 } 156 157 function savePrefs() { 158 // If we've used the pref service to write prefs for the new profile, it's too 159 // early in startup for the service to have a profile directory, so we have to 160 // manually tell it where to save the prefs file. 161 let newPrefsFile = currentProfileDir.clone(); 162 newPrefsFile.append("prefs.js"); 163 Services.prefs.savePrefFile(newPrefsFile); 164 } 165 166 function configureHomepage(resetSession) { 167 // We just refreshed the profile, so don't show the profile reset prompt 168 // on the homepage. 169 Services.prefs.setBoolPref("browser.disableResetPrompt", true); 170 if (resetSession) { 171 // We're resetting the user's session, not creating a new one. Set the 172 // homepage_override prefs so that the browser doesn't override our 173 // session with an unwanted homepage. 174 let buildID = Services.appinfo.platformBuildID; 175 let mstone = Services.appinfo.platformVersion; 176 Services.prefs.setCharPref( 177 "browser.startup.homepage_override.mstone", 178 mstone 179 ); 180 Services.prefs.setCharPref( 181 "browser.startup.homepage_override.buildID", 182 buildID 183 ); 184 } 185 } 186 187 let types = MigrationUtils.resourceTypes; 188 let places = getFileResource(types.HISTORY, [ 189 "places.sqlite", 190 "places.sqlite-wal", 191 ]); 192 let favicons = getFileResource(types.HISTORY, [ 193 "favicons.sqlite", 194 "favicons.sqlite-wal", 195 ]); 196 let cookies = getFileResource(types.COOKIES, [ 197 "cookies.sqlite", 198 "cookies.sqlite-wal", 199 ]); 200 let passwords = getFileResource(types.PASSWORDS, [ 201 "logins.json", 202 "key3.db", 203 "key4.db", 204 ]); 205 let formData = getFileResource(types.FORMDATA, [ 206 "formhistory.sqlite", 207 "autofill-profiles.json", 208 ]); 209 let bookmarksBackups = getFileResource(types.OTHERDATA, [ 210 lazy.PlacesBackups.profileRelativeFolderPath, 211 ]); 212 let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]); 213 214 // Determine if we want to restore the previous session or start a new one 215 const NEW_SESSION = "0"; 216 const RESTORE_SESSION = "1"; 217 let resetSession = Services.env.get("MOZ_RESET_PROFILE_SESSION"); 218 Services.env.set("MOZ_RESET_PROFILE_SESSION", ""); 219 220 let session; 221 if (resetSession === RESTORE_SESSION) { 222 // We only want to restore the previous firefox session if the profile 223 // refresh was triggered by the user, such as through about:support. In 224 // these cases, MOZ_RESET_PROFILE_SESSION is set to restore, signaling 225 // that session data migration is required. 226 let sessionCheckpoints = this._getFileObject( 227 sourceProfileDir, 228 "sessionCheckpoints.json" 229 ); 230 let sessionFile = this._getFileObject( 231 sourceProfileDir, 232 "sessionstore.jsonlz4" 233 ); 234 if (sessionFile) { 235 session = { 236 type: types.SESSION, 237 migrate(aCallback) { 238 sessionCheckpoints.copyTo( 239 currentProfileDir, 240 "sessionCheckpoints.json" 241 ); 242 let newSessionFile = currentProfileDir.clone(); 243 newSessionFile.append("sessionstore.jsonlz4"); 244 let migrationPromise = lazy.SessionMigration.migrate( 245 sessionFile.path, 246 newSessionFile.path 247 ); 248 migrationPromise.then( 249 function () { 250 // Force the browser to one-off resume the session that we give it: 251 Services.prefs.setBoolPref( 252 "browser.sessionstore.resume_session_once", 253 true 254 ); 255 configureHomepage(true); 256 savePrefs(); 257 aCallback(true); 258 }, 259 function () { 260 aCallback(false); 261 } 262 ); 263 }, 264 }; 265 } 266 } else if (resetSession === NEW_SESSION) { 267 // If this is first startup and the profile refresh was triggered via the 268 // command line, such as through the stub installer, we do not restore the 269 // previous session. 270 configureHomepage(); 271 savePrefs(); 272 } 273 274 // Sync/FxA related data 275 let sync = { 276 name: "sync", // name is used only by tests. 277 type: types.OTHERDATA, 278 migrate: async aCallback => { 279 // Try and parse a signedInUser.json file from the source directory and 280 // if we can, copy it to the new profile and set sync's username pref 281 // (which acts as a de-facto flag to indicate if sync is configured) 282 try { 283 let oldPath = PathUtils.join( 284 sourceProfileDir.path, 285 "signedInUser.json" 286 ); 287 let exists = await IOUtils.exists(oldPath); 288 if (exists) { 289 let data = await IOUtils.readJSON(oldPath); 290 if (data && data.accountData && data.accountData.email) { 291 let username = data.accountData.email; 292 // copy the file itself. 293 await IOUtils.copy( 294 oldPath, 295 PathUtils.join(currentProfileDir.path, "signedInUser.json") 296 ); 297 // Now we need to know whether Sync is actually configured for this 298 // user. The only way we know is by looking at the prefs file from 299 // the old profile. We avoid trying to do a full parse of the prefs 300 // file and even avoid parsing the single string value we care 301 // about. 302 let oldRawPrefs = await readOldPrefs(); 303 if (/^user_pref\("services\.sync\.username"/m.test(oldRawPrefs)) { 304 // sync's configured in the source profile - ensure it is in the 305 // new profile too. 306 // Write it to prefs.js and flush the file. 307 Services.prefs.setStringPref( 308 "services.sync.username", 309 username 310 ); 311 savePrefs(); 312 } 313 } 314 } 315 } catch (ex) { 316 aCallback(false); 317 return; 318 } 319 aCallback(true); 320 }, 321 }; 322 323 // Telemetry related migrations. 324 let times = { 325 name: "times", // name is used only by tests. 326 type: types.OTHERDATA, 327 migrate: aCallback => { 328 let file = this._getFileObject(sourceProfileDir, "times.json"); 329 if (file) { 330 file.copyTo(currentProfileDir, ""); 331 } 332 // And record the fact a migration (ie, a reset) happened. 333 let recordMigration = async () => { 334 try { 335 let profileTimes = await lazy.ProfileAge(currentProfileDir.path); 336 await profileTimes.recordProfileReset(); 337 aCallback(true); 338 } catch (e) { 339 aCallback(false); 340 } 341 }; 342 343 recordMigration(); 344 }, 345 }; 346 let telemetry = { 347 name: "telemetry", // name is used only by tests... 348 type: types.OTHERDATA, 349 migrate: async aCallback => { 350 let createSubDir = name => { 351 let dir = currentProfileDir.clone(); 352 dir.append(name); 353 dir.create(Ci.nsIFile.DIRECTORY_TYPE, lazy.FileUtils.PERMS_DIRECTORY); 354 return dir; 355 }; 356 357 // If the 'datareporting' directory exists we migrate files from it. 358 let dataReportingDir = this._getFileObject( 359 sourceProfileDir, 360 "datareporting" 361 ); 362 if (dataReportingDir && dataReportingDir.isDirectory()) { 363 // Copy only specific files. 364 let toCopy = ["state.json", "session-state.json"]; 365 366 let dest = createSubDir("datareporting"); 367 let enumerator = dataReportingDir.directoryEntries; 368 while (enumerator.hasMoreElements()) { 369 let file = enumerator.nextFile; 370 if (file.isDirectory() || !toCopy.includes(file.leafName)) { 371 continue; 372 } 373 file.copyTo(dest, ""); 374 } 375 } 376 377 try { 378 let oldRawPrefs = await readOldPrefs(); 379 let writePrefs = false; 380 const PREFS = ["bookmarks", "csvpasswords", "history", "passwords"]; 381 382 for (let pref of PREFS) { 383 let fullPref = `browser\.migrate\.interactions\.${pref}`; 384 let regex = new RegExp('^user_pref\\("' + fullPref, "m"); 385 if (regex.test(oldRawPrefs)) { 386 Services.prefs.setBoolPref(fullPref, true); 387 writePrefs = true; 388 } 389 } 390 391 if (writePrefs) { 392 savePrefs(); 393 } 394 } catch (e) { 395 aCallback(false); 396 return; 397 } 398 399 aCallback(true); 400 }, 401 }; 402 403 return [ 404 places, 405 cookies, 406 passwords, 407 formData, 408 dictionary, 409 bookmarksBackups, 410 session, 411 sync, 412 times, 413 telemetry, 414 favicons, 415 ].filter(r => r); 416 } 417 418 get startupOnlyMigrator() { 419 return true; 420 } 421 }