/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { NetUtil } = ChromeUtils.importESModule(
"resource://gre/modules/NetUtil.sys.mjs"
);
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const NS_ERROR_START_PROFILE_MANAGER = 0x805800c9;
const UPDATE_CHANNEL = AppConstants.MOZ_UPDATE_CHANNEL;
let gProfD = do_get_profile();
let gDataHome = gProfD.clone();
gDataHome.append(
"data");
gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let gDataHomeLocal = gProfD.clone();
gDataHomeLocal.append(
"local");
gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let xreDirProvider = Cc[
"@mozilla.org/xre/directory-provider;1"].getService(
Ci.nsIXREDirProvider
);
xreDirProvider.setUserDataDirectory(gDataHome,
false);
xreDirProvider.setUserDataDirectory(gDataHomeLocal,
true);
Services.dirsvc.set(
"UAppData", gDataHome);
let gProfilesRoot = gDataHome.clone();
let gProfilesTemp = gDataHomeLocal.clone();
if (!AppConstants.XP_UNIX || AppConstants.platform ==
"macosx") {
gProfilesRoot.append(
"Profiles");
gProfilesTemp.append(
"Profiles");
}
Services.dirsvc.set(
"DefProfRt", gProfilesRoot);
Services.dirsvc.set(
"DefProfLRt", gProfilesTemp);
let gIsDefaultApp =
false;
const ShellService = {
register() {
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
let factory = {
createInstance(iid) {
return ShellService.QueryInterface(iid);
},
};
registrar.registerFactory(
this.ID,
"ToolkitShellService",
this.CONTRACT,
factory
);
},
isDefaultApplication() {
return gIsDefaultApp;
},
QueryInterface: ChromeUtils.generateQI([
"nsIToolkitShellService"]),
ID: Components.ID(
"{ce724e0c-ed70-41c9-ab31-1033b0b591be}"),
CONTRACT:
"@mozilla.org/toolkit/shell-service;1",
};
ShellService.register();
let gIsLegacy =
false;
function enableLegacyProfiles() {
Services.env.set(
"MOZ_LEGACY_PROFILES",
"1");
gIsLegacy =
true;
}
function getProfileService() {
return Cc[
"@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
}
let PROFILE_DEFAULT =
"default";
let DEDICATED_NAME = `default-${UPDATE_CHANNEL}`;
if (AppConstants.MOZ_DEV_EDITION) {
DEDICATED_NAME = PROFILE_DEFAULT =
"dev-edition-default";
}
// Shared data for backgroundtasks tests.
const BACKGROUNDTASKS_PROFILE_DATA = (() => {
let hash = xreDirProvider.getInstallHash();
let profileData = {
options: {
startWithLastProfile:
true,
},
profiles: [
{
name:
"Profile1",
path:
"Path1",
storeID:
null,
default:
false,
},
{
name:
"Profile3",
path:
"Path3",
storeID:
null,
default:
false,
},
],
installs: {
[hash]: {
default:
"Path1",
},
},
backgroundTasksProfiles: [
{
name: `MozillaBackgroundTask-${hash}-unrelated_task`,
path: `saltsalt.MozillaBackgroundTask-${hash}-unrelated_task`,
},
],
};
return profileData;
})();
/**
* Creates a random profile path for use.
*/
function makeRandomProfileDir(name) {
let file = gDataHome.clone();
file.append(name);
file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
return file;
}
/**
* A wrapper around nsIToolkitProfileService.selectStartupProfile to make it
* a bit nicer to use from JS.
*/
function selectStartupProfile(args = [], isResetting =
false, legacyHash =
"") {
let service = getProfileService();
let rootDir = {};
let localDir = {};
let profile = {};
let didCreate = service.selectStartupProfile(
[
"xpcshell", ...args],
isResetting,
UPDATE_CHANNEL,
legacyHash,
rootDir,
localDir,
profile
);
if (profile.value) {
Assert.ok(
rootDir.value.equals(profile.value.rootDir),
"Should have matched the root dir."
);
Assert.ok(
localDir.value.equals(profile.value.localDir),
"Should have matched the local dir."
);
Assert.ok(
service.currentProfile === profile.value,
"Should have marked the profile as the current profile."
);
}
else {
Assert.ok(!service.currentProfile,
"Should be no current profile.");
}
return {
rootDir: rootDir.value,
localDir: localDir.value,
profile: profile.value,
didCreate,
};
}
function testStartsProfileManager(args = [], isResetting =
false) {
try {
selectStartupProfile(args, isResetting);
Assert.ok(
false,
"Should have started the profile manager");
checkStartupReason();
}
catch (e) {
Assert.equal(
e.result,
NS_ERROR_START_PROFILE_MANAGER,
"Should have started the profile manager"
);
}
}
function safeGet(ini, section, key) {
try {
return ini.getString(section, key);
}
catch (e) {
return null;
}
}
/**
* Writes a compatibility.ini file that marks the give profile directory as last
* used by the given install path.
*/
function writeCompatibilityIni(
dir,
appDir = FileUtils.getDir(
"CurProcD", []),
greDir = FileUtils.getDir(
"GreD", [])
) {
let target = dir.clone();
target.append(
"compatibility.ini");
let factory = Cc[
"@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter);
// The profile service doesn't care about these so just use fixed values
ini.setString(
"Compatibility",
"LastVersion",
"64.0a1_20180919123806/20180919123806"
);
ini.setString(
"Compatibility",
"LastOSABI",
"Darwin_x86_64-gcc3");
ini.setString(
"Compatibility",
"LastPlatformDir",
greDir.persistentDescriptor
);
ini.setString(
"Compatibility",
"LastAppDir", appDir.persistentDescriptor);
ini.writeFile(target);
}
/**
* Writes a profiles.ini based on the passed profile data.
* profileData should contain two properties, options and profiles.
* options contains a single property, startWithLastProfile.
* profiles is an array of profiles each containing name, path and default
* properties.
*/
function writeProfilesIni(profileData) {
let target = gDataHome.clone();
target.append(
"profiles.ini");
let factory = Cc[
"@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter);
const {
options = {},
profiles = [],
installs =
null,
backgroundTasksProfiles =
null,
} = profileData;
let { startWithLastProfile =
true } = options;
ini.setString(
"General",
"StartWithLastProfile",
startWithLastProfile ?
"1" :
"0"
);
for (let i = 0; i < profiles.length; i++) {
let profile = profiles[i];
let section = `Profile${i}`;
ini.setString(section,
"Name", profile.name);
ini.setString(section,
"IsRelative", 1);
ini.setString(section,
"Path", profile.path);
if (
"storeID" in profile) {
ini.setString(section,
"StoreID", profile.storeID);
}
if (profile.
default) {
ini.setString(section,
"Default",
"1");
}
}
if (backgroundTasksProfiles) {
let section =
"BackgroundTasksProfiles";
for (let backgroundTasksProfile of backgroundTasksProfiles) {
ini.setString(
section,
backgroundTasksProfile.name,
backgroundTasksProfile.path
);
}
}
if (installs) {
ini.setString(
"General",
"Version",
"2");
for (let hash of Object.keys(installs)) {
ini.setString(`Install${hash}`,
"Default", installs[hash].
default);
if (
"locked" in installs[hash]) {
ini.setString(
`Install${hash}`,
"Locked",
installs[hash].locked ?
"1" :
"0"
);
}
}
writeInstallsIni({ installs });
}
else {
writeInstallsIni(
null);
}
ini.writeFile(target);
}
/**
* Reads the existing profiles.ini into the same structure as that accepted by
* writeProfilesIni above. The profiles property is sorted according to name
* because the order is irrelevant and it makes testing easier if we can make
* that assumption.
*/
function readProfilesIni() {
let target = gDataHome.clone();
target.append(
"profiles.ini");
let profileData = {
options: {
startWithLastProfile:
true,
},
profiles: [],
installs:
null,
};
if (!target.exists()) {
return profileData;
}
let factory = Cc[
"@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser(target);
profileData.options.startWithLastProfile =
safeGet(ini,
"General",
"StartWithLastProfile") ==
"1";
if (safeGet(ini,
"General",
"Version") ==
"2") {
profileData.installs = {};
}
let sections = ini.getSections();
while (sections.hasMore()) {
let section = sections.getNext();
if (section ==
"General") {
continue;
}
if (section.startsWith(
"Profile")) {
let isRelative = safeGet(ini, section,
"IsRelative");
if (isRelative ===
null) {
break;
}
Assert.equal(
isRelative,
"1",
"Paths should always be relative in these tests."
);
let profile = {
name: safeGet(ini, section,
"Name"),
path: safeGet(ini, section,
"Path"),
// TODO: currently, if there's a StoreID key but no value, this gets
// translated into JS as an empty string, while if there's no StoreID
// in the file at all, then it gets translated into JS as null.
// Work around this in the tests by converting empty strings to nulls,
// since otherwise some tests fail strict object comparisons.
storeID: safeGet(ini, section,
"StoreID") ||
null,
};
try {
profile.
default = ini.getString(section,
"Default") ==
"1";
Assert.ok(
profile.
default,
"The Default value is only written when true."
);
}
catch (e) {
profile.
default =
false;
}
profileData.profiles.push(profile);
}
if (section.startsWith(
"Install")) {
Assert.ok(
profileData.installs,
"Should only see an install section if the ini version was correct."
);
profileData.installs[section.substring(7)] = {
default: safeGet(ini, section,
"Default"),
};
let locked = safeGet(ini, section,
"Locked");
if (locked !==
null) {
profileData.installs[section.substring(7)].locked = locked;
}
}
if (section ==
"BackgroundTasksProfiles") {
profileData.backgroundTasksProfiles = [];
let backgroundTasksProfiles = ini.getKeys(section);
while (backgroundTasksProfiles.hasMore()) {
let name = backgroundTasksProfiles.getNext();
let path = ini.getString(section, name);
profileData.backgroundTasksProfiles.push({ name, path });
}
profileData.backgroundTasksProfiles.sort((a, b) =>
a.name.localeCompare(b.name)
);
}
}
profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
return profileData;
}
/**
* Writes an installs.ini based on the supplied data. Should be an object with
* keys for every installation hash each mapping to an object. Each object
* should have a default property for the relative path to the profile.
*/
function writeInstallsIni(installData) {
let target = gDataHome.clone();
target.append(
"installs.ini");
if (!installData) {
try {
target.remove(
false);
}
catch (e) {}
return;
}
const { installs = {} } = installData;
let factory = Cc[
"@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser(
null).QueryInterface(Ci.nsIINIParserWriter);
for (let hash of Object.keys(installs)) {
ini.setString(hash,
"Default", installs[hash].
default);
if (
"locked" in installs[hash]) {
ini.setString(hash,
"Locked", installs[hash].locked ?
"1" :
"0");
}
}
ini.writeFile(target);
}
/**
* Reads installs.ini into a structure like that used in the above function.
*/
function readInstallsIni() {
let target = gDataHome.clone();
target.append(
"installs.ini");
let installData = {
installs: {},
};
if (!target.exists()) {
return installData;
}
let factory = Cc[
"@mozilla.org/xpcom/ini-parser-factory;1"].getService(
Ci.nsIINIParserFactory
);
let ini = factory.createINIParser(target);
let sections = ini.getSections();
while (sections.hasMore()) {
let hash = sections.getNext();
if (hash !=
"General") {
installData.installs[hash] = {
default: safeGet(ini, hash,
"Default"),
};
let locked = safeGet(ini, hash,
"Locked");
if (locked !==
null) {
installData.installs[hash].locked = locked;
}
}
}
return installData;
}
/**
* Check that the backup data in installs.ini matches the install data in
* profiles.ini.
*/
function checkBackup(
profileData = readProfilesIni(),
installData = readInstallsIni()
) {
if (!profileData.installs) {
// If the profiles db isn't of the right version we wouldn't expect the
// backup to be accurate.
return;
}
Assert.deepEqual(
profileData.installs,
installData.installs,
"Backup installs.ini should match installs in profiles.ini"
);
}
/**
* Checks that the profile service seems to have the right data in it compared
* to profile and install data structured as in the above functions.
*/
function checkProfileService(
profileData = readProfilesIni(),
verifyBackup =
true
) {
let service = getProfileService();
let expectedStartWithLast =
true;
if (
"options" in profileData) {
expectedStartWithLast = profileData.options.startWithLastProfile;
}
Assert.equal(
service.startWithLastProfile,
expectedStartWithLast,
"Start with last profile should match."
);
let serviceProfiles = Array.from(service.profiles);
Assert.equal(
serviceProfiles.length,
profileData.profiles.length,
"Should be the same number of profiles."
);
// Sort to make matching easy.
serviceProfiles.sort((a, b) => a.name.localeCompare(b.name));
profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
let hash = xreDirProvider.getInstallHash();
let defaultPath =
profileData.installs && hash in profileData.installs
? profileData.installs[hash].
default
:
null;
let dedicatedProfile =
null;
let legacyProfile =
null;
for (let i = 0; i < serviceProfiles.length; i++) {
let serviceProfile = serviceProfiles[i];
let expectedProfile = profileData.profiles[i];
Assert.equal(
serviceProfile.name,
expectedProfile.name,
"Should have the same name."
);
let expectedPath = Cc[
"@mozilla.org/file/local;1"].createInstance(
Ci.nsIFile
);
expectedPath.setRelativeDescriptor(gDataHome, expectedProfile.path);
Assert.equal(
serviceProfile.rootDir.path,
expectedPath.path,
"Should have the same path."
);
// The StoreID is null if not present on the serviceProfile, so be sure
// we convert a possible missing storeID on expectedProfile from
// undefined to null.
Assert.equal(
serviceProfile.storeID,
expectedProfile.storeID ||
null,
"Should have the same (possibly null) StoreID."
);
if (expectedProfile.path == defaultPath) {
dedicatedProfile = serviceProfile;
}
if (AppConstants.MOZ_DEV_EDITION) {
if (expectedProfile.name == PROFILE_DEFAULT) {
legacyProfile = serviceProfile;
}
}
else if (expectedProfile.
default) {
legacyProfile = serviceProfile;
}
}
if (gIsLegacy || Services.env.get(
"SNAP_NAME")) {
Assert.equal(
service.defaultProfile,
legacyProfile,
"Should have seen the right profile selected."
);
}
else {
Assert.equal(
service.defaultProfile,
dedicatedProfile,
"Should have seen the right profile selected."
);
}
if (verifyBackup) {
checkBackup(profileData);
}
}
// Maps the interesting scalar IDs to simple names that can be used as JS variables.
const SCALARS = {
selectionReason:
"startup.profile_selection_reason",
databaseVersion:
"startup.profile_database_version",
profileCount:
"startup.profile_count",
};
function getTelemetryScalars() {
let scalars = TelemetryTestUtils.getProcessScalars(
"parent");
let results = {};
for (let [prop, scalarId] of Object.entries(SCALARS)) {
results[prop] = scalars[scalarId];
}
return results;
}
function checkStartupReason(expected = undefined) {
let { selectionReason } = getTelemetryScalars();
Assert.equal(
selectionReason,
expected,
"Should have seen the right startup reason."
);
}