Bug 1802961 - Convert MigratorPrototype into an ES6 class and move into its own ESM as MigratorBase to be subclassed. r=NeilDeakin

Differential Revision: https://phabricator.services.mozilla.com/D163257
This commit is contained in:
Mike Conley
2022-12-06 17:50:39 +00:00
parent 6d71851894
commit 56e95c55b0
9 changed files with 1600 additions and 1481 deletions

View File

@@ -11,13 +11,9 @@ const AUTH_TYPE = {
}; };
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
import { import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
MigratorPrototype,
MigrationUtils,
} from "resource:///modules/MigrationUtils.sys.mjs";
const lazy = {}; const lazy = {};
@@ -72,136 +68,297 @@ function convertBookmarks(items, errorAccumulator) {
return itemsToInsert; 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"; get classID() {
ChromeProfileMigrator.prototype._keychainAccountName = "Chrome"; return Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
}
ChromeProfileMigrator.prototype._getChromeUserDataPathIfExists = async function() { get _chromeUserDataPathSuffix() {
if (this._chromeUserDataPath) { 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; 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( async getResources(aProfile) {
aProfile let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
) { if (chromeUserDataPath) {
let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); let profileFolder = chromeUserDataPath;
if (chromeUserDataPath) { if (aProfile) {
let profileFolder = chromeUserDataPath; profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id);
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)
);
} }
let possibleResources = await Promise.all(possibleResourcePromises); if (await IOUtils.exists(profileFolder)) {
return possibleResources.filter(r => r != null); let possibleResourcePromises = [
} GetBookmarksResource(profileFolder, this.getBrowserKey()),
} GetHistoryResource(profileFolder),
return []; GetCookiesResource(profileFolder),
}; ];
if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) {
ChromeProfileMigrator.prototype.getLastUsedDate = async function Chrome_getLastUsedDate() { possibleResourcePromises.push(
let sourceProfiles = await this.getSourceProfiles(); this._GetPasswordsResource(profileFolder)
let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); );
if (!chromeUserDataPath) { }
return new Date(0); let possibleResources = await Promise.all(possibleResourcePromises);
} return possibleResources.filter(r => r != null);
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));
};
ChromeProfileMigrator.prototype.getSourceProfiles = async function Chrome_getSourceProfiles() {
if ("__sourceProfiles" in this) {
return this.__sourceProfiles;
}
let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
if (!chromeUserDataPath) {
return []; return [];
} }
let localState; async getLastUsedDate() {
let profiles = []; let sourceProfiles = await this.getSourceProfiles();
try { let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
localState = await lazy.ChromeMigrationUtils.getLocalState( if (!chromeUserDataPath) {
this._chromeUserDataPathSuffix return new Date(0);
);
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 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( async getSourceProfiles() {
profiles.map(async profile => ({ if ("__sourceProfiles" in this) {
profile, return this.__sourceProfiles;
resources: await this.getResources(profile), }
}))
);
// Only list profiles from which any data can be imported let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
this.__sourceProfiles = profileResources if (!chromeUserDataPath) {
.filter(({ resources }) => { return [];
return resources && !!resources.length; }
}, this)
.map(({ profile }) => profile); let localState;
return this.__sourceProfiles; 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) { async function GetBookmarksResource(aProfileFolder, aBrowserKey) {
let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks"); let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks");
@@ -458,328 +615,250 @@ async function GetCookiesResource(aProfileFolder) {
}; };
} }
ChromeProfileMigrator.prototype._GetPasswordsResource = async function( /**
aProfileFolder * Chromium migrator
) { */
let loginPath = PathUtils.join(aProfileFolder, "Login Data"); export class ChromiumProfileMigrator extends ChromeProfileMigrator {
if (!(await IOUtils.exists(loginPath))) { get classDescription() {
return null; return "Chromium Profile Migrator";
} }
let { get contractID() {
_chromeUserDataPathSuffix, return "@mozilla.org/profile/migrator;1?app=browser&type=chromium";
_keychainServiceName, }
_keychainAccountName,
_keychainMockPassphrase = null,
} = this;
return { get classID() {
type: MigrationUtils.resourceTypes.PASSWORDS, return Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}");
}
async migrate(aCallback) { _chromeUserDataPathSuffix = "Chromium";
let rows = await MigrationUtils.getRowsFromDBWithoutLocks( _keychainServiceName = "Chromium Safe Storage";
loginPath, _keychainAccountName = "Chromium";
"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";
} }
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 * Chrome Canary
* Not available on Linux * Not available on Linux
*/ */
export function CanaryProfileMigrator() { export class CanaryProfileMigrator extends ChromeProfileMigrator {
this._chromeUserDataPathSuffix = "Canary"; 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) * Chrome Dev - Linux only (not available in Mac and Windows)
*/ */
export function ChromeDevMigrator() { export class ChromeDevMigrator extends ChromeProfileMigrator {
this._chromeUserDataPathSuffix = "Chrome Dev"; get classDescription() {
} return "Chrome Dev Profile Migrator";
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 function ChromeBetaMigrator() { get contractID() {
this._chromeUserDataPathSuffix = "Chrome Beta"; return "@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev";
} }
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}"
);
export function BraveProfileMigrator() { get classID() {
this._chromeUserDataPathSuffix = "Brave"; return Components.ID("{7370a02a-4886-42c3-a4ec-d48c726ec30a}");
this._keychainServiceName = "Brave Browser Safe Storage"; }
this._keychainAccountName = "Brave Browser";
_chromeUserDataPathSuffix = "Chrome Dev";
_keychainServiceName = "Chromium Safe Storage";
_keychainAccountName = "Chromium";
} }
BraveProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); /**
BraveProfileMigrator.prototype.classDescription = "Brave Browser Migrator"; * Chrome Beta migrator
BraveProfileMigrator.prototype.contractID = */
"@mozilla.org/profile/migrator;1?app=browser&type=brave"; export class ChromeBetaMigrator extends ChromeProfileMigrator {
BraveProfileMigrator.prototype.classID = Components.ID( get classDescription() {
"{4071880a-69e4-4c83-88b4-6c589a62801d}" return "Chrome Beta Profile Migrator";
); }
export function ChromiumEdgeMigrator() { get contractID() {
this._chromeUserDataPathSuffix = "Edge"; return "@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta";
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}"
);
export function ChromiumEdgeBetaMigrator() { get classID() {
this._chromeUserDataPathSuffix = "Edge Beta"; return Components.ID("{47f75963-840b-4950-a1f0-d9c1864f8b8e}");
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}"
);
export function Chromium360seMigrator() { _chromeUserDataPathSuffix = "Chrome Beta";
this._chromeUserDataPathSuffix = "360 SE"; _keychainServiceName = "Chromium Safe Storage";
} _keychainAccountName = "Chromium";
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";
} }
VivaldiProfileMigrator.prototype = Object.create( /**
ChromeProfileMigrator.prototype * Brave migrator
); */
VivaldiProfileMigrator.prototype.classDescription = "Vivaldi Migrator"; export class BraveProfileMigrator extends ChromeProfileMigrator {
VivaldiProfileMigrator.prototype.contractID = get classDescription() {
"@mozilla.org/profile/migrator;1?app=browser&type=vivaldi"; return "Brave Browser Migrator";
VivaldiProfileMigrator.prototype.classID = Components.ID( }
"{54a6a025-e70d-49dd-ba95-0f7e45d728d3}"
); 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";
}

View File

@@ -6,10 +6,8 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
MigrationUtils, import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
MigratorPrototype,
} from "resource:///modules/MigrationUtils.sys.mjs";
import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs"; import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs";
const lazy = {}; const lazy = {};
@@ -466,89 +464,97 @@ EdgeBookmarksMigrator.prototype = {
}, },
}; };
export function EdgeProfileMigrator() { /**
this.wrappedJSObject = this; * Edge (EdgeHTML) profile migrator
}
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.
*/ */
EdgeProfileMigrator.prototype.getSourceProfiles = function() { export class EdgeProfileMigrator extends MigratorBase {
let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10"); constructor() {
return isWin10OrHigher ? null : []; super();
}; this.wrappedJSObject = this;
}
EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator"; get classDescription() {
EdgeProfileMigrator.prototype.contractID = return "Edge Profile Migrator";
"@mozilla.org/profile/migrator;1?app=browser&type=edge"; }
EdgeProfileMigrator.prototype.classID = Components.ID(
"{62e8834b-2d17-49f5-96ff-56344903a2ae}" 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 : [];
}
}

View File

@@ -11,10 +11,9 @@
* from the source profile. * from the source profile.
*/ */
import { import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
MigrationUtils,
MigratorPrototype, import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
} from "resource:///modules/MigrationUtils.sys.mjs";
const lazy = {}; const lazy = {};
@@ -25,341 +24,352 @@ ChromeUtils.defineESModuleGetters(lazy, {
SessionMigration: "resource:///modules/sessionstore/SessionMigration.sys.mjs", SessionMigration: "resource:///modules/sessionstore/SessionMigration.sys.mjs",
}); });
export function FirefoxProfileMigrator() { /**
this.wrappedJSObject = this; // for testing... * 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
FirefoxProfileMigrator.prototype = Object.create(MigratorPrototype); * old profile data into the existing profile.
*
FirefoxProfileMigrator.prototype._getAllProfiles = function() { * This migrator is what powers the "Profile Refresh" mechanism.
let allProfiles = new Map(); */
let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( export class FirefoxProfileMigrator extends MigratorBase {
Ci.nsIToolkitProfileService constructor() {
); super();
for (let profile of profileService.profiles) { this.wrappedJSObject = this; // for testing...
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;
} }
// Being a startup-only migrator, we can rely on get classDescription() {
// MigrationUtils.profileStartup being set. return "Firefox Profile Migrator";
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); get contractID() {
}; return "@mozilla.org/profile/migrator;1?app=browser&type=firefox";
}
FirefoxProfileMigrator.prototype.getLastUsedDate = function() { get classID() {
// We always pretend we're really old, so that we don't mess return Components.ID("{91185366-ba97-4438-acba-48deaca63386}");
// up the determination of which browser is the most 'recent' }
// to import from.
return Promise.resolve(new Date(0));
};
FirefoxProfileMigrator.prototype._getResourcesInternal = function( _getAllProfiles() {
sourceProfileDir, let allProfiles = new Map();
currentProfileDir let profileService = Cc[
) { "@mozilla.org/toolkit/profile-service;1"
let getFileResource = (aMigrationType, aFileNames) => { ].getService(Ci.nsIToolkitProfileService);
let files = []; for (let profile of profileService.profiles) {
for (let fileName of aFileNames) { let rootDir = profile.rootDir;
let file = this._getFileObject(sourceProfileDir, fileName);
if (file) { if (
files.push(file); 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 null;
} }
return {
type: aMigrationType, // Being a startup-only migrator, we can rely on
migrate(aCallback) { // MigrationUtils.profileStartup being set.
for (let file of files) { let currentProfileDir = MigrationUtils.profileStartup.directory;
file.copyTo(currentProfileDir, "");
// 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); aCallback(true);
}, },
}; };
};
function savePrefs() { // Telemetry related migrations.
// If we've used the pref service to write prefs for the new profile, it's too let times = {
// early in startup for the service to have a profile directory, so we have to name: "times", // name is used only by tests.
// manually tell it where to save the prefs file. type: types.OTHERDATA,
let newPrefsFile = currentProfileDir.clone(); migrate: aCallback => {
newPrefsFile.append("prefs.js"); let file = this._getFileObject(sourceProfileDir, "times.json");
Services.prefs.savePrefFile(newPrefsFile); 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; recordMigration();
let places = getFileResource(types.HISTORY, [ },
"places.sqlite", };
"places.sqlite-wal", let telemetry = {
]); name: "telemetry", // name is used only by tests...
let favicons = getFileResource(types.HISTORY, [ type: types.OTHERDATA,
"favicons.sqlite", migrate: aCallback => {
"favicons.sqlite-wal", let createSubDir = name => {
]); let dir = currentProfileDir.clone();
let cookies = getFileResource(types.COOKIES, [ dir.append(name);
"cookies.sqlite", dir.create(Ci.nsIFile.DIRECTORY_TYPE, lazy.FileUtils.PERMS_DIRECTORY);
"cookies.sqlite-wal", return dir;
]); };
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 the 'datareporting' directory exists we migrate files from it.
if (Services.env.get("MOZ_RESET_PROFILE_MIGRATE_SESSION")) { let dataReportingDir = this._getFileObject(
// We only want to restore the previous firefox session if the profile refresh was sourceProfileDir,
// triggered by user. The MOZ_RESET_PROFILE_MIGRATE_SESSION would be set when a user-triggered "datareporting"
// 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 (dataReportingDir && dataReportingDir.isDirectory()) {
if (exists) { // Copy only specific files.
let data = await IOUtils.readJSON(oldPath); let toCopy = ["state.json", "session-state.json"];
if (data && data.accountData && data.accountData.email) {
let username = data.accountData.email; let dest = createSubDir("datareporting");
// copy the file itself. let enumerator = dataReportingDir.directoryEntries;
await IOUtils.copy( while (enumerator.hasMoreElements()) {
oldPath, let file = enumerator.nextFile;
PathUtils.join(currentProfileDir.path, "signedInUser.json") if (file.isDirectory() || !toCopy.includes(file.leafName)) {
); continue;
// 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();
}
} }
file.copyTo(dest, "");
} }
} }
} catch (ex) {
aCallback(false);
return;
}
aCallback(true);
},
};
// Telemetry related migrations. aCallback(true);
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);
}
};
recordMigration(); return [
}, places,
}; cookies,
let telemetry = { passwords,
name: "telemetry", // name is used only by tests... formData,
type: types.OTHERDATA, dictionary,
migrate: aCallback => { bookmarksBackups,
let createSubDir = name => { session,
let dir = currentProfileDir.clone(); sync,
dir.append(name); times,
dir.create(Ci.nsIFile.DIRECTORY_TYPE, lazy.FileUtils.PERMS_DIRECTORY); telemetry,
return dir; favicons,
}; ].filter(r => r);
}
// If the 'datareporting' directory exists we migrate files from it. get startupOnlyMigrator() {
let dataReportingDir = this._getFileObject( return true;
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}"
);

View File

@@ -8,10 +8,8 @@ const kLoginsKey =
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
import { import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
MigrationUtils, import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
MigratorPrototype,
} from "resource:///modules/MigrationUtils.sys.mjs";
import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs"; import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs";
const lazy = {}; 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. * Internet Explorer profile migrator
} */
export class IEProfileMigrator extends MigratorBase {
IEProfileMigrator.prototype = Object.create(MigratorPrototype); constructor() {
super();
IEProfileMigrator.prototype.getResources = function IE_getResources() { this.wrappedJSObject = this; // export this to be able to use it in the unittest.
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);
};
IEProfileMigrator.prototype.getLastUsedDate = function IE_getLastUsedDate() { get classDescription() {
let datePromises = ["Favs", "CookD"].map(dirId => { return "IE Profile Migrator";
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));
});
};
IEProfileMigrator.prototype.classDescription = "IE Profile Migrator"; get contractID() {
IEProfileMigrator.prototype.contractID = return "@mozilla.org/profile/migrator;1?app=browser&type=ie";
"@mozilla.org/profile/migrator;1?app=browser&type=ie"; }
IEProfileMigrator.prototype.classID = Components.ID(
"{3d2532e3-4932-4774-b7ba-968f5899d3a4}" 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));
});
}
}

View File

@@ -2,22 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * 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"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {}; const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, {
BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.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", Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
@@ -44,475 +35,12 @@ function getL10n() {
return gL10n; 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<Date>}
* 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.<BROWSER_KEY>.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<MigrationResource[]>}
*/
_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 * The singleton MigrationUtils service. This service is the primary mechanism
* by which migrations from other browsers to this browser occur. The singleton * by which migrations from other browsers to this browser occur. The singleton
* instance of this class is exported from this module as `MigrationUtils`. * instance of this class is exported from this module as `MigrationUtils`.
*/ */
class MigrationUtilsSingleton { class MigrationUtils {
resourceTypes = Object.freeze({ resourceTypes = Object.freeze({
COOKIES: Ci.nsIBrowserProfileMigrator.COOKIES, COOKIES: Ci.nsIBrowserProfileMigrator.COOKIES,
HISTORY: Ci.nsIBrowserProfileMigrator.HISTORY, HISTORY: Ci.nsIBrowserProfileMigrator.HISTORY,
@@ -525,7 +53,7 @@ class MigrationUtilsSingleton {
/** /**
* Helper for implementing simple asynchronous cases of migration resources' * 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 * 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 * everything right away, you can wrap the async-function with this helper
* and not worry about notifying the callback. * and not worry about notifying the callback.
@@ -711,7 +239,7 @@ class MigrationUtilsSingleton {
* Internal name of the migration source. See `availableMigratorKeys` * Internal name of the migration source. See `availableMigratorKeys`
* for supported values by OS. * for supported values by OS.
* *
* @returns {MigratorPrototype} * @returns {MigratorBase}
* A profile migrator implementing nsIBrowserProfileMigrator, if it can * A profile migrator implementing nsIBrowserProfileMigrator, if it can
* import any data, null otherwise. * import any data, null otherwise.
*/ */
@@ -1420,4 +948,6 @@ class MigrationUtilsSingleton {
} }
} }
export const MigrationUtils = new MigrationUtilsSingleton(); const MigrationUtilsSingleton = new MigrationUtils();
export { MigrationUtilsSingleton as MigrationUtils };

View File

@@ -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[]>|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<Date>}
* 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.<BROWSER_KEY>.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<MigrationResource[]>}
*/
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];
}
}

View File

@@ -5,10 +5,8 @@
import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
import { import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
MigrationUtils, import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
MigratorPrototype,
} from "resource:///modules/MigrationUtils.sys.mjs";
const lazy = {}; const lazy = {};
@@ -344,135 +342,134 @@ SearchStrings.prototype = {
}, },
}; };
export function SafariProfileMigrator() {} /**
* Safari migrator
SafariProfileMigrator.prototype = Object.create(MigratorPrototype); */
export class SafariProfileMigrator extends MigratorBase {
SafariProfileMigrator.prototype.getResources = function SM_getResources() { get classDescription() {
let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); return "Safari Profile Migrator";
if (!profileDir.exists()) {
return null;
} }
let resources = []; get contractID() {
let pushProfileFileResource = function(aFileName, aConstructor) { return "@mozilla.org/profile/migrator;1?app=browser&type=safari";
let file = profileDir.clone(); }
file.append(aFileName);
if (file.exists()) { get classID() {
resources.push(new aConstructor(file)); 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); let resources = [];
pushProfileFileResource("Bookmarks.plist", Bookmarks); 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 pushProfileFileResource("History.plist", History);
// Mac versions of Safari. Not surprisingly, they are stored in the same pushProfileFileResource("Bookmarks.plist", Bookmarks);
// 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; // The Reading List feature was introduced at the same time in Windows and
if (prefs) { // Mac versions of Safari. Not surprisingly, they are stored in the same
resources.push(new SearchStrings(prefs)); // 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; getLastUsedDate() {
}; let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false);
let datePromises = ["Bookmarks.plist", "History.plist"].map(file => {
SafariProfileMigrator.prototype.getLastUsedDate = function SM_getLastUsedDate() { let path = OS.Path.join(profileDir.path, file);
let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); return OS.File.stat(path)
let datePromises = ["Bookmarks.plist", "History.plist"].map(file => { .catch(() => null)
let path = OS.Path.join(profileDir.path, file); .then(info => {
return OS.File.stat(path) return info ? info.lastModificationDate : 0;
.catch(() => null) });
.then(info => { });
return info ? info.lastModificationDate : 0; return Promise.all(datePromises).then(dates => {
}); return new Date(Math.max.apply(Math, dates));
}); });
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;
} }
// 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( async hasPermissions() {
win if (this._hasPermissions) {
) { return true;
// Keep prompting the user until they pick a file that grants us access, }
// or they cancel out of the file open panel. // Check if we have access:
while (!(await this.hasPermissions())) { let target = FileUtils.getDir(
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); "ULibDir",
// The title (second arg) is not displayed on macOS, so leave it blank. ["Safari", "Bookmarks.plist"],
fp.init(win, "", Ci.nsIFilePicker.modeOpen); false
// 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 try {
// directory. Anyway, let's not confuse the user: the sensible idea // 'stat' is always allowed, but reading is somehow not, if the user hasn't
// here is to ask for permissions for Bookmarks.plist, and we'll // allowed it:
// silently accept whatever input as long as we can then read the plist. await IOUtils.read(target.path, { maxBytes: 1 });
fp.appendFilter("plist", "*.plist"); this._hasPermissions = true;
fp.filterIndex = 1; return true;
fp.displayDirectory = FileUtils.getDir("ULibDir", ["Safari"], false); } catch (ex) {
// 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 false;
} }
} }
};
Object.defineProperty( async getPermissions(win) {
SafariProfileMigrator.prototype, // Keep prompting the user until they pick a file that grants us access,
"mainPreferencesPropertyList", // or they cancel out of the file open panel.
{ while (!(await this.hasPermissions())) {
get: function get_mainPreferencesPropertyList() { let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
if (this._mainPreferencesPropertyList === undefined) { // The title (second arg) is not displayed on macOS, so leave it blank.
let file = FileUtils.getDir("UsrPrfs", [], false); fp.init(win, "", Ci.nsIFilePicker.modeOpen);
if (file.exists()) { // This is a little weird. You'd expect that it matters which file
file.append("com.apple.Safari.plist"); // the user picks, but it doesn't really, as long as it's in this
if (file.exists()) { // directory. Anyway, let's not confuse the user: the sensible idea
this._mainPreferencesPropertyList = new MainPreferencesPropertyList( // here is to ask for permissions for Bookmarks.plist, and we'll
file // silently accept whatever input as long as we can then read the plist.
); fp.appendFilter("plist", "*.plist");
return this._mainPreferencesPropertyList; fp.filterIndex = 1;
} fp.displayDirectory = FileUtils.getDir("ULibDir", ["Safari"], false);
} // Now wait for the filepicker to open and close. If the user picks
this._mainPreferencesPropertyList = null; // any file in this directory, macOS will grant us read access, so
return this._mainPreferencesPropertyList; // 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"; get mainPreferencesPropertyList() {
SafariProfileMigrator.prototype.contractID = if (this._mainPreferencesPropertyList === undefined) {
"@mozilla.org/profile/migrator;1?app=browser&type=safari"; let file = FileUtils.getDir("UsrPrfs", [], false);
SafariProfileMigrator.prototype.classID = Components.ID( if (file.exists()) {
"{4b609ecf-60b2-4655-9df4-dc149e474da1}" 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;
}
}

View File

@@ -24,6 +24,7 @@ EXTRA_JS_MODULES += [
"ChromeProfileMigrator.sys.mjs", "ChromeProfileMigrator.sys.mjs",
"FirefoxProfileMigrator.sys.mjs", "FirefoxProfileMigrator.sys.mjs",
"MigrationUtils.sys.mjs", "MigrationUtils.sys.mjs",
"MigratorBase.sys.mjs",
"ProfileMigrator.sys.mjs", "ProfileMigrator.sys.mjs",
] ]

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
var { MigrationUtils, MigratorPrototype } = ChromeUtils.importESModule( var { MigrationUtils } = ChromeUtils.importESModule(
"resource:///modules/MigrationUtils.sys.mjs" "resource:///modules/MigrationUtils.sys.mjs"
); );
var { LoginHelper } = ChromeUtils.import( var { LoginHelper } = ChromeUtils.import(
@@ -42,7 +42,7 @@ updateAppInfo();
/** /**
* Migrates the requested resource and waits for the migration to be complete. * 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. * The migrator being used to migrate the data.
* @param {number} resourceType * @param {number} resourceType
* This is a bitfield with bits from nsIBrowserProfileMigrator flipped to indicate what * This is a bitfield with bits from nsIBrowserProfileMigrator flipped to indicate what