diff --git a/browser/components/migration/ChromeProfileMigrator.sys.mjs b/browser/components/migration/ChromeProfileMigrator.sys.mjs index 42c12e2a2974..01f1265b98fa 100644 --- a/browser/components/migration/ChromeProfileMigrator.sys.mjs +++ b/browser/components/migration/ChromeProfileMigrator.sys.mjs @@ -11,13 +11,9 @@ const AUTH_TYPE = { }; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; - -import { - MigratorPrototype, - MigrationUtils, -} from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; const lazy = {}; @@ -72,136 +68,297 @@ function convertBookmarks(items, errorAccumulator) { return itemsToInsert; } -export function ChromeProfileMigrator() { - this._chromeUserDataPathSuffix = "Chrome"; -} +/** + * Chrome profile migrator. This can also be used as a parent class for + * migrators for browsers that are variants of Chrome. + */ +export class ChromeProfileMigrator extends MigratorBase { + get classDescription() { + return "Chrome Profile Migrator"; + } -ChromeProfileMigrator.prototype = Object.create(MigratorPrototype); + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chrome"; + } -ChromeProfileMigrator.prototype._keychainServiceName = "Chrome Safe Storage"; -ChromeProfileMigrator.prototype._keychainAccountName = "Chrome"; + get classID() { + return Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}"); + } -ChromeProfileMigrator.prototype._getChromeUserDataPathIfExists = async function() { - if (this._chromeUserDataPath) { + get _chromeUserDataPathSuffix() { + return "Chrome"; + } + + _keychainServiceName = "Chrome Safe Storage"; + + _keychainAccountName = "Chrome"; + + async _getChromeUserDataPathIfExists() { + if (this._chromeUserDataPath) { + return this._chromeUserDataPath; + } + let path = lazy.ChromeMigrationUtils.getDataPath( + this._chromeUserDataPathSuffix + ); + let exists = await IOUtils.exists(path); + if (exists) { + this._chromeUserDataPath = path; + } else { + this._chromeUserDataPath = null; + } return this._chromeUserDataPath; } - let path = lazy.ChromeMigrationUtils.getDataPath( - this._chromeUserDataPathSuffix - ); - let exists = await IOUtils.exists(path); - if (exists) { - this._chromeUserDataPath = path; - } else { - this._chromeUserDataPath = null; - } - return this._chromeUserDataPath; -}; -ChromeProfileMigrator.prototype.getResources = async function Chrome_getResources( - aProfile -) { - let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); - if (chromeUserDataPath) { - let profileFolder = chromeUserDataPath; - if (aProfile) { - profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id); - } - if (await IOUtils.exists(profileFolder)) { - let possibleResourcePromises = [ - GetBookmarksResource(profileFolder, this.getBrowserKey()), - GetHistoryResource(profileFolder), - GetCookiesResource(profileFolder), - ]; - if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) { - possibleResourcePromises.push( - this._GetPasswordsResource(profileFolder) - ); + async getResources(aProfile) { + let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); + if (chromeUserDataPath) { + let profileFolder = chromeUserDataPath; + if (aProfile) { + profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id); } - let possibleResources = await Promise.all(possibleResourcePromises); - return possibleResources.filter(r => r != null); - } - } - return []; -}; - -ChromeProfileMigrator.prototype.getLastUsedDate = async function Chrome_getLastUsedDate() { - let sourceProfiles = await this.getSourceProfiles(); - let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); - if (!chromeUserDataPath) { - return new Date(0); - } - let datePromises = sourceProfiles.map(async profile => { - let basePath = PathUtils.join(chromeUserDataPath, profile.id); - let fileDatePromises = ["Bookmarks", "History", "Cookies"].map( - async leafName => { - let path = PathUtils.join(basePath, leafName); - let info = await IOUtils.stat(path).catch(() => null); - return info ? info.lastModificationDate : 0; + if (await IOUtils.exists(profileFolder)) { + let possibleResourcePromises = [ + GetBookmarksResource(profileFolder, this.getBrowserKey()), + GetHistoryResource(profileFolder), + GetCookiesResource(profileFolder), + ]; + if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) { + possibleResourcePromises.push( + this._GetPasswordsResource(profileFolder) + ); + } + let possibleResources = await Promise.all(possibleResourcePromises); + return possibleResources.filter(r => r != null); } - ); - let dates = await Promise.all(fileDatePromises); - return Math.max(...dates); - }); - let datesOuter = await Promise.all(datePromises); - datesOuter.push(0); - return new Date(Math.max(...datesOuter)); -}; - -ChromeProfileMigrator.prototype.getSourceProfiles = async function Chrome_getSourceProfiles() { - if ("__sourceProfiles" in this) { - return this.__sourceProfiles; - } - - let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); - if (!chromeUserDataPath) { + } return []; } - let localState; - let profiles = []; - try { - localState = await lazy.ChromeMigrationUtils.getLocalState( - this._chromeUserDataPathSuffix - ); - let info_cache = localState.profile.info_cache; - for (let profileFolderName in info_cache) { - profiles.push({ - id: profileFolderName, - name: info_cache[profileFolderName].name || profileFolderName, - }); - } - } catch (e) { - // Avoid reporting NotFoundErrors from trying to get local state. - if (localState || e.name != "NotFoundError") { - Cu.reportError("Error detecting Chrome profiles: " + e); - } - // If we weren't able to detect any profiles above, fallback to the Default profile. - let defaultProfilePath = PathUtils.join(chromeUserDataPath, "Default"); - if (await IOUtils.exists(defaultProfilePath)) { - profiles = [ - { - id: "Default", - name: "Default", - }, - ]; + async getLastUsedDate() { + let sourceProfiles = await this.getSourceProfiles(); + let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); + if (!chromeUserDataPath) { + return new Date(0); } + let datePromises = sourceProfiles.map(async profile => { + let basePath = PathUtils.join(chromeUserDataPath, profile.id); + let fileDatePromises = ["Bookmarks", "History", "Cookies"].map( + async leafName => { + let path = PathUtils.join(basePath, leafName); + let info = await IOUtils.stat(path).catch(() => null); + return info ? info.lastModificationDate : 0; + } + ); + let dates = await Promise.all(fileDatePromises); + return Math.max(...dates); + }); + let datesOuter = await Promise.all(datePromises); + datesOuter.push(0); + return new Date(Math.max(...datesOuter)); } - let profileResources = await Promise.all( - profiles.map(async profile => ({ - profile, - resources: await this.getResources(profile), - })) - ); + async getSourceProfiles() { + if ("__sourceProfiles" in this) { + return this.__sourceProfiles; + } - // Only list profiles from which any data can be imported - this.__sourceProfiles = profileResources - .filter(({ resources }) => { - return resources && !!resources.length; - }, this) - .map(({ profile }) => profile); - return this.__sourceProfiles; -}; + let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); + if (!chromeUserDataPath) { + return []; + } + + let localState; + let profiles = []; + try { + localState = await lazy.ChromeMigrationUtils.getLocalState( + this._chromeUserDataPathSuffix + ); + let info_cache = localState.profile.info_cache; + for (let profileFolderName in info_cache) { + profiles.push({ + id: profileFolderName, + name: info_cache[profileFolderName].name || profileFolderName, + }); + } + } catch (e) { + // Avoid reporting NotFoundErrors from trying to get local state. + if (localState || e.name != "NotFoundError") { + Cu.reportError("Error detecting Chrome profiles: " + e); + } + // If we weren't able to detect any profiles above, fallback to the Default profile. + let defaultProfilePath = PathUtils.join(chromeUserDataPath, "Default"); + if (await IOUtils.exists(defaultProfilePath)) { + profiles = [ + { + id: "Default", + name: "Default", + }, + ]; + } + } + + let profileResources = await Promise.all( + profiles.map(async profile => ({ + profile, + resources: await this.getResources(profile), + })) + ); + + // Only list profiles from which any data can be imported + this.__sourceProfiles = profileResources + .filter(({ resources }) => { + return resources && !!resources.length; + }, this) + .map(({ profile }) => profile); + return this.__sourceProfiles; + } + + async _GetPasswordsResource(aProfileFolder) { + let loginPath = PathUtils.join(aProfileFolder, "Login Data"); + if (!(await IOUtils.exists(loginPath))) { + return null; + } + + let { + _chromeUserDataPathSuffix, + _keychainServiceName, + _keychainAccountName, + _keychainMockPassphrase = null, + } = this; + + return { + type: MigrationUtils.resourceTypes.PASSWORDS, + + async migrate(aCallback) { + let rows = await MigrationUtils.getRowsFromDBWithoutLocks( + loginPath, + "Chrome passwords", + `SELECT origin_url, action_url, username_element, username_value, + password_element, password_value, signon_realm, scheme, date_created, + times_used FROM logins WHERE blacklisted_by_user = 0` + ).catch(ex => { + Cu.reportError(ex); + aCallback(false); + }); + // If the promise was rejected we will have already called aCallback, + // so we can just return here. + if (!rows) { + return; + } + + // If there are no relevant rows, return before initializing crypto and + // thus prompting for Keychain access on macOS. + if (!rows.length) { + aCallback(true); + return; + } + + let crypto; + try { + if (AppConstants.platform == "win") { + let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule( + "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs" + ); + crypto = new ChromeWindowsLoginCrypto(_chromeUserDataPathSuffix); + } else if (AppConstants.platform == "macosx") { + let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule( + "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs" + ); + crypto = new ChromeMacOSLoginCrypto( + _keychainServiceName, + _keychainAccountName, + _keychainMockPassphrase + ); + } else { + aCallback(false); + return; + } + } catch (ex) { + // Handle the user canceling Keychain access or other OSCrypto errors. + Cu.reportError(ex); + aCallback(false); + return; + } + + let logins = []; + let fallbackCreationDate = new Date(); + for (let row of rows) { + try { + let origin_url = lazy.NetUtil.newURI( + row.getResultByName("origin_url") + ); + // Ignore entries for non-http(s)/ftp URLs because we likely can't + // use them anyway. + const kValidSchemes = new Set(["https", "http", "ftp"]); + if (!kValidSchemes.has(origin_url.scheme)) { + continue; + } + let loginInfo = { + username: row.getResultByName("username_value"), + password: await crypto.decryptData( + row.getResultByName("password_value"), + null + ), + origin: origin_url.prePath, + formActionOrigin: null, + httpRealm: null, + usernameElement: row.getResultByName("username_element"), + passwordElement: row.getResultByName("password_element"), + timeCreated: lazy.ChromeMigrationUtils.chromeTimeToDate( + row.getResultByName("date_created") + 0, + fallbackCreationDate + ).getTime(), + timesUsed: row.getResultByName("times_used") + 0, + }; + + switch (row.getResultByName("scheme")) { + case AUTH_TYPE.SCHEME_HTML: + let action_url = row.getResultByName("action_url"); + if (!action_url) { + // If there is no action_url, store the wildcard "" value. + // See the `formActionOrigin` IDL comments. + loginInfo.formActionOrigin = ""; + break; + } + let action_uri = lazy.NetUtil.newURI(action_url); + if (!kValidSchemes.has(action_uri.scheme)) { + continue; // This continues the outer for loop. + } + loginInfo.formActionOrigin = action_uri.prePath; + break; + case AUTH_TYPE.SCHEME_BASIC: + case AUTH_TYPE.SCHEME_DIGEST: + // signon_realm format is URIrealm, so we need remove URI + loginInfo.httpRealm = row + .getResultByName("signon_realm") + .substring(loginInfo.origin.length + 1); + break; + default: + throw new Error( + "Login data scheme type not supported: " + + row.getResultByName("scheme") + ); + } + logins.push(loginInfo); + } catch (e) { + Cu.reportError(e); + } + } + try { + if (logins.length) { + await MigrationUtils.insertLoginsWrapper(logins); + } + } catch (e) { + Cu.reportError(e); + } + if (crypto.finalize) { + crypto.finalize(); + } + aCallback(true); + }, + }; + } +} async function GetBookmarksResource(aProfileFolder, aBrowserKey) { let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks"); @@ -458,328 +615,250 @@ async function GetCookiesResource(aProfileFolder) { }; } -ChromeProfileMigrator.prototype._GetPasswordsResource = async function( - aProfileFolder -) { - let loginPath = PathUtils.join(aProfileFolder, "Login Data"); - if (!(await IOUtils.exists(loginPath))) { - return null; +/** + * Chromium migrator + */ +export class ChromiumProfileMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chromium Profile Migrator"; } - let { - _chromeUserDataPathSuffix, - _keychainServiceName, - _keychainAccountName, - _keychainMockPassphrase = null, - } = this; + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chromium"; + } - return { - type: MigrationUtils.resourceTypes.PASSWORDS, + get classID() { + return Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}"); + } - async migrate(aCallback) { - let rows = await MigrationUtils.getRowsFromDBWithoutLocks( - loginPath, - "Chrome passwords", - `SELECT origin_url, action_url, username_element, username_value, - password_element, password_value, signon_realm, scheme, date_created, - times_used FROM logins WHERE blacklisted_by_user = 0` - ).catch(ex => { - Cu.reportError(ex); - aCallback(false); - }); - // If the promise was rejected we will have already called aCallback, - // so we can just return here. - if (!rows) { - return; - } - - // If there are no relevant rows, return before initializing crypto and - // thus prompting for Keychain access on macOS. - if (!rows.length) { - aCallback(true); - return; - } - - let crypto; - try { - if (AppConstants.platform == "win") { - let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule( - "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs" - ); - crypto = new ChromeWindowsLoginCrypto(_chromeUserDataPathSuffix); - } else if (AppConstants.platform == "macosx") { - let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule( - "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs" - ); - crypto = new ChromeMacOSLoginCrypto( - _keychainServiceName, - _keychainAccountName, - _keychainMockPassphrase - ); - } else { - aCallback(false); - return; - } - } catch (ex) { - // Handle the user canceling Keychain access or other OSCrypto errors. - Cu.reportError(ex); - aCallback(false); - return; - } - - let logins = []; - let fallbackCreationDate = new Date(); - for (let row of rows) { - try { - let origin_url = lazy.NetUtil.newURI( - row.getResultByName("origin_url") - ); - // Ignore entries for non-http(s)/ftp URLs because we likely can't - // use them anyway. - const kValidSchemes = new Set(["https", "http", "ftp"]); - if (!kValidSchemes.has(origin_url.scheme)) { - continue; - } - let loginInfo = { - username: row.getResultByName("username_value"), - password: await crypto.decryptData( - row.getResultByName("password_value"), - null - ), - origin: origin_url.prePath, - formActionOrigin: null, - httpRealm: null, - usernameElement: row.getResultByName("username_element"), - passwordElement: row.getResultByName("password_element"), - timeCreated: lazy.ChromeMigrationUtils.chromeTimeToDate( - row.getResultByName("date_created") + 0, - fallbackCreationDate - ).getTime(), - timesUsed: row.getResultByName("times_used") + 0, - }; - - switch (row.getResultByName("scheme")) { - case AUTH_TYPE.SCHEME_HTML: - let action_url = row.getResultByName("action_url"); - if (!action_url) { - // If there is no action_url, store the wildcard "" value. - // See the `formActionOrigin` IDL comments. - loginInfo.formActionOrigin = ""; - break; - } - let action_uri = lazy.NetUtil.newURI(action_url); - if (!kValidSchemes.has(action_uri.scheme)) { - continue; // This continues the outer for loop. - } - loginInfo.formActionOrigin = action_uri.prePath; - break; - case AUTH_TYPE.SCHEME_BASIC: - case AUTH_TYPE.SCHEME_DIGEST: - // signon_realm format is URIrealm, so we need remove URI - loginInfo.httpRealm = row - .getResultByName("signon_realm") - .substring(loginInfo.origin.length + 1); - break; - default: - throw new Error( - "Login data scheme type not supported: " + - row.getResultByName("scheme") - ); - } - logins.push(loginInfo); - } catch (e) { - Cu.reportError(e); - } - } - try { - if (logins.length) { - await MigrationUtils.insertLoginsWrapper(logins); - } - } catch (e) { - Cu.reportError(e); - } - if (crypto.finalize) { - crypto.finalize(); - } - aCallback(true); - }, - }; -}; - -ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator"; -ChromeProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chrome"; -ChromeProfileMigrator.prototype.classID = Components.ID( - "{4cec1de4-1671-4fc3-a53e-6c539dc77a26}" -); - -/** - * Chromium migration - */ -export function ChromiumProfileMigrator() { - this._chromeUserDataPathSuffix = "Chromium"; - this._keychainServiceName = "Chromium Safe Storage"; - this._keychainAccountName = "Chromium"; + _chromeUserDataPathSuffix = "Chromium"; + _keychainServiceName = "Chromium Safe Storage"; + _keychainAccountName = "Chromium"; } -ChromiumProfileMigrator.prototype = Object.create( - ChromeProfileMigrator.prototype -); -ChromiumProfileMigrator.prototype.classDescription = - "Chromium Profile Migrator"; -ChromiumProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chromium"; -ChromiumProfileMigrator.prototype.classID = Components.ID( - "{8cece922-9720-42de-b7db-7cef88cb07ca}" -); - /** * Chrome Canary * Not available on Linux */ -export function CanaryProfileMigrator() { - this._chromeUserDataPathSuffix = "Canary"; +export class CanaryProfileMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chrome Canary Profile Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=canary"; + } + + get classID() { + return Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}"); + } + + get _chromeUserDataPathSuffix() { + return "Canary"; + } + + get _keychainServiceName() { + return "Chromium Safe Storage"; + } + + get _keychainAccountName() { + return "Chromium"; + } } -CanaryProfileMigrator.prototype = Object.create( - ChromeProfileMigrator.prototype -); -CanaryProfileMigrator.prototype.classDescription = - "Chrome Canary Profile Migrator"; -CanaryProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=canary"; -CanaryProfileMigrator.prototype.classID = Components.ID( - "{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}" -); /** * Chrome Dev - Linux only (not available in Mac and Windows) */ -export function ChromeDevMigrator() { - this._chromeUserDataPathSuffix = "Chrome Dev"; -} -ChromeDevMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); -ChromeDevMigrator.prototype.classDescription = "Chrome Dev Profile Migrator"; -ChromeDevMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev"; -ChromeDevMigrator.prototype.classID = Components.ID( - "{7370a02a-4886-42c3-a4ec-d48c726ec30a}" -); +export class ChromeDevMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chrome Dev Profile Migrator"; + } -export function ChromeBetaMigrator() { - this._chromeUserDataPathSuffix = "Chrome Beta"; -} -ChromeBetaMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); -ChromeBetaMigrator.prototype.classDescription = "Chrome Beta Profile Migrator"; -ChromeBetaMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta"; -ChromeBetaMigrator.prototype.classID = Components.ID( - "{47f75963-840b-4950-a1f0-d9c1864f8b8e}" -); + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev"; + } -export function BraveProfileMigrator() { - this._chromeUserDataPathSuffix = "Brave"; - this._keychainServiceName = "Brave Browser Safe Storage"; - this._keychainAccountName = "Brave Browser"; + get classID() { + return Components.ID("{7370a02a-4886-42c3-a4ec-d48c726ec30a}"); + } + + _chromeUserDataPathSuffix = "Chrome Dev"; + _keychainServiceName = "Chromium Safe Storage"; + _keychainAccountName = "Chromium"; } -BraveProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); -BraveProfileMigrator.prototype.classDescription = "Brave Browser Migrator"; -BraveProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=brave"; -BraveProfileMigrator.prototype.classID = Components.ID( - "{4071880a-69e4-4c83-88b4-6c589a62801d}" -); +/** + * Chrome Beta migrator + */ +export class ChromeBetaMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chrome Beta Profile Migrator"; + } -export function ChromiumEdgeMigrator() { - this._chromeUserDataPathSuffix = "Edge"; - this._keychainServiceName = "Microsoft Edge Safe Storage"; - this._keychainAccountName = "Microsoft Edge"; -} -ChromiumEdgeMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); -ChromiumEdgeMigrator.prototype.classDescription = - "Chromium Edge Profile Migrator"; -ChromiumEdgeMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chromium-edge"; -ChromiumEdgeMigrator.prototype.classID = Components.ID( - "{3c7f6b7c-baa9-4338-acfa-04bf79f1dcf1}" -); + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta"; + } -export function ChromiumEdgeBetaMigrator() { - this._chromeUserDataPathSuffix = "Edge Beta"; - this._keychainServiceName = "Microsoft Edge Safe Storage"; - this._keychainAccountName = "Microsoft Edge"; -} -ChromiumEdgeBetaMigrator.prototype = Object.create( - ChromiumEdgeMigrator.prototype -); -ChromiumEdgeBetaMigrator.prototype.classDescription = - "Chromium Edge Beta Profile Migrator"; -ChromiumEdgeBetaMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chromium-edge-beta"; -ChromiumEdgeBetaMigrator.prototype.classID = Components.ID( - "{0fc3d48a-c1c3-4871-b58f-a8b47d1555fb}" -); + get classID() { + return Components.ID("{47f75963-840b-4950-a1f0-d9c1864f8b8e}"); + } -export function Chromium360seMigrator() { - this._chromeUserDataPathSuffix = "360 SE"; -} -Chromium360seMigrator.prototype = Object.create( - ChromeProfileMigrator.prototype -); -Chromium360seMigrator.prototype.classDescription = - "Chromium 360 Secure Browser Profile Migrator"; -Chromium360seMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=chromium-360se"; -Chromium360seMigrator.prototype.classID = Components.ID( - "{2e1a182e-ce4f-4dc9-a22c-d4125b931552}" -); - -export function OperaProfileMigrator() { - this._chromeUserDataPathSuffix = "Opera"; - this._keychainServiceName = "Opera Browser Safe Storage"; - this._keychainAccountName = "Opera Browser"; -} -OperaProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); -OperaProfileMigrator.prototype.classDescription = "Opera Browser Migrator"; -OperaProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=opera"; -OperaProfileMigrator.prototype.classID = Components.ID( - "{16c5d501-e411-41eb-93f2-af6c9ba64dee}" -); -OperaProfileMigrator.prototype.getSourceProfiles = function() { - return null; -}; - -export function OperaGXProfileMigrator() { - this._chromeUserDataPathSuffix = "Opera GX"; - this._keychainServiceName = "Opera Browser Safe Storage"; - this._keychainAccountName = "Opera Browser"; -} -OperaGXProfileMigrator.prototype = Object.create( - ChromeProfileMigrator.prototype -); -OperaGXProfileMigrator.prototype.classDescription = "Opera GX Browser Migrator"; -OperaGXProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=opera-gx"; -OperaGXProfileMigrator.prototype.classID = Components.ID( - "{26F4E0A0-B533-4FDA-B344-6FF5DA45D6DC}" -); -OperaGXProfileMigrator.prototype.getSourceProfiles = function() { - return null; -}; - -export function VivaldiProfileMigrator() { - this._chromeUserDataPathSuffix = "Vivaldi"; - this._keychainServiceName = "Vivaldi Safe Storage"; - this._keychainAccountName = "Vivaldi"; + _chromeUserDataPathSuffix = "Chrome Beta"; + _keychainServiceName = "Chromium Safe Storage"; + _keychainAccountName = "Chromium"; } -VivaldiProfileMigrator.prototype = Object.create( - ChromeProfileMigrator.prototype -); -VivaldiProfileMigrator.prototype.classDescription = "Vivaldi Migrator"; -VivaldiProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=vivaldi"; -VivaldiProfileMigrator.prototype.classID = Components.ID( - "{54a6a025-e70d-49dd-ba95-0f7e45d728d3}" -); +/** + * Brave migrator + */ +export class BraveProfileMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Brave Browser Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=brave"; + } + + get classID() { + return Components.ID("{4071880a-69e4-4c83-88b4-6c589a62801d}"); + } + + _chromeUserDataPathSuffix = "Brave"; + _keychainServiceName = "Brave Browser Safe Storage"; + _keychainAccountName = "Brave Browser"; +} + +/** + * Edge (Chromium-based) migrator + */ +export class ChromiumEdgeMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chromium Edge Profile Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chromium-edge"; + } + + get classID() { + return Components.ID("{3c7f6b7c-baa9-4338-acfa-04bf79f1dcf1}"); + } + + _chromeUserDataPathSuffix = "Edge"; + _keychainServiceName = "Microsoft Edge Safe Storage"; + _keychainAccountName = "Microsoft Edge"; +} + +/** + * Edge Beta (Chromium-based) migrator + */ +export class ChromiumEdgeBetaMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chromium Edge Beta Profile Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chromium-edge-beta"; + } + + get classID() { + return Components.ID("{0fc3d48a-c1c3-4871-b58f-a8b47d1555fb}"); + } + + _chromeUserDataPathSuffix = "Edge Beta"; + _keychainServiceName = "Microsoft Edge Safe Storage"; + _keychainAccountName = "Microsoft Edge"; +} + +/** + * Chromium 360 migrator + */ +export class Chromium360seMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Chromium 360 Secure Browser Profile Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=chromium-360se"; + } + + get classID() { + return Components.ID("{2e1a182e-ce4f-4dc9-a22c-d4125b931552}"); + } + + _chromeUserDataPathSuffix = "360 SE"; + _keychainServiceName = "Microsoft Edge Safe Storage"; + _keychainAccountName = "Microsoft Edge"; +} + +/** + * Opera migrator + */ +export class OperaProfileMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Opera Browser Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=opera"; + } + + get classID() { + return Components.ID("{16c5d501-e411-41eb-93f2-af6c9ba64dee}"); + } + + _chromeUserDataPathSuffix = "Opera"; + _keychainServiceName = "Opera Browser Safe Storage"; + _keychainAccountName = "Opera Browser"; + + getSourceProfiles() { + return null; + } +} + +/** + * Opera GX migrator + */ +export class OperaGXProfileMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Opera GX Browser Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=opera-gx"; + } + + get classID() { + return Components.ID("{26F4E0A0-B533-4FDA-B344-6FF5DA45D6DC}"); + } + + _chromeUserDataPathSuffix = "Opera GX"; + _keychainServiceName = "Opera Browser Safe Storage"; + _keychainAccountName = "Opera Browser"; + + getSourceProfiles() { + return null; + } +} + +/** + * Vivaldi migrator + */ +export class VivaldiProfileMigrator extends ChromeProfileMigrator { + get classDescription() { + return "Vivaldi Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=vivaldi"; + } + + get classID() { + return Components.ID("{54a6a025-e70d-49dd-ba95-0f7e45d728d3}"); + } + + _chromeUserDataPathSuffix = "Vivaldi"; + _keychainServiceName = "Vivaldi Safe Storage"; + _keychainAccountName = "Vivaldi"; +} diff --git a/browser/components/migration/EdgeProfileMigrator.sys.mjs b/browser/components/migration/EdgeProfileMigrator.sys.mjs index 2657b13ed611..25839d83b020 100644 --- a/browser/components/migration/EdgeProfileMigrator.sys.mjs +++ b/browser/components/migration/EdgeProfileMigrator.sys.mjs @@ -6,10 +6,8 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; -import { - MigrationUtils, - MigratorPrototype, -} from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs"; const lazy = {}; @@ -466,89 +464,97 @@ EdgeBookmarksMigrator.prototype = { }, }; -export function EdgeProfileMigrator() { - this.wrappedJSObject = this; -} - -EdgeProfileMigrator.prototype = Object.create(MigratorPrototype); - -EdgeProfileMigrator.prototype.getBookmarksMigratorForTesting = function( - dbOverride -) { - return new EdgeBookmarksMigrator(dbOverride); -}; - -EdgeProfileMigrator.prototype.getReadingListMigratorForTesting = function( - dbOverride -) { - return new EdgeReadingListMigrator(dbOverride); -}; - -EdgeProfileMigrator.prototype.getResources = function() { - let resources = [ - new EdgeBookmarksMigrator(), - MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE), - new EdgeTypedURLMigrator(), - new EdgeTypedURLDBMigrator(), - new EdgeReadingListMigrator(), - ]; - let windowsVaultFormPasswordsMigrator = MSMigrationUtils.getWindowsVaultFormPasswordsMigrator(); - windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords"; - resources.push(windowsVaultFormPasswordsMigrator); - return resources.filter(r => r.exists); -}; - -EdgeProfileMigrator.prototype.getLastUsedDate = async function() { - // Don't do this if we don't have a single profile (see the comment for - // sourceProfiles) or if we can't find the database file: - let sourceProfiles = await this.getSourceProfiles(); - if (sourceProfiles !== null || !lazy.gEdgeDatabase) { - return Promise.resolve(new Date(0)); - } - let logFilePath = PathUtils.join( - lazy.gEdgeDatabase.parent.path, - "LogFiles", - "edb.log" - ); - let dbPath = lazy.gEdgeDatabase.path; - let cookieMigrator = MSMigrationUtils.getCookiesMigrator( - MSMigrationUtils.MIGRATION_TYPE_EDGE - ); - let cookiePaths = cookieMigrator._cookiesFolders.map(f => f.path); - let datePromises = [logFilePath, dbPath, ...cookiePaths].map(path => { - return IOUtils.stat(path) - .then(info => info.lastModified) - .catch(() => 0); - }); - datePromises.push( - new Promise(resolve => { - let typedURLs = new Map(); - try { - typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot); - } catch (ex) {} - let times = [0, ...typedURLs.values()]; - // dates is an array of PRTimes, which are in microseconds - convert to milliseconds - resolve(Math.max.apply(Math, times) / 1000); - }) - ); - return Promise.all(datePromises).then(dates => { - return new Date(Math.max.apply(Math, dates)); - }); -}; - -/* Somewhat counterintuitively, this returns: - * - |null| to indicate "There is only 1 (default) profile" (on win10+) - * - |[]| to indicate "There are no profiles" (on <=win8.1) which will avoid using this migrator. - * See MigrationUtils.jsm for slightly more info on how sourceProfiles is used. +/** + * Edge (EdgeHTML) profile migrator */ -EdgeProfileMigrator.prototype.getSourceProfiles = function() { - let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10"); - return isWin10OrHigher ? null : []; -}; +export class EdgeProfileMigrator extends MigratorBase { + constructor() { + super(); + this.wrappedJSObject = this; + } -EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator"; -EdgeProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=edge"; -EdgeProfileMigrator.prototype.classID = Components.ID( - "{62e8834b-2d17-49f5-96ff-56344903a2ae}" -); + get classDescription() { + return "Edge Profile Migrator"; + } + + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=edge"; + } + + get classID() { + return Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}"); + } + + getBookmarksMigratorForTesting(dbOverride) { + return new EdgeBookmarksMigrator(dbOverride); + } + + getReadingListMigratorForTesting(dbOverride) { + return new EdgeReadingListMigrator(dbOverride); + } + + getResources() { + let resources = [ + new EdgeBookmarksMigrator(), + MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE), + new EdgeTypedURLMigrator(), + new EdgeTypedURLDBMigrator(), + new EdgeReadingListMigrator(), + ]; + let windowsVaultFormPasswordsMigrator = MSMigrationUtils.getWindowsVaultFormPasswordsMigrator(); + windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords"; + resources.push(windowsVaultFormPasswordsMigrator); + return resources.filter(r => r.exists); + } + + async getLastUsedDate() { + // Don't do this if we don't have a single profile (see the comment for + // sourceProfiles) or if we can't find the database file: + let sourceProfiles = await this.getSourceProfiles(); + if (sourceProfiles !== null || !lazy.gEdgeDatabase) { + return Promise.resolve(new Date(0)); + } + let logFilePath = PathUtils.join( + lazy.gEdgeDatabase.parent.path, + "LogFiles", + "edb.log" + ); + let dbPath = lazy.gEdgeDatabase.path; + let cookieMigrator = MSMigrationUtils.getCookiesMigrator( + MSMigrationUtils.MIGRATION_TYPE_EDGE + ); + let cookiePaths = cookieMigrator._cookiesFolders.map(f => f.path); + let datePromises = [logFilePath, dbPath, ...cookiePaths].map(path => { + return IOUtils.stat(path) + .then(info => info.lastModified) + .catch(() => 0); + }); + datePromises.push( + new Promise(resolve => { + let typedURLs = new Map(); + try { + typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot); + } catch (ex) {} + let times = [0, ...typedURLs.values()]; + // dates is an array of PRTimes, which are in microseconds - convert to milliseconds + resolve(Math.max.apply(Math, times) / 1000); + }) + ); + return Promise.all(datePromises).then(dates => { + return new Date(Math.max.apply(Math, dates)); + }); + } + + /** + * @returns {Array|null} + * Somewhat counterintuitively, this returns: + * - |null| to indicate "There is only 1 (default) profile" (on win10+) + * - |[]| to indicate "There are no profiles" (on <=win8.1) which will avoid + * using this migrator. + * See MigrationUtils.sys.mjs for slightly more info on how sourceProfiles is used. + */ + getSourceProfiles() { + let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10"); + return isWin10OrHigher ? null : []; + } +} diff --git a/browser/components/migration/FirefoxProfileMigrator.sys.mjs b/browser/components/migration/FirefoxProfileMigrator.sys.mjs index 69b94dd51e10..1044e178f4fb 100644 --- a/browser/components/migration/FirefoxProfileMigrator.sys.mjs +++ b/browser/components/migration/FirefoxProfileMigrator.sys.mjs @@ -11,10 +11,9 @@ * from the source profile. */ -import { - MigrationUtils, - MigratorPrototype, -} from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; + +import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; const lazy = {}; @@ -25,341 +24,352 @@ ChromeUtils.defineESModuleGetters(lazy, { SessionMigration: "resource:///modules/sessionstore/SessionMigration.sys.mjs", }); -export function FirefoxProfileMigrator() { - this.wrappedJSObject = this; // for testing... -} - -FirefoxProfileMigrator.prototype = Object.create(MigratorPrototype); - -FirefoxProfileMigrator.prototype._getAllProfiles = function() { - let allProfiles = new Map(); - let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( - Ci.nsIToolkitProfileService - ); - for (let profile of profileService.profiles) { - let rootDir = profile.rootDir; - - if ( - rootDir.exists() && - rootDir.isReadable() && - !rootDir.equals(MigrationUtils.profileStartup.directory) - ) { - allProfiles.set(profile.name, rootDir); - } - } - return allProfiles; -}; - -function sorter(a, b) { - return a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase()); -} - -FirefoxProfileMigrator.prototype.getSourceProfiles = function() { - return [...this._getAllProfiles().keys()] - .map(x => ({ id: x, name: x })) - .sort(sorter); -}; - -FirefoxProfileMigrator.prototype._getFileObject = function(dir, fileName) { - let file = dir.clone(); - file.append(fileName); - - // File resources are monolithic. We don't make partial copies since - // they are not expected to work alone. Return null to avoid trying to - // copy non-existing files. - return file.exists() ? file : null; -}; - -FirefoxProfileMigrator.prototype.getResources = function(aProfile) { - let sourceProfileDir = aProfile - ? this._getAllProfiles().get(aProfile.id) - : Cc["@mozilla.org/toolkit/profile-service;1"].getService( - Ci.nsIToolkitProfileService - ).defaultProfile.rootDir; - if ( - !sourceProfileDir || - !sourceProfileDir.exists() || - !sourceProfileDir.isReadable() - ) { - return null; +/** + * Firefox profile migrator. Currently, this class only does "pave over" + * migrations, where various parts of an old profile overwrite a new + * profile. This is distinct from other migrators which attempt to import + * old profile data into the existing profile. + * + * This migrator is what powers the "Profile Refresh" mechanism. + */ +export class FirefoxProfileMigrator extends MigratorBase { + constructor() { + super(); + this.wrappedJSObject = this; // for testing... } - // Being a startup-only migrator, we can rely on - // MigrationUtils.profileStartup being set. - let currentProfileDir = MigrationUtils.profileStartup.directory; - - // Surely data cannot be imported from the current profile. - if (sourceProfileDir.equals(currentProfileDir)) { - return null; + get classDescription() { + return "Firefox Profile Migrator"; } - return this._getResourcesInternal(sourceProfileDir, currentProfileDir); -}; + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=firefox"; + } -FirefoxProfileMigrator.prototype.getLastUsedDate = function() { - // We always pretend we're really old, so that we don't mess - // up the determination of which browser is the most 'recent' - // to import from. - return Promise.resolve(new Date(0)); -}; + get classID() { + return Components.ID("{91185366-ba97-4438-acba-48deaca63386}"); + } -FirefoxProfileMigrator.prototype._getResourcesInternal = function( - sourceProfileDir, - currentProfileDir -) { - let getFileResource = (aMigrationType, aFileNames) => { - let files = []; - for (let fileName of aFileNames) { - let file = this._getFileObject(sourceProfileDir, fileName); - if (file) { - files.push(file); + _getAllProfiles() { + let allProfiles = new Map(); + let profileService = Cc[ + "@mozilla.org/toolkit/profile-service;1" + ].getService(Ci.nsIToolkitProfileService); + for (let profile of profileService.profiles) { + let rootDir = profile.rootDir; + + if ( + rootDir.exists() && + rootDir.isReadable() && + !rootDir.equals(MigrationUtils.profileStartup.directory) + ) { + allProfiles.set(profile.name, rootDir); } } - if (!files.length) { + return allProfiles; + } + + getSourceProfiles() { + let sorter = (a, b) => { + return a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase()); + }; + + return [...this._getAllProfiles().keys()] + .map(x => ({ id: x, name: x })) + .sort(sorter); + } + + _getFileObject(dir, fileName) { + let file = dir.clone(); + file.append(fileName); + + // File resources are monolithic. We don't make partial copies since + // they are not expected to work alone. Return null to avoid trying to + // copy non-existing files. + return file.exists() ? file : null; + } + + getResources(aProfile) { + let sourceProfileDir = aProfile + ? this._getAllProfiles().get(aProfile.id) + : Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ).defaultProfile.rootDir; + if ( + !sourceProfileDir || + !sourceProfileDir.exists() || + !sourceProfileDir.isReadable() + ) { return null; } - return { - type: aMigrationType, - migrate(aCallback) { - for (let file of files) { - file.copyTo(currentProfileDir, ""); + + // Being a startup-only migrator, we can rely on + // MigrationUtils.profileStartup being set. + let currentProfileDir = MigrationUtils.profileStartup.directory; + + // Surely data cannot be imported from the current profile. + if (sourceProfileDir.equals(currentProfileDir)) { + return null; + } + + return this._getResourcesInternal(sourceProfileDir, currentProfileDir); + } + + getLastUsedDate() { + // We always pretend we're really old, so that we don't mess + // up the determination of which browser is the most 'recent' + // to import from. + return Promise.resolve(new Date(0)); + } + + _getResourcesInternal(sourceProfileDir, currentProfileDir) { + let getFileResource = (aMigrationType, aFileNames) => { + let files = []; + for (let fileName of aFileNames) { + let file = this._getFileObject(sourceProfileDir, fileName); + if (file) { + files.push(file); + } + } + if (!files.length) { + return null; + } + return { + type: aMigrationType, + migrate(aCallback) { + for (let file of files) { + file.copyTo(currentProfileDir, ""); + } + aCallback(true); + }, + }; + }; + + function savePrefs() { + // If we've used the pref service to write prefs for the new profile, it's too + // early in startup for the service to have a profile directory, so we have to + // manually tell it where to save the prefs file. + let newPrefsFile = currentProfileDir.clone(); + newPrefsFile.append("prefs.js"); + Services.prefs.savePrefFile(newPrefsFile); + } + + let types = MigrationUtils.resourceTypes; + let places = getFileResource(types.HISTORY, [ + "places.sqlite", + "places.sqlite-wal", + ]); + let favicons = getFileResource(types.HISTORY, [ + "favicons.sqlite", + "favicons.sqlite-wal", + ]); + let cookies = getFileResource(types.COOKIES, [ + "cookies.sqlite", + "cookies.sqlite-wal", + ]); + let passwords = getFileResource(types.PASSWORDS, [ + "signons.sqlite", + "logins.json", + "key3.db", + "key4.db", + ]); + let formData = getFileResource(types.FORMDATA, [ + "formhistory.sqlite", + "autofill-profiles.json", + ]); + let bookmarksBackups = getFileResource(types.OTHERDATA, [ + lazy.PlacesBackups.profileRelativeFolderPath, + ]); + let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]); + + let session; + if (Services.env.get("MOZ_RESET_PROFILE_MIGRATE_SESSION")) { + // We only want to restore the previous firefox session if the profile refresh was + // triggered by user. The MOZ_RESET_PROFILE_MIGRATE_SESSION would be set when a user-triggered + // profile refresh happened in nsAppRunner.cpp. Hence, we detect the MOZ_RESET_PROFILE_MIGRATE_SESSION + // to see if session data migration is required. + Services.env.set("MOZ_RESET_PROFILE_MIGRATE_SESSION", ""); + let sessionCheckpoints = this._getFileObject( + sourceProfileDir, + "sessionCheckpoints.json" + ); + let sessionFile = this._getFileObject( + sourceProfileDir, + "sessionstore.jsonlz4" + ); + if (sessionFile) { + session = { + type: types.SESSION, + migrate(aCallback) { + sessionCheckpoints.copyTo( + currentProfileDir, + "sessionCheckpoints.json" + ); + let newSessionFile = currentProfileDir.clone(); + newSessionFile.append("sessionstore.jsonlz4"); + let migrationPromise = lazy.SessionMigration.migrate( + sessionFile.path, + newSessionFile.path + ); + migrationPromise.then( + function() { + let buildID = Services.appinfo.platformBuildID; + let mstone = Services.appinfo.platformVersion; + // Force the browser to one-off resume the session that we give it: + Services.prefs.setBoolPref( + "browser.sessionstore.resume_session_once", + true + ); + // Reset the homepage_override prefs so that the browser doesn't override our + // session with the "what's new" page: + Services.prefs.setCharPref( + "browser.startup.homepage_override.mstone", + mstone + ); + Services.prefs.setCharPref( + "browser.startup.homepage_override.buildID", + buildID + ); + savePrefs(); + aCallback(true); + }, + function() { + aCallback(false); + } + ); + }, + }; + } + } + + // Sync/FxA related data + let sync = { + name: "sync", // name is used only by tests. + type: types.OTHERDATA, + migrate: async aCallback => { + // Try and parse a signedInUser.json file from the source directory and + // if we can, copy it to the new profile and set sync's username pref + // (which acts as a de-facto flag to indicate if sync is configured) + try { + let oldPath = PathUtils.join( + sourceProfileDir.path, + "signedInUser.json" + ); + let exists = await IOUtils.exists(oldPath); + if (exists) { + let data = await IOUtils.readJSON(oldPath); + if (data && data.accountData && data.accountData.email) { + let username = data.accountData.email; + // copy the file itself. + await IOUtils.copy( + oldPath, + PathUtils.join(currentProfileDir.path, "signedInUser.json") + ); + // Now we need to know whether Sync is actually configured for this + // user. The only way we know is by looking at the prefs file from + // the old profile. We avoid trying to do a full parse of the prefs + // file and even avoid parsing the single string value we care + // about. + let prefsPath = PathUtils.join(sourceProfileDir.path, "prefs.js"); + if (await IOUtils.exists(oldPath)) { + let rawPrefs = await IOUtils.readUTF8(prefsPath, { + encoding: "utf-8", + }); + if (/^user_pref\("services\.sync\.username"/m.test(rawPrefs)) { + // sync's configured in the source profile - ensure it is in the + // new profile too. + // Write it to prefs.js and flush the file. + Services.prefs.setStringPref( + "services.sync.username", + username + ); + savePrefs(); + } + } + } + } + } catch (ex) { + aCallback(false); + return; } aCallback(true); }, }; - }; - function savePrefs() { - // If we've used the pref service to write prefs for the new profile, it's too - // early in startup for the service to have a profile directory, so we have to - // manually tell it where to save the prefs file. - let newPrefsFile = currentProfileDir.clone(); - newPrefsFile.append("prefs.js"); - Services.prefs.savePrefFile(newPrefsFile); - } + // Telemetry related migrations. + let times = { + name: "times", // name is used only by tests. + type: types.OTHERDATA, + migrate: aCallback => { + let file = this._getFileObject(sourceProfileDir, "times.json"); + if (file) { + file.copyTo(currentProfileDir, ""); + } + // And record the fact a migration (ie, a reset) happened. + let recordMigration = async () => { + try { + let profileTimes = await lazy.ProfileAge(currentProfileDir.path); + await profileTimes.recordProfileReset(); + aCallback(true); + } catch (e) { + aCallback(false); + } + }; - let types = MigrationUtils.resourceTypes; - let places = getFileResource(types.HISTORY, [ - "places.sqlite", - "places.sqlite-wal", - ]); - let favicons = getFileResource(types.HISTORY, [ - "favicons.sqlite", - "favicons.sqlite-wal", - ]); - let cookies = getFileResource(types.COOKIES, [ - "cookies.sqlite", - "cookies.sqlite-wal", - ]); - let passwords = getFileResource(types.PASSWORDS, [ - "signons.sqlite", - "logins.json", - "key3.db", - "key4.db", - ]); - let formData = getFileResource(types.FORMDATA, [ - "formhistory.sqlite", - "autofill-profiles.json", - ]); - let bookmarksBackups = getFileResource(types.OTHERDATA, [ - lazy.PlacesBackups.profileRelativeFolderPath, - ]); - let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]); + recordMigration(); + }, + }; + let telemetry = { + name: "telemetry", // name is used only by tests... + type: types.OTHERDATA, + migrate: aCallback => { + let createSubDir = name => { + let dir = currentProfileDir.clone(); + dir.append(name); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, lazy.FileUtils.PERMS_DIRECTORY); + return dir; + }; - let session; - if (Services.env.get("MOZ_RESET_PROFILE_MIGRATE_SESSION")) { - // We only want to restore the previous firefox session if the profile refresh was - // triggered by user. The MOZ_RESET_PROFILE_MIGRATE_SESSION would be set when a user-triggered - // profile refresh happened in nsAppRunner.cpp. Hence, we detect the MOZ_RESET_PROFILE_MIGRATE_SESSION - // to see if session data migration is required. - Services.env.set("MOZ_RESET_PROFILE_MIGRATE_SESSION", ""); - let sessionCheckpoints = this._getFileObject( - sourceProfileDir, - "sessionCheckpoints.json" - ); - let sessionFile = this._getFileObject( - sourceProfileDir, - "sessionstore.jsonlz4" - ); - if (sessionFile) { - session = { - type: types.SESSION, - migrate(aCallback) { - sessionCheckpoints.copyTo( - currentProfileDir, - "sessionCheckpoints.json" - ); - let newSessionFile = currentProfileDir.clone(); - newSessionFile.append("sessionstore.jsonlz4"); - let migrationPromise = lazy.SessionMigration.migrate( - sessionFile.path, - newSessionFile.path - ); - migrationPromise.then( - function() { - let buildID = Services.appinfo.platformBuildID; - let mstone = Services.appinfo.platformVersion; - // Force the browser to one-off resume the session that we give it: - Services.prefs.setBoolPref( - "browser.sessionstore.resume_session_once", - true - ); - // Reset the homepage_override prefs so that the browser doesn't override our - // session with the "what's new" page: - Services.prefs.setCharPref( - "browser.startup.homepage_override.mstone", - mstone - ); - Services.prefs.setCharPref( - "browser.startup.homepage_override.buildID", - buildID - ); - savePrefs(); - aCallback(true); - }, - function() { - aCallback(false); - } - ); - }, - }; - } - } - - // Sync/FxA related data - let sync = { - name: "sync", // name is used only by tests. - type: types.OTHERDATA, - migrate: async aCallback => { - // Try and parse a signedInUser.json file from the source directory and - // if we can, copy it to the new profile and set sync's username pref - // (which acts as a de-facto flag to indicate if sync is configured) - try { - let oldPath = PathUtils.join( - sourceProfileDir.path, - "signedInUser.json" + // If the 'datareporting' directory exists we migrate files from it. + let dataReportingDir = this._getFileObject( + sourceProfileDir, + "datareporting" ); - let exists = await IOUtils.exists(oldPath); - if (exists) { - let data = await IOUtils.readJSON(oldPath); - if (data && data.accountData && data.accountData.email) { - let username = data.accountData.email; - // copy the file itself. - await IOUtils.copy( - oldPath, - PathUtils.join(currentProfileDir.path, "signedInUser.json") - ); - // Now we need to know whether Sync is actually configured for this - // user. The only way we know is by looking at the prefs file from - // the old profile. We avoid trying to do a full parse of the prefs - // file and even avoid parsing the single string value we care - // about. - let prefsPath = PathUtils.join(sourceProfileDir.path, "prefs.js"); - if (await IOUtils.exists(oldPath)) { - let rawPrefs = await IOUtils.readUTF8(prefsPath, { - encoding: "utf-8", - }); - if (/^user_pref\("services\.sync\.username"/m.test(rawPrefs)) { - // sync's configured in the source profile - ensure it is in the - // new profile too. - // Write it to prefs.js and flush the file. - Services.prefs.setStringPref( - "services.sync.username", - username - ); - savePrefs(); - } + if (dataReportingDir && dataReportingDir.isDirectory()) { + // Copy only specific files. + let toCopy = ["state.json", "session-state.json"]; + + let dest = createSubDir("datareporting"); + let enumerator = dataReportingDir.directoryEntries; + while (enumerator.hasMoreElements()) { + let file = enumerator.nextFile; + if (file.isDirectory() || !toCopy.includes(file.leafName)) { + continue; } + file.copyTo(dest, ""); } } - } catch (ex) { - aCallback(false); - return; - } - aCallback(true); - }, - }; - // Telemetry related migrations. - let times = { - name: "times", // name is used only by tests. - type: types.OTHERDATA, - migrate: aCallback => { - let file = this._getFileObject(sourceProfileDir, "times.json"); - if (file) { - file.copyTo(currentProfileDir, ""); - } - // And record the fact a migration (ie, a reset) happened. - let recordMigration = async () => { - try { - let profileTimes = await lazy.ProfileAge(currentProfileDir.path); - await profileTimes.recordProfileReset(); - aCallback(true); - } catch (e) { - aCallback(false); - } - }; + aCallback(true); + }, + }; - recordMigration(); - }, - }; - let telemetry = { - name: "telemetry", // name is used only by tests... - type: types.OTHERDATA, - migrate: aCallback => { - let createSubDir = name => { - let dir = currentProfileDir.clone(); - dir.append(name); - dir.create(Ci.nsIFile.DIRECTORY_TYPE, lazy.FileUtils.PERMS_DIRECTORY); - return dir; - }; + return [ + places, + cookies, + passwords, + formData, + dictionary, + bookmarksBackups, + session, + sync, + times, + telemetry, + favicons, + ].filter(r => r); + } - // If the 'datareporting' directory exists we migrate files from it. - let dataReportingDir = this._getFileObject( - sourceProfileDir, - "datareporting" - ); - if (dataReportingDir && dataReportingDir.isDirectory()) { - // Copy only specific files. - let toCopy = ["state.json", "session-state.json"]; - - let dest = createSubDir("datareporting"); - let enumerator = dataReportingDir.directoryEntries; - while (enumerator.hasMoreElements()) { - let file = enumerator.nextFile; - if (file.isDirectory() || !toCopy.includes(file.leafName)) { - continue; - } - file.copyTo(dest, ""); - } - } - - aCallback(true); - }, - }; - - return [ - places, - cookies, - passwords, - formData, - dictionary, - bookmarksBackups, - session, - sync, - times, - telemetry, - favicons, - ].filter(r => r); -}; - -Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", { - get: () => true, -}); - -FirefoxProfileMigrator.prototype.classDescription = "Firefox Profile Migrator"; -FirefoxProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=firefox"; -FirefoxProfileMigrator.prototype.classID = Components.ID( - "{91185366-ba97-4438-acba-48deaca63386}" -); + get startupOnlyMigrator() { + return true; + } +} diff --git a/browser/components/migration/IEProfileMigrator.sys.mjs b/browser/components/migration/IEProfileMigrator.sys.mjs index 3921713ced8b..e1b1b884e4b1 100644 --- a/browser/components/migration/IEProfileMigrator.sys.mjs +++ b/browser/components/migration/IEProfileMigrator.sys.mjs @@ -8,10 +8,8 @@ const kLoginsKey = import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); -import { - MigrationUtils, - MigratorPrototype, -} from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs"; const lazy = {}; @@ -351,58 +349,67 @@ IE7FormPasswords.prototype = { }, }; -export function IEProfileMigrator() { - this.wrappedJSObject = this; // export this to be able to use it in the unittest. -} - -IEProfileMigrator.prototype = Object.create(MigratorPrototype); - -IEProfileMigrator.prototype.getResources = function IE_getResources() { - let resources = [ - MSMigrationUtils.getBookmarksMigrator(), - new History(), - MSMigrationUtils.getCookiesMigrator(), - ]; - // Only support the form password migrator for Windows XP to 7. - if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) { - resources.push(new IE7FormPasswords()); +/** + * Internet Explorer profile migrator + */ +export class IEProfileMigrator extends MigratorBase { + constructor() { + super(); + this.wrappedJSObject = this; // export this to be able to use it in the unittest. } - let windowsVaultFormPasswordsMigrator = MSMigrationUtils.getWindowsVaultFormPasswordsMigrator(); - windowsVaultFormPasswordsMigrator.name = "IEVaultFormPasswords"; - resources.push(windowsVaultFormPasswordsMigrator); - return resources.filter(r => r.exists); -}; -IEProfileMigrator.prototype.getLastUsedDate = function IE_getLastUsedDate() { - let datePromises = ["Favs", "CookD"].map(dirId => { - let { path } = Services.dirsvc.get(dirId, Ci.nsIFile); - return OS.File.stat(path) - .catch(() => null) - .then(info => { - return info ? info.lastModificationDate : 0; - }); - }); - datePromises.push( - new Promise(resolve => { - let typedURLs = new Map(); - try { - typedURLs = MSMigrationUtils.getTypedURLs( - "Software\\Microsoft\\Internet Explorer" - ); - } catch (ex) {} - let dates = [0, ...typedURLs.values()]; - // dates is an array of PRTimes, which are in microseconds - convert to milliseconds - resolve(Math.max.apply(Math, dates) / 1000); - }) - ); - return Promise.all(datePromises).then(dates => { - return new Date(Math.max.apply(Math, dates)); - }); -}; + get classDescription() { + return "IE Profile Migrator"; + } -IEProfileMigrator.prototype.classDescription = "IE Profile Migrator"; -IEProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=ie"; -IEProfileMigrator.prototype.classID = Components.ID( - "{3d2532e3-4932-4774-b7ba-968f5899d3a4}" -); + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=ie"; + } + + get classID() { + return Components.ID("{3d2532e3-4932-4774-b7ba-968f5899d3a4}"); + } + + getResources() { + let resources = [ + MSMigrationUtils.getBookmarksMigrator(), + new History(), + MSMigrationUtils.getCookiesMigrator(), + ]; + // Only support the form password migrator for Windows XP to 7. + if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) { + resources.push(new IE7FormPasswords()); + } + let windowsVaultFormPasswordsMigrator = MSMigrationUtils.getWindowsVaultFormPasswordsMigrator(); + windowsVaultFormPasswordsMigrator.name = "IEVaultFormPasswords"; + resources.push(windowsVaultFormPasswordsMigrator); + return resources.filter(r => r.exists); + } + + getLastUsedDate() { + let datePromises = ["Favs", "CookD"].map(dirId => { + let { path } = Services.dirsvc.get(dirId, Ci.nsIFile); + return OS.File.stat(path) + .catch(() => null) + .then(info => { + return info ? info.lastModificationDate : 0; + }); + }); + datePromises.push( + new Promise(resolve => { + let typedURLs = new Map(); + try { + typedURLs = MSMigrationUtils.getTypedURLs( + "Software\\Microsoft\\Internet Explorer" + ); + } catch (ex) {} + let dates = [0, ...typedURLs.values()]; + // dates is an array of PRTimes, which are in microseconds - convert to milliseconds + resolve(Math.max.apply(Math, dates) / 1000); + }) + ); + return Promise.all(datePromises).then(dates => { + return new Date(Math.max.apply(Math, dates)); + }); + } +} diff --git a/browser/components/migration/MigrationUtils.sys.mjs b/browser/components/migration/MigrationUtils.sys.mjs index e78314aa4696..0b8c59e4f18c 100644 --- a/browser/components/migration/MigrationUtils.sys.mjs +++ b/browser/components/migration/MigrationUtils.sys.mjs @@ -2,22 +2,13 @@ * 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/. */ -const TOPIC_WILL_IMPORT_BOOKMARKS = - "initial-migration-will-import-default-bookmarks"; -const TOPIC_DID_IMPORT_BOOKMARKS = - "initial-migration-did-import-default-bookmarks"; -const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete"; - import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", - PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", - ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.sys.mjs", Sqlite: "resource://gre/modules/Sqlite.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", @@ -44,475 +35,12 @@ function getL10n() { return gL10n; } -/** - * @typedef {object} MigratorResource - * A resource returned by a subclass of MigratorPrototype that can migrate - * data to this browser. - * @property {number} type - * A bitfield with bits from nsIBrowserProfileMigrator flipped to indicate - * what this resource represents. A resource can represent one or more types - * of data, for example HISTORY and FORMDATA. - * @property {Function} migrate - * A function that will actually perform the migration of this resource's - * data into this browser. - */ - -/** - * Shared prototype for migrators, implementing nsIBrowserProfileMigrator. - * - * To implement a migrator: - * 1. Import this module. - * 2. Create the prototype for the migrator, extending MigratorPrototype. - * Namely: MosaicMigrator.prototype = Object.create(MigratorPrototype); - * 3. Set classDescription, contractID and classID for your migrator, and set - * NSGetFactory appropriately. - * 4. If the migrator supports multiple profiles, override the sourceProfiles - * Here we default for single-profile migrator. - * 5. Implement getResources(aProfile) (see below). - * 6. For startup-only migrators, override |startupOnlyMigrator|. - */ -export var MigratorPrototype = { - QueryInterface: ChromeUtils.generateQI(["nsIBrowserProfileMigrator"]), - - /** - * OVERRIDE IF AND ONLY IF the source supports multiple profiles. - * - * Returns array of profile objects from which data may be imported. The object - * should have the following keys: - * id - a unique string identifier for the profile - * name - a pretty name to display to the user in the UI - * - * Only profiles from which data can be imported should be listed. Otherwise - * the behavior of the migration wizard isn't well-defined. - * - * For a single-profile source (e.g. safari, ie), this returns null, - * and not an empty array. That is the default implementation. - * - * @abstract - * @returns {object[]|null} - */ - getSourceProfiles() { - return null; - }, - - /** - * MUST BE OVERRIDDEN. - * - * Returns an array of "migration resources" objects for the given profile, - * or for the "default" profile, if the migrator does not support multiple - * profiles. - * - * Each migration resource should provide: - * - a |type| getter, returning any of the migration types (see - * nsIBrowserProfileMigrator). - * - * - a |migrate| method, taking a single argument, aCallback(bool success), - * for migrating the data for this resource. It may do its job - * synchronously or asynchronously. Either way, it must call - * aCallback(bool aSuccess) when it's done. In the case of an exception - * thrown from |migrate|, it's taken as if aCallback(false) is called. - * - * Note: In the case of a simple asynchronous implementation, you may find - * MigrationUtils.wrapMigrateFunction handy for handling aCallback easily. - * - * For each migration type listed in nsIBrowserProfileMigrator, multiple - * migration resources may be provided. This practice is useful when the - * data for a certain migration type is independently stored in few - * locations. For example, the mac version of Safari stores its "reading list" - * bookmarks in a separate property list. - * - * Note that the importation of a particular migration type is reported as - * successful if _any_ of its resources succeeded to import (that is, called, - * |aCallback(true)|). However, completion-status for a particular migration - * type is reported to the UI only once all of its migrators have called - * aCallback. - * - * NOTE: The returned array should only include resources from which data - * can be imported. So, for example, before adding a resource for the - * BOOKMARKS migration type, you should check if you should check that the - * bookmarks file exists. - * - * @abstract - * @param {object|string} aProfile - * The profile from which data may be imported, or an empty string - * in the case of a single-profile migrator. - * In the case of multiple-profiles migrator, it is guaranteed that - * aProfile is a value returned by the sourceProfiles getter (see - * above). - */ - // eslint-disable-next-line no-unused-vars - getResources: function MP_getResources(aProfile) { - throw new Error("getResources must be overridden"); - }, - - /** - * OVERRIDE in order to provide an estimate of when the last time was - * that somebody used the browser. It is OK that this is somewhat fuzzy - - * history may not be available (or be wiped or not present due to e.g. - * incognito mode). - * - * If not overridden, the promise will resolve to the Unix epoch. - * - * @returns {Promise} - * A Promise that resolves to the last used date. - */ - getLastUsedDate() { - return Promise.resolve(new Date(0)); - }, - - /** - * OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now, - * that is just the Firefox migrator, see bug 737381). Default: false. - * - * Startup-only migrators are different in two ways: - * - they may only be used during startup. - * - the user-profile is half baked during migration. The folder exists, - * but it's only accessible through MigrationUtils.profileStartup. - * The migrator can call MigrationUtils.profileStartup.doStartup - * at any point in order to initialize the profile. - * - * @returns {boolean} - * true if the migrator is start-up only. - */ - get startupOnlyMigrator() { - return false; - }, - - /** - * Returns true if the migrator is configured to be enabled. This is - * controlled by the `browser.migrate..enabled` boolean - * preference. - * - * @returns {boolean} - * true if the migrator should be shown in the migration wizard. - */ - get enabled() { - let key = this.getBrowserKey(); - return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false); - }, - - /** - * DO NOT OVERRIDE - After deCOMing migration, the UI will just call - * getResources. - * - * @see nsIBrowserProfileMigrator - * @param {object|string} aProfile - * The profile from which data may be imported, or an empty string - * in the case of a single-profile migrator. - * @returns {MigratorResource[]} - */ - getMigrateData: async function MP_getMigrateData(aProfile) { - let resources = await this._getMaybeCachedResources(aProfile); - if (!resources) { - return 0; - } - let types = resources.map(r => r.type); - return types.reduce((a, b) => { - a |= b; - return a; - }, 0); - }, - - getBrowserKey: function MP_getBrowserKey() { - return this.contractID.match(/\=([^\=]+)$/)[1]; - }, - - /** - * DO NOT OVERRIDE - After deCOMing migration, the UI will just call - * migrate for each resource. - * - * @see nsIBrowserProfileMigrator - * @param {number} aItems - * A bitfield with bits from nsIBrowserProfileMigrator flipped to indicate - * what types of resources should be migrated. - * @param {boolean} aStartup - * True if this migration is occurring during startup. - * @param {object|string} aProfile - * The other browser profile that is being migrated from. - */ - migrate: async function MP_migrate(aItems, aStartup, aProfile) { - let resources = await this._getMaybeCachedResources(aProfile); - if (!resources.length) { - throw new Error("migrate called for a non-existent source"); - } - - if (aItems != Ci.nsIBrowserProfileMigrator.ALL) { - resources = resources.filter(r => aItems & r.type); - } - - // Used to periodically give back control to the main-thread loop. - let unblockMainThread = function() { - return new Promise(resolve => { - Services.tm.dispatchToMainThread(resolve); - }); - }; - - let getHistogramIdForResourceType = (resourceType, template) => { - if (resourceType == MigrationUtils.resourceTypes.HISTORY) { - return template.replace("*", "HISTORY"); - } - if (resourceType == MigrationUtils.resourceTypes.BOOKMARKS) { - return template.replace("*", "BOOKMARKS"); - } - if (resourceType == MigrationUtils.resourceTypes.PASSWORDS) { - return template.replace("*", "LOGINS"); - } - return null; - }; - - let browserKey = this.getBrowserKey(); - - let maybeStartTelemetryStopwatch = resourceType => { - let histogramId = getHistogramIdForResourceType( - resourceType, - "FX_MIGRATION_*_IMPORT_MS" - ); - if (histogramId) { - TelemetryStopwatch.startKeyed(histogramId, browserKey); - } - return histogramId; - }; - - let maybeStartResponsivenessMonitor = resourceType => { - let responsivenessMonitor; - let responsivenessHistogramId = getHistogramIdForResourceType( - resourceType, - "FX_MIGRATION_*_JANK_MS" - ); - if (responsivenessHistogramId) { - responsivenessMonitor = new lazy.ResponsivenessMonitor(); - } - return { responsivenessMonitor, responsivenessHistogramId }; - }; - - let maybeFinishResponsivenessMonitor = ( - responsivenessMonitor, - histogramId - ) => { - if (responsivenessMonitor) { - let accumulatedDelay = responsivenessMonitor.finish(); - if (histogramId) { - try { - Services.telemetry - .getKeyedHistogramById(histogramId) - .add(browserKey, accumulatedDelay); - } catch (ex) { - Cu.reportError(histogramId + ": " + ex); - } - } - } - }; - - let collectQuantityTelemetry = () => { - for (let resourceType of Object.keys(MigrationUtils._importQuantities)) { - let histogramId = - "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY"; - try { - Services.telemetry - .getKeyedHistogramById(histogramId) - .add(browserKey, MigrationUtils._importQuantities[resourceType]); - } catch (ex) { - Cu.reportError(histogramId + ": " + ex); - } - } - }; - - // Called either directly or through the bookmarks import callback. - let doMigrate = async function() { - let resourcesGroupedByItems = new Map(); - resources.forEach(function(resource) { - if (!resourcesGroupedByItems.has(resource.type)) { - resourcesGroupedByItems.set(resource.type, new Set()); - } - resourcesGroupedByItems.get(resource.type).add(resource); - }); - - if (resourcesGroupedByItems.size == 0) { - throw new Error("No items to import"); - } - - let notify = function(aMsg, aItemType) { - Services.obs.notifyObservers(null, aMsg, aItemType); - }; - - for (let resourceType of Object.keys(MigrationUtils._importQuantities)) { - MigrationUtils._importQuantities[resourceType] = 0; - } - notify("Migration:Started"); - for (let [migrationType, itemResources] of resourcesGroupedByItems) { - notify("Migration:ItemBeforeMigrate", migrationType); - - let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType); - - let { - responsivenessMonitor, - responsivenessHistogramId, - } = maybeStartResponsivenessMonitor(migrationType); - - let itemSuccess = false; - for (let res of itemResources) { - let completeDeferred = lazy.PromiseUtils.defer(); - let resourceDone = function(aSuccess) { - itemResources.delete(res); - itemSuccess |= aSuccess; - if (itemResources.size == 0) { - notify( - itemSuccess - ? "Migration:ItemAfterMigrate" - : "Migration:ItemError", - migrationType - ); - resourcesGroupedByItems.delete(migrationType); - - if (stopwatchHistogramId) { - TelemetryStopwatch.finishKeyed( - stopwatchHistogramId, - browserKey - ); - } - - maybeFinishResponsivenessMonitor( - responsivenessMonitor, - responsivenessHistogramId - ); - - if (resourcesGroupedByItems.size == 0) { - collectQuantityTelemetry(); - notify("Migration:Ended"); - } - } - completeDeferred.resolve(); - }; - - // If migrate throws, an error occurred, and the callback - // (itemMayBeDone) might haven't been called. - try { - res.migrate(resourceDone); - } catch (ex) { - Cu.reportError(ex); - resourceDone(false); - } - - await completeDeferred.promise; - await unblockMainThread(); - } - } - }; - - if ( - MigrationUtils.isStartupMigration && - !this.startupOnlyMigrator && - Services.policies.isAllowed("defaultBookmarks") - ) { - MigrationUtils.profileStartup.doStartup(); - // First import the default bookmarks. - // Note: We do not need to do so for the Firefox migrator - // (=startupOnlyMigrator), as it just copies over the places database - // from another profile. - (async function() { - // Tell nsBrowserGlue we're importing default bookmarks. - let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( - Ci.nsIObserver - ); - browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, ""); - - // Import the default bookmarks. We ignore whether or not we succeed. - await lazy.BookmarkHTMLUtils.importFromURL( - "chrome://browser/content/default-bookmarks.html", - { - replace: true, - source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP, - } - ).catch(Cu.reportError); - - // We'll tell nsBrowserGlue we've imported bookmarks, but before that - // we need to make sure we're going to know when it's finished - // initializing places: - let placesInitedPromise = new Promise(resolve => { - let onPlacesInited = function() { - Services.obs.removeObserver( - onPlacesInited, - TOPIC_PLACES_DEFAULTS_FINISHED - ); - resolve(); - }; - Services.obs.addObserver( - onPlacesInited, - TOPIC_PLACES_DEFAULTS_FINISHED - ); - }); - browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, ""); - await placesInitedPromise; - doMigrate(); - })(); - return; - } - doMigrate(); - }, - - /** - * DO NOT OVERRIDE - After deCOMing migration, this code - * won't be part of the migrator itself. - * - * @see nsIBrowserProfileMigrator - */ - async isSourceAvailable() { - if (this.startupOnlyMigrator && !MigrationUtils.isStartupMigration) { - return false; - } - - // For a single-profile source, check if any data is available. - // For multiple-profiles source, make sure that at least one - // profile is available. - let exists = false; - try { - let profiles = await this.getSourceProfiles(); - if (!profiles) { - let resources = await this._getMaybeCachedResources(""); - if (resources && resources.length) { - exists = true; - } - } else { - exists = !!profiles.length; - } - } catch (ex) { - Cu.reportError(ex); - } - return exists; - }, - - /*** PRIVATE STUFF - DO NOT OVERRIDE ***/ - - /** - * Returns resources for a particular profile and then caches them for later - * lookups. - * - * @param {object|string} aProfile - * The profile that resources are being imported from. - * @returns {Promise} - */ - _getMaybeCachedResources: async function PMB__getMaybeCachedResources( - aProfile - ) { - let profileKey = aProfile ? aProfile.id : ""; - if (this._resourcesByProfile) { - if (profileKey in this._resourcesByProfile) { - return this._resourcesByProfile[profileKey]; - } - } else { - this._resourcesByProfile = {}; - } - this._resourcesByProfile[profileKey] = await this.getResources(aProfile); - return this._resourcesByProfile[profileKey]; - }, -}; - /** * The singleton MigrationUtils service. This service is the primary mechanism * by which migrations from other browsers to this browser occur. The singleton * instance of this class is exported from this module as `MigrationUtils`. */ -class MigrationUtilsSingleton { +class MigrationUtils { resourceTypes = Object.freeze({ COOKIES: Ci.nsIBrowserProfileMigrator.COOKIES, HISTORY: Ci.nsIBrowserProfileMigrator.HISTORY, @@ -525,7 +53,7 @@ class MigrationUtilsSingleton { /** * Helper for implementing simple asynchronous cases of migration resources' - * |migrate(aCallback)| (see MigratorPrototype). If your |migrate| method + * |migrate(aCallback)| (see MigratorBase). If your |migrate| method * just waits for some file to be read, for example, and then migrates * everything right away, you can wrap the async-function with this helper * and not worry about notifying the callback. @@ -711,7 +239,7 @@ class MigrationUtilsSingleton { * Internal name of the migration source. See `availableMigratorKeys` * for supported values by OS. * - * @returns {MigratorPrototype} + * @returns {MigratorBase} * A profile migrator implementing nsIBrowserProfileMigrator, if it can * import any data, null otherwise. */ @@ -1420,4 +948,6 @@ class MigrationUtilsSingleton { } } -export const MigrationUtils = new MigrationUtilsSingleton(); +const MigrationUtilsSingleton = new MigrationUtils(); + +export { MigrationUtilsSingleton as MigrationUtils }; diff --git a/browser/components/migration/MigratorBase.sys.mjs b/browser/components/migration/MigratorBase.sys.mjs new file mode 100644 index 000000000000..7817ec82318e --- /dev/null +++ b/browser/components/migration/MigratorBase.sys.mjs @@ -0,0 +1,489 @@ +/* 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/. */ + +const TOPIC_WILL_IMPORT_BOOKMARKS = + "initial-migration-will-import-default-bookmarks"; +const TOPIC_DID_IMPORT_BOOKMARKS = + "initial-migration-did-import-default-bookmarks"; +const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", + MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.sys.mjs", +}); + +/** + * @typedef {object} MigratorResource + * A resource returned by a subclass of MigratorBase that can migrate + * data to this browser. + * @property {number} type + * A bitfield with bits from nsIBrowserProfileMigrator flipped to indicate + * what this resource represents. A resource can represent one or more types + * of data, for example HISTORY and FORMDATA. + * @property {Function} migrate + * A function that will actually perform the migration of this resource's + * data into this browser. + */ + +/** + * Shared prototype for migrators, implementing nsIBrowserProfileMigrator. + * + * To implement a migrator: + * 1. Import this module. + * 2. Create the prototype for the migrator, extending MigratorBase. + * 3. Set classDescription, contractID and classID for your migrator, and update + * components.conf to register the migrator as an XPCOM component. + * 4. If the migrator supports multiple profiles, override the sourceProfiles + * Here we default for single-profile migrator. + * 5. Implement getResources(aProfile) (see below). + * 6. For startup-only migrators, override |startupOnlyMigrator|. + */ +export class MigratorBase { + QueryInterface = ChromeUtils.generateQI(["nsIBrowserProfileMigrator"]); + + /** + * OVERRIDE IF AND ONLY IF the source supports multiple profiles. + * + * Returns array of profile objects from which data may be imported. The object + * should have the following keys: + * id - a unique string identifier for the profile + * name - a pretty name to display to the user in the UI + * + * Only profiles from which data can be imported should be listed. Otherwise + * the behavior of the migration wizard isn't well-defined. + * + * For a single-profile source (e.g. safari, ie), this returns null, + * and not an empty array. That is the default implementation. + * + * @abstract + * @returns {object[]|null} + */ + getSourceProfiles() { + return null; + } + + /** + * MUST BE OVERRIDDEN. + * + * Returns an array of "migration resources" objects for the given profile, + * or for the "default" profile, if the migrator does not support multiple + * profiles. + * + * Each migration resource should provide: + * - a |type| getter, returning any of the migration types (see + * nsIBrowserProfileMigrator). + * + * - a |migrate| method, taking a single argument, aCallback(bool success), + * for migrating the data for this resource. It may do its job + * synchronously or asynchronously. Either way, it must call + * aCallback(bool aSuccess) when it's done. In the case of an exception + * thrown from |migrate|, it's taken as if aCallback(false) is called. + * + * Note: In the case of a simple asynchronous implementation, you may find + * MigrationUtils.wrapMigrateFunction handy for handling aCallback easily. + * + * For each migration type listed in nsIBrowserProfileMigrator, multiple + * migration resources may be provided. This practice is useful when the + * data for a certain migration type is independently stored in few + * locations. For example, the mac version of Safari stores its "reading list" + * bookmarks in a separate property list. + * + * Note that the importation of a particular migration type is reported as + * successful if _any_ of its resources succeeded to import (that is, called, + * |aCallback(true)|). However, completion-status for a particular migration + * type is reported to the UI only once all of its migrators have called + * aCallback. + * + * NOTE: The returned array should only include resources from which data + * can be imported. So, for example, before adding a resource for the + * BOOKMARKS migration type, you should check if you should check that the + * bookmarks file exists. + * + * @abstract + * @param {object|string} aProfile + * The profile from which data may be imported, or an empty string + * in the case of a single-profile migrator. + * In the case of multiple-profiles migrator, it is guaranteed that + * aProfile is a value returned by the sourceProfiles getter (see + * above). + * @returns {Promise|MigratorResource[]} + */ + // eslint-disable-next-line no-unused-vars + getResources(aProfile) { + throw new Error("getResources must be overridden"); + } + + /** + * OVERRIDE in order to provide an estimate of when the last time was + * that somebody used the browser. It is OK that this is somewhat fuzzy - + * history may not be available (or be wiped or not present due to e.g. + * incognito mode). + * + * If not overridden, the promise will resolve to the Unix epoch. + * + * @returns {Promise} + * A Promise that resolves to the last used date. + */ + getLastUsedDate() { + return Promise.resolve(new Date(0)); + } + + /** + * OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now, + * that is just the Firefox migrator, see bug 737381). Default: false. + * + * Startup-only migrators are different in two ways: + * - they may only be used during startup. + * - the user-profile is half baked during migration. The folder exists, + * but it's only accessible through MigrationUtils.profileStartup. + * The migrator can call MigrationUtils.profileStartup.doStartup + * at any point in order to initialize the profile. + * + * @returns {boolean} + * true if the migrator is start-up only. + */ + get startupOnlyMigrator() { + return false; + } + + /** + * Returns true if the migrator is configured to be enabled. This is + * controlled by the `browser.migrate..enabled` boolean + * preference. + * + * @returns {boolean} + * true if the migrator should be shown in the migration wizard. + */ + get enabled() { + let key = this.getBrowserKey(); + return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false); + } + + /** + * DO NOT OVERRIDE - After deCOMing migration, the UI will just call + * getResources. + * + * See nsIBrowserProfileMigrator. + * + * @param {object|string} aProfile + * The profile from which data may be imported, or an empty string + * in the case of a single-profile migrator. + * @returns {MigratorResource[]} + */ + async getMigrateData(aProfile) { + let resources = await this.#getMaybeCachedResources(aProfile); + if (!resources) { + return 0; + } + let types = resources.map(r => r.type); + return types.reduce((a, b) => { + a |= b; + return a; + }, 0); + } + + getBrowserKey() { + return this.contractID.match(/\=([^\=]+)$/)[1]; + } + + /** + * DO NOT OVERRIDE - After deCOMing migration, the UI will just call + * migrate for each resource. + * + * See nsIBrowserProfileMigrator. + * + * @param {number} aItems + * A bitfield with bits from nsIBrowserProfileMigrator flipped to indicate + * what types of resources should be migrated. + * @param {boolean} aStartup + * True if this migration is occurring during startup. + * @param {object|string} aProfile + * The other browser profile that is being migrated from. + */ + async migrate(aItems, aStartup, aProfile) { + let resources = await this.#getMaybeCachedResources(aProfile); + if (!resources.length) { + throw new Error("migrate called for a non-existent source"); + } + + if (aItems != Ci.nsIBrowserProfileMigrator.ALL) { + resources = resources.filter(r => aItems & r.type); + } + + // Used to periodically give back control to the main-thread loop. + let unblockMainThread = function() { + return new Promise(resolve => { + Services.tm.dispatchToMainThread(resolve); + }); + }; + + let getHistogramIdForResourceType = (resourceType, template) => { + if (resourceType == lazy.MigrationUtils.resourceTypes.HISTORY) { + return template.replace("*", "HISTORY"); + } + if (resourceType == lazy.MigrationUtils.resourceTypes.BOOKMARKS) { + return template.replace("*", "BOOKMARKS"); + } + if (resourceType == lazy.MigrationUtils.resourceTypes.PASSWORDS) { + return template.replace("*", "LOGINS"); + } + return null; + }; + + let browserKey = this.getBrowserKey(); + + let maybeStartTelemetryStopwatch = resourceType => { + let histogramId = getHistogramIdForResourceType( + resourceType, + "FX_MIGRATION_*_IMPORT_MS" + ); + if (histogramId) { + TelemetryStopwatch.startKeyed(histogramId, browserKey); + } + return histogramId; + }; + + let maybeStartResponsivenessMonitor = resourceType => { + let responsivenessMonitor; + let responsivenessHistogramId = getHistogramIdForResourceType( + resourceType, + "FX_MIGRATION_*_JANK_MS" + ); + if (responsivenessHistogramId) { + responsivenessMonitor = new lazy.ResponsivenessMonitor(); + } + return { responsivenessMonitor, responsivenessHistogramId }; + }; + + let maybeFinishResponsivenessMonitor = ( + responsivenessMonitor, + histogramId + ) => { + if (responsivenessMonitor) { + let accumulatedDelay = responsivenessMonitor.finish(); + if (histogramId) { + try { + Services.telemetry + .getKeyedHistogramById(histogramId) + .add(browserKey, accumulatedDelay); + } catch (ex) { + Cu.reportError(histogramId + ": " + ex); + } + } + } + }; + + let collectQuantityTelemetry = () => { + for (let resourceType of Object.keys( + lazy.MigrationUtils._importQuantities + )) { + let histogramId = + "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY"; + try { + Services.telemetry + .getKeyedHistogramById(histogramId) + .add( + browserKey, + lazy.MigrationUtils._importQuantities[resourceType] + ); + } catch (ex) { + Cu.reportError(histogramId + ": " + ex); + } + } + }; + + // Called either directly or through the bookmarks import callback. + let doMigrate = async function() { + let resourcesGroupedByItems = new Map(); + resources.forEach(function(resource) { + if (!resourcesGroupedByItems.has(resource.type)) { + resourcesGroupedByItems.set(resource.type, new Set()); + } + resourcesGroupedByItems.get(resource.type).add(resource); + }); + + if (resourcesGroupedByItems.size == 0) { + throw new Error("No items to import"); + } + + let notify = function(aMsg, aItemType) { + Services.obs.notifyObservers(null, aMsg, aItemType); + }; + + for (let resourceType of Object.keys( + lazy.MigrationUtils._importQuantities + )) { + lazy.MigrationUtils._importQuantities[resourceType] = 0; + } + notify("Migration:Started"); + for (let [migrationType, itemResources] of resourcesGroupedByItems) { + notify("Migration:ItemBeforeMigrate", migrationType); + + let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType); + + let { + responsivenessMonitor, + responsivenessHistogramId, + } = maybeStartResponsivenessMonitor(migrationType); + + let itemSuccess = false; + for (let res of itemResources) { + let completeDeferred = lazy.PromiseUtils.defer(); + let resourceDone = function(aSuccess) { + itemResources.delete(res); + itemSuccess |= aSuccess; + if (itemResources.size == 0) { + notify( + itemSuccess + ? "Migration:ItemAfterMigrate" + : "Migration:ItemError", + migrationType + ); + resourcesGroupedByItems.delete(migrationType); + + if (stopwatchHistogramId) { + TelemetryStopwatch.finishKeyed( + stopwatchHistogramId, + browserKey + ); + } + + maybeFinishResponsivenessMonitor( + responsivenessMonitor, + responsivenessHistogramId + ); + + if (resourcesGroupedByItems.size == 0) { + collectQuantityTelemetry(); + notify("Migration:Ended"); + } + } + completeDeferred.resolve(); + }; + + // If migrate throws, an error occurred, and the callback + // (itemMayBeDone) might haven't been called. + try { + res.migrate(resourceDone); + } catch (ex) { + Cu.reportError(ex); + resourceDone(false); + } + + await completeDeferred.promise; + await unblockMainThread(); + } + } + }; + + if ( + lazy.MigrationUtils.isStartupMigration && + !this.startupOnlyMigrator && + Services.policies.isAllowed("defaultBookmarks") + ) { + lazy.MigrationUtils.profileStartup.doStartup(); + // First import the default bookmarks. + // Note: We do not need to do so for the Firefox migrator + // (=startupOnlyMigrator), as it just copies over the places database + // from another profile. + (async function() { + // Tell nsBrowserGlue we're importing default bookmarks. + let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver + ); + browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, ""); + + // Import the default bookmarks. We ignore whether or not we succeed. + await lazy.BookmarkHTMLUtils.importFromURL( + "chrome://browser/content/default-bookmarks.html", + { + replace: true, + source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP, + } + ).catch(Cu.reportError); + + // We'll tell nsBrowserGlue we've imported bookmarks, but before that + // we need to make sure we're going to know when it's finished + // initializing places: + let placesInitedPromise = new Promise(resolve => { + let onPlacesInited = function() { + Services.obs.removeObserver( + onPlacesInited, + TOPIC_PLACES_DEFAULTS_FINISHED + ); + resolve(); + }; + Services.obs.addObserver( + onPlacesInited, + TOPIC_PLACES_DEFAULTS_FINISHED + ); + }); + browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, ""); + await placesInitedPromise; + doMigrate(); + })(); + return; + } + doMigrate(); + } + + /** + * DO NOT OVERRIDE - After deCOMing migration, this code + * won't be part of the migrator itself. + * + * See nsIBrowserProfileMigrator. + */ + async isSourceAvailable() { + if (this.startupOnlyMigrator && !lazy.MigrationUtils.isStartupMigration) { + return false; + } + + // For a single-profile source, check if any data is available. + // For multiple-profiles source, make sure that at least one + // profile is available. + let exists = false; + try { + let profiles = await this.getSourceProfiles(); + if (!profiles) { + let resources = await this.#getMaybeCachedResources(""); + if (resources && resources.length) { + exists = true; + } + } else { + exists = !!profiles.length; + } + } catch (ex) { + Cu.reportError(ex); + } + return exists; + } + + /*** PRIVATE STUFF - DO NOT OVERRIDE ***/ + + /** + * Returns resources for a particular profile and then caches them for later + * lookups. + * + * @param {object|string} aProfile + * The profile that resources are being imported from. + * @returns {Promise} + */ + async #getMaybeCachedResources(aProfile) { + let profileKey = aProfile ? aProfile.id : ""; + if (this._resourcesByProfile) { + if (profileKey in this._resourcesByProfile) { + return this._resourcesByProfile[profileKey]; + } + } else { + this._resourcesByProfile = {}; + } + this._resourcesByProfile[profileKey] = await this.getResources(aProfile); + return this._resourcesByProfile[profileKey]; + } +} diff --git a/browser/components/migration/SafariProfileMigrator.sys.mjs b/browser/components/migration/SafariProfileMigrator.sys.mjs index 04f202472b30..48c0336b1675 100644 --- a/browser/components/migration/SafariProfileMigrator.sys.mjs +++ b/browser/components/migration/SafariProfileMigrator.sys.mjs @@ -5,10 +5,8 @@ import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); -import { - MigrationUtils, - MigratorPrototype, -} from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; const lazy = {}; @@ -344,135 +342,134 @@ SearchStrings.prototype = { }, }; -export function SafariProfileMigrator() {} - -SafariProfileMigrator.prototype = Object.create(MigratorPrototype); - -SafariProfileMigrator.prototype.getResources = function SM_getResources() { - let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); - if (!profileDir.exists()) { - return null; +/** + * Safari migrator + */ +export class SafariProfileMigrator extends MigratorBase { + get classDescription() { + return "Safari Profile Migrator"; } - let resources = []; - let pushProfileFileResource = function(aFileName, aConstructor) { - let file = profileDir.clone(); - file.append(aFileName); - if (file.exists()) { - resources.push(new aConstructor(file)); + get contractID() { + return "@mozilla.org/profile/migrator;1?app=browser&type=safari"; + } + + get classID() { + return Components.ID("{4b609ecf-60b2-4655-9df4-dc149e474da1}"); + } + + getResources() { + let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); + if (!profileDir.exists()) { + return null; } - }; - pushProfileFileResource("History.plist", History); - pushProfileFileResource("Bookmarks.plist", Bookmarks); + let resources = []; + let pushProfileFileResource = function(aFileName, aConstructor) { + let file = profileDir.clone(); + file.append(aFileName); + if (file.exists()) { + resources.push(new aConstructor(file)); + } + }; - // The Reading List feature was introduced at the same time in Windows and - // Mac versions of Safari. Not surprisingly, they are stored in the same - // format in both versions. Surpsingly, only on Windows there is a - // separate property list for it. This code is used on mac too, because - // Apple may fix this at some point. - pushProfileFileResource("ReadingList.plist", Bookmarks); + pushProfileFileResource("History.plist", History); + pushProfileFileResource("Bookmarks.plist", Bookmarks); - let prefs = this.mainPreferencesPropertyList; - if (prefs) { - resources.push(new SearchStrings(prefs)); + // The Reading List feature was introduced at the same time in Windows and + // Mac versions of Safari. Not surprisingly, they are stored in the same + // format in both versions. Surpsingly, only on Windows there is a + // separate property list for it. This code is used on mac too, because + // Apple may fix this at some point. + pushProfileFileResource("ReadingList.plist", Bookmarks); + + let prefs = this.mainPreferencesPropertyList; + if (prefs) { + resources.push(new SearchStrings(prefs)); + } + + return resources; } - return resources; -}; - -SafariProfileMigrator.prototype.getLastUsedDate = function SM_getLastUsedDate() { - let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); - let datePromises = ["Bookmarks.plist", "History.plist"].map(file => { - let path = OS.Path.join(profileDir.path, file); - return OS.File.stat(path) - .catch(() => null) - .then(info => { - return info ? info.lastModificationDate : 0; - }); - }); - return Promise.all(datePromises).then(dates => { - return new Date(Math.max.apply(Math, dates)); - }); -}; - -SafariProfileMigrator.prototype.hasPermissions = async function SM_hasPermissions() { - if (this._hasPermissions) { - return true; + getLastUsedDate() { + let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); + let datePromises = ["Bookmarks.plist", "History.plist"].map(file => { + let path = OS.Path.join(profileDir.path, file); + return OS.File.stat(path) + .catch(() => null) + .then(info => { + return info ? info.lastModificationDate : 0; + }); + }); + return Promise.all(datePromises).then(dates => { + return new Date(Math.max.apply(Math, dates)); + }); } - // Check if we have access: - let target = FileUtils.getDir( - "ULibDir", - ["Safari", "Bookmarks.plist"], - false - ); - try { - // 'stat' is always allowed, but reading is somehow not, if the user hasn't - // allowed it: - await IOUtils.read(target.path, { maxBytes: 1 }); - this._hasPermissions = true; - return true; - } catch (ex) { - return false; - } -}; -SafariProfileMigrator.prototype.getPermissions = async function SM_getPermissions( - win -) { - // Keep prompting the user until they pick a file that grants us access, - // or they cancel out of the file open panel. - while (!(await this.hasPermissions())) { - let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - // The title (second arg) is not displayed on macOS, so leave it blank. - fp.init(win, "", Ci.nsIFilePicker.modeOpen); - // This is a little weird. You'd expect that it matters which file - // the user picks, but it doesn't really, as long as it's in this - // directory. Anyway, let's not confuse the user: the sensible idea - // here is to ask for permissions for Bookmarks.plist, and we'll - // silently accept whatever input as long as we can then read the plist. - fp.appendFilter("plist", "*.plist"); - fp.filterIndex = 1; - fp.displayDirectory = FileUtils.getDir("ULibDir", ["Safari"], false); - // Now wait for the filepicker to open and close. If the user picks - // any file in this directory, macOS will grant us read access, so - // we don't need to check or do anything else with the file returned - // by the filepicker. - let result = await new Promise(resolve => fp.open(resolve)); - // Bail if the user cancels the dialog: - if (result == Ci.nsIFilePicker.returnCancel) { + async hasPermissions() { + if (this._hasPermissions) { + return true; + } + // Check if we have access: + let target = FileUtils.getDir( + "ULibDir", + ["Safari", "Bookmarks.plist"], + false + ); + try { + // 'stat' is always allowed, but reading is somehow not, if the user hasn't + // allowed it: + await IOUtils.read(target.path, { maxBytes: 1 }); + this._hasPermissions = true; + return true; + } catch (ex) { return false; } } -}; -Object.defineProperty( - SafariProfileMigrator.prototype, - "mainPreferencesPropertyList", - { - get: function get_mainPreferencesPropertyList() { - if (this._mainPreferencesPropertyList === undefined) { - let file = FileUtils.getDir("UsrPrfs", [], false); - if (file.exists()) { - file.append("com.apple.Safari.plist"); - if (file.exists()) { - this._mainPreferencesPropertyList = new MainPreferencesPropertyList( - file - ); - return this._mainPreferencesPropertyList; - } - } - this._mainPreferencesPropertyList = null; - return this._mainPreferencesPropertyList; + async getPermissions(win) { + // Keep prompting the user until they pick a file that grants us access, + // or they cancel out of the file open panel. + while (!(await this.hasPermissions())) { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + // The title (second arg) is not displayed on macOS, so leave it blank. + fp.init(win, "", Ci.nsIFilePicker.modeOpen); + // This is a little weird. You'd expect that it matters which file + // the user picks, but it doesn't really, as long as it's in this + // directory. Anyway, let's not confuse the user: the sensible idea + // here is to ask for permissions for Bookmarks.plist, and we'll + // silently accept whatever input as long as we can then read the plist. + fp.appendFilter("plist", "*.plist"); + fp.filterIndex = 1; + fp.displayDirectory = FileUtils.getDir("ULibDir", ["Safari"], false); + // Now wait for the filepicker to open and close. If the user picks + // any file in this directory, macOS will grant us read access, so + // we don't need to check or do anything else with the file returned + // by the filepicker. + let result = await new Promise(resolve => fp.open(resolve)); + // Bail if the user cancels the dialog: + if (result == Ci.nsIFilePicker.returnCancel) { + return false; } - return this._mainPreferencesPropertyList; - }, + } + return true; } -); -SafariProfileMigrator.prototype.classDescription = "Safari Profile Migrator"; -SafariProfileMigrator.prototype.contractID = - "@mozilla.org/profile/migrator;1?app=browser&type=safari"; -SafariProfileMigrator.prototype.classID = Components.ID( - "{4b609ecf-60b2-4655-9df4-dc149e474da1}" -); + get mainPreferencesPropertyList() { + if (this._mainPreferencesPropertyList === undefined) { + let file = FileUtils.getDir("UsrPrfs", [], false); + if (file.exists()) { + file.append("com.apple.Safari.plist"); + if (file.exists()) { + this._mainPreferencesPropertyList = new MainPreferencesPropertyList( + file + ); + return this._mainPreferencesPropertyList; + } + } + this._mainPreferencesPropertyList = null; + return this._mainPreferencesPropertyList; + } + return this._mainPreferencesPropertyList; + } +} diff --git a/browser/components/migration/moz.build b/browser/components/migration/moz.build index 7835409d2e75..e756b2685cb1 100644 --- a/browser/components/migration/moz.build +++ b/browser/components/migration/moz.build @@ -24,6 +24,7 @@ EXTRA_JS_MODULES += [ "ChromeProfileMigrator.sys.mjs", "FirefoxProfileMigrator.sys.mjs", "MigrationUtils.sys.mjs", + "MigratorBase.sys.mjs", "ProfileMigrator.sys.mjs", ] diff --git a/browser/components/migration/tests/unit/head_migration.js b/browser/components/migration/tests/unit/head_migration.js index 84ea85a73ae4..ab1f34e41956 100644 --- a/browser/components/migration/tests/unit/head_migration.js +++ b/browser/components/migration/tests/unit/head_migration.js @@ -1,6 +1,6 @@ "use strict"; -var { MigrationUtils, MigratorPrototype } = ChromeUtils.importESModule( +var { MigrationUtils } = ChromeUtils.importESModule( "resource:///modules/MigrationUtils.sys.mjs" ); var { LoginHelper } = ChromeUtils.import( @@ -42,7 +42,7 @@ updateAppInfo(); /** * Migrates the requested resource and waits for the migration to be complete. * - * @param {MigratorPrototype} migrator + * @param {MigratorBase} migrator * The migrator being used to migrate the data. * @param {number} resourceType * This is a bitfield with bits from nsIBrowserProfileMigrator flipped to indicate what