Bug 1893722: Recover StoreID from prefs in the event of a profiles.ini reset. r=jhirsch
Differential Revision: https://phabricator.services.mozilla.com/D226806
This commit is contained in:
@@ -91,6 +91,7 @@ class SelectableProfileServiceClass {
|
||||
#asyncShutdownBlocker = null;
|
||||
#initialized = false;
|
||||
#groupToolkitProfile = null;
|
||||
#storeID = null;
|
||||
#currentProfile = null;
|
||||
#everyWindowCallbackId = "SelectableProfileService";
|
||||
#defaultAvatars = [
|
||||
@@ -101,6 +102,7 @@ class SelectableProfileServiceClass {
|
||||
"shopping",
|
||||
"star",
|
||||
];
|
||||
#initPromise = null;
|
||||
static #dirSvc = null;
|
||||
|
||||
constructor() {
|
||||
@@ -111,6 +113,23 @@ class SelectableProfileServiceClass {
|
||||
|
||||
this.#groupToolkitProfile =
|
||||
this.#profileService.currentProfile ?? this.#profileService.groupProfile;
|
||||
|
||||
this.#storeID = this.#groupToolkitProfile?.storeID;
|
||||
|
||||
if (!this.#storeID) {
|
||||
this.#storeID = Services.prefs.getCharPref(
|
||||
"toolkit.profiles.storeID",
|
||||
""
|
||||
);
|
||||
if (this.#storeID) {
|
||||
// This can happen if profiles.ini has been reset by a version of Firefox prior to 67 and
|
||||
// the current profile is not the current default for the group. We can recover by
|
||||
// attempting to find the group profile from the database.
|
||||
this.#initPromise = this.restoreStoreID()
|
||||
.catch(console.error)
|
||||
.finally(() => (this.#initPromise = null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,6 +186,10 @@ class SelectableProfileServiceClass {
|
||||
}
|
||||
}
|
||||
|
||||
get storeID() {
|
||||
return this.#storeID;
|
||||
}
|
||||
|
||||
get groupToolkitProfile() {
|
||||
return this.#groupToolkitProfile;
|
||||
}
|
||||
@@ -188,7 +211,7 @@ class SelectableProfileServiceClass {
|
||||
}
|
||||
|
||||
async maybeCreateProfilesStorePath() {
|
||||
if (!this.#groupToolkitProfile || this.#groupToolkitProfile.storeID) {
|
||||
if (this.storeID) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,6 +225,7 @@ class SelectableProfileServiceClass {
|
||||
.replace("{", "")
|
||||
.split("-")[0];
|
||||
this.#groupToolkitProfile.storeID = storageID;
|
||||
this.#storeID = storageID;
|
||||
await this.#attemptFlushProfileService();
|
||||
}
|
||||
|
||||
@@ -210,22 +234,34 @@ class SelectableProfileServiceClass {
|
||||
|
||||
return PathUtils.join(
|
||||
SelectableProfileServiceClass.PROFILE_GROUPS_DIR,
|
||||
`${this.#groupToolkitProfile.storeID}.sqlite`
|
||||
`${this.storeID}.sqlite`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* At startup, store the nsToolkitProfile for the group.
|
||||
* Get the groupDBPath from the nsToolkitProfile, and connect to it.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async init() {
|
||||
init() {
|
||||
if (!this.#initPromise) {
|
||||
this.#initPromise = this.#init().finally(
|
||||
() => (this.#initPromise = null)
|
||||
);
|
||||
}
|
||||
|
||||
return this.#initPromise;
|
||||
}
|
||||
|
||||
async #init() {
|
||||
if (this.#initialized || !this.groupToolkitProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the storeID doesn't exist, we don't want to create the db until we
|
||||
// need to so we early return.
|
||||
if (!this.groupToolkitProfile.storeID) {
|
||||
if (!this.storeID) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -347,6 +383,33 @@ class SelectableProfileServiceClass {
|
||||
}
|
||||
}
|
||||
|
||||
async restoreStoreID() {
|
||||
try {
|
||||
await this.#init();
|
||||
|
||||
for (let profile of await this.getAllProfiles()) {
|
||||
let groupProfile = this.#profileService.getProfileByDir(
|
||||
await profile.rootDir
|
||||
);
|
||||
|
||||
if (groupProfile) {
|
||||
this.#groupToolkitProfile = groupProfile;
|
||||
this.#groupToolkitProfile.storeID = this.storeID;
|
||||
await this.#profileService.asyncFlush();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// If we were unable to find a matching toolkit profile then assume the
|
||||
// store ID is bogus so clear it and uninit.
|
||||
this.#storeID = null;
|
||||
await this.uninit();
|
||||
Services.prefs.clearUserPref("toolkit.profiles.storeID");
|
||||
}
|
||||
|
||||
async handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "activate": {
|
||||
@@ -422,11 +485,12 @@ class SelectableProfileServiceClass {
|
||||
* and vacuum the group DB.
|
||||
*/
|
||||
async deleteProfileGroup() {
|
||||
if (this.getAllProfiles().length) {
|
||||
if ((await this.getAllProfiles()).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#groupToolkitProfile.storeID = null;
|
||||
this.#storeID = null;
|
||||
await this.#attemptFlushProfileService();
|
||||
await this.vacuumAndCloseGroupDB();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,18 @@ async function initSelectableProfileService() {
|
||||
await SelectableProfileService.maybeSetupDataStore();
|
||||
}
|
||||
|
||||
function getRelativeProfilePath(path) {
|
||||
let relativePath = path.getRelativePath(
|
||||
Services.dirsvc.get("UAppData", Ci.nsIFile)
|
||||
);
|
||||
|
||||
if (AppConstants.platform === "win") {
|
||||
relativePath = relativePath.replace("/", "\\");
|
||||
}
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
async function createTestProfile(profileData = {}) {
|
||||
const SelectableProfileService = getSelectableProfileService();
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => !AppConstants.MOZ_SELECTABLE_PROFILES,
|
||||
},
|
||||
async function test_recover_storeID() {
|
||||
startProfileService();
|
||||
Services.prefs.setCharPref("toolkit.profiles.storeID", "foobar");
|
||||
|
||||
const SelectableProfileService = getSelectableProfileService();
|
||||
await SelectableProfileService.init();
|
||||
Assert.ok(
|
||||
!SelectableProfileService.initialized,
|
||||
"Didn't initialize the service"
|
||||
);
|
||||
|
||||
let profile = SelectableProfileService.currentProfile;
|
||||
Assert.ok(!profile, "Should not have a current profile");
|
||||
Assert.equal(
|
||||
getProfileService().currentProfile.storeID,
|
||||
null,
|
||||
"Should not have updated the store ID on the profile"
|
||||
);
|
||||
|
||||
Assert.ok(
|
||||
!Services.prefs.prefHasUserValue("toolkit.profiles.storeID"),
|
||||
"Should have cleared the storeID pref"
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Sqlite } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Sqlite.sys.mjs"
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => !AppConstants.MOZ_SELECTABLE_PROFILES,
|
||||
},
|
||||
async function test_recover_storeID() {
|
||||
startProfileService();
|
||||
Services.prefs.setCharPref("toolkit.profiles.storeID", "foobar");
|
||||
|
||||
// The database needs to exist already
|
||||
let groupsPath = PathUtils.join(
|
||||
Services.dirsvc.get("UAppData", Ci.nsIFile).path,
|
||||
"Profile Groups"
|
||||
);
|
||||
|
||||
await IOUtils.makeDirectory(groupsPath);
|
||||
let dbFile = PathUtils.join(groupsPath, "foobar.sqlite");
|
||||
let db = await Sqlite.openConnection({
|
||||
path: dbFile,
|
||||
openNotExclusive: true,
|
||||
});
|
||||
|
||||
let path = getRelativeProfilePath(
|
||||
getProfileService().currentProfile.rootDir
|
||||
);
|
||||
|
||||
// Slightly annoying we have to replicate this...
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS "Profiles" (
|
||||
id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
avatar TEXT NOT NULL,
|
||||
themeL10nId TEXT NOT NULL,
|
||||
themeFg TEXT NOT NULL,
|
||||
themeBg TEXT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);`);
|
||||
await db.execute(
|
||||
`INSERT INTO Profiles VALUES (NULL, :path, :name, :avatar, :themeL10nId, :themeFg, :themeBg);`,
|
||||
{
|
||||
path,
|
||||
name: "Fake Profile",
|
||||
avatar: "book",
|
||||
themeL10nId: "default",
|
||||
themeFg: "",
|
||||
themeBg: "",
|
||||
}
|
||||
);
|
||||
|
||||
await db.close();
|
||||
|
||||
const SelectableProfileService = getSelectableProfileService();
|
||||
await SelectableProfileService.init();
|
||||
Assert.ok(
|
||||
SelectableProfileService.initialized,
|
||||
"Did initialize the service"
|
||||
);
|
||||
|
||||
let profile = SelectableProfileService.currentProfile;
|
||||
Assert.ok(profile, "Should have a current profile");
|
||||
Assert.equal(profile.name, "Fake Profile");
|
||||
Assert.equal(
|
||||
getProfileService().currentProfile.storeID,
|
||||
"foobar",
|
||||
"Should have updated the store ID on the profile"
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -2,6 +2,10 @@
|
||||
head = "../../../../../toolkit/profile/test/xpcshell/head.js head.js"
|
||||
firefox-appdir = "browser"
|
||||
|
||||
["test_fail_recover_storeID.js"]
|
||||
|
||||
["test_recover_storeID.js"]
|
||||
|
||||
["test_selectable_profile_launch.js"]
|
||||
|
||||
["test_selectable_profile_service_exists.js"]
|
||||
|
||||
@@ -103,6 +103,14 @@ interface nsIToolkitProfileService : nsISupports
|
||||
*/
|
||||
nsIToolkitProfile getProfileByName(in AUTF8String aName);
|
||||
|
||||
/**
|
||||
* Get a profile by directory. Finds a profile with the matching root directory
|
||||
*
|
||||
* @param aRootDir The root directory to match against.
|
||||
* @param aLocalDir And optional local directory to also match against.
|
||||
*/
|
||||
nsIToolkitProfile getProfileByDir(in nsIFile aRootDir, [optional] in nsIFile aLocalDir);
|
||||
|
||||
/**
|
||||
* Create a new profile.
|
||||
*
|
||||
|
||||
@@ -80,6 +80,7 @@ using namespace mozilla;
|
||||
#define PROFILE_DB_VERSION "2"
|
||||
#define INSTALL_PREFIX "Install"
|
||||
#define INSTALL_PREFIX_LENGTH 7
|
||||
#define STORE_ID_PREF "toolkit.profiles.storeID"
|
||||
|
||||
struct KeyValue {
|
||||
KeyValue(const char* aKey, const char* aValue) : key(aKey), value(aValue) {}
|
||||
@@ -357,32 +358,35 @@ nsToolkitProfile::SetStoreID(const nsACString& aStoreID) {
|
||||
mSection.get(), "StoreID", PromiseFlatCString(aStoreID).get());
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = prefs->SetCharPref("toolkit.profiles.storeID", aStoreID);
|
||||
rv = nsToolkitProfileService::gService->mProfileDB.SetString(
|
||||
mSection.get(), "ShowSelector", mShowProfileSelector ? "1" : "0");
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
nsToolkitProfileService::gService->mGroupProfile = this;
|
||||
} else {
|
||||
rv = nsToolkitProfileService::gService->mProfileDB.DeleteString(
|
||||
mSection.get(), "StoreID");
|
||||
if (nsToolkitProfileService::gService->mCurrent == this) {
|
||||
rv = prefs->SetCharPref(STORE_ID_PREF, aStoreID);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// If the string was not present in the ini file, just ignore the error.
|
||||
if (rv == NS_ERROR_FAILURE) {
|
||||
rv = NS_OK;
|
||||
nsToolkitProfileService::gService->mGroupProfile = this;
|
||||
}
|
||||
} else {
|
||||
// If the string was not present in the ini file, just ignore the error.
|
||||
nsToolkitProfileService::gService->mProfileDB.DeleteString(mSection.get(),
|
||||
"StoreID");
|
||||
|
||||
// We need a StoreID to show the profile selector, so if StoreID has been
|
||||
// removed, then remove ShowSelector also.
|
||||
mShowProfileSelector = false;
|
||||
rv = nsToolkitProfileService::gService->mProfileDB.DeleteString(
|
||||
mSection.get(), "ShowSelector");
|
||||
if (rv == NS_ERROR_FAILURE) {
|
||||
rv = NS_OK;
|
||||
|
||||
// If the string was not present in the ini file, just ignore the error.
|
||||
nsToolkitProfileService::gService->mProfileDB.DeleteString(mSection.get(),
|
||||
"ShowSelector");
|
||||
|
||||
if (nsToolkitProfileService::gService->mCurrent == this) {
|
||||
rv = prefs->ClearUserPref(STORE_ID_PREF);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
nsToolkitProfileService::gService->mGroupProfile = nullptr;
|
||||
}
|
||||
|
||||
rv = prefs->ClearUserPref("toolkit.profiles.storeID");
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
nsToolkitProfileService::gService->mGroupProfile = nullptr;
|
||||
}
|
||||
mStoreID = aStoreID;
|
||||
|
||||
@@ -670,13 +674,15 @@ void nsToolkitProfileService::CompleteStartup() {
|
||||
glean::startup::profile_database_version.Set(mStartupFileVersion);
|
||||
glean::startup::profile_count.Set(static_cast<uint32_t>(mProfiles.length()));
|
||||
|
||||
nsresult rv;
|
||||
bool needsFlush = false;
|
||||
|
||||
// If we started into an unmanaged profile in a profile group, set the group
|
||||
// profile to be the managed profile belonging to the group.
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
|
||||
if (!mCurrent) {
|
||||
nsCString storeID;
|
||||
rv = prefs->GetCharPref("toolkit.profiles.storeID", storeID);
|
||||
rv = prefs->GetCharPref(STORE_ID_PREF, storeID);
|
||||
if (NS_SUCCEEDED(rv) && !storeID.IsEmpty()) {
|
||||
mGroupProfile = GetProfileByStoreID(storeID);
|
||||
}
|
||||
@@ -685,32 +691,44 @@ void nsToolkitProfileService::CompleteStartup() {
|
||||
// profile for some group.
|
||||
if (!mCurrent->mStoreID.IsVoid()) {
|
||||
mGroupProfile = mCurrent;
|
||||
rv = prefs->SetCharPref("toolkit.profiles.storeID", mCurrent->mStoreID);
|
||||
rv = prefs->SetCharPref(STORE_ID_PREF, mCurrent->mStoreID);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
} else {
|
||||
// Otherwise if the current profile has a store ID set in prefs but not in
|
||||
// the database then restore it. This can happen if a version of Firefox
|
||||
// prior to 67 has overwritten the database.
|
||||
nsCString storeID;
|
||||
rv = prefs->GetCharPref(STORE_ID_PREF, storeID);
|
||||
if (NS_SUCCEEDED(rv) && !storeID.IsEmpty()) {
|
||||
rv = mCurrent->SetStoreID(storeID);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
needsFlush = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMaybeLockProfile) {
|
||||
nsCOMPtr<nsIToolkitShellService> shell =
|
||||
do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID);
|
||||
if (!shell) {
|
||||
return;
|
||||
if (shell) {
|
||||
bool isDefaultApp;
|
||||
rv = shell->IsDefaultApplication(&isDefaultApp);
|
||||
if (NS_SUCCEEDED(rv) && isDefaultApp) {
|
||||
mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
|
||||
|
||||
needsFlush = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isDefaultApp;
|
||||
nsresult rv = shell->IsDefaultApplication(&isDefaultApp);
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
|
||||
if (isDefaultApp) {
|
||||
mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
|
||||
|
||||
// There is a very small chance that this could fail if something else
|
||||
// overwrote the profiles database since we started up, probably less than
|
||||
// a second ago. There isn't really a sane response here, all the other
|
||||
// profile changes are already flushed so whether we fail to flush here or
|
||||
// force quit the app makes no difference.
|
||||
NS_ENSURE_SUCCESS_VOID(Flush());
|
||||
}
|
||||
if (needsFlush) {
|
||||
// There is a very small chance that this could fail if something else
|
||||
// overwrote the profiles database since we started up, probably less than
|
||||
// a second ago. There isn't really a sane response here, all the other
|
||||
// profile changes are already flushed so whether we fail to flush here or
|
||||
// force quit the app makes no difference.
|
||||
NS_ENSURE_SUCCESS_VOID(Flush());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2077,6 +2095,16 @@ void nsToolkitProfileService::GetProfileByDir(nsIFile* aRootDir,
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsToolkitProfileService::GetProfileByDir(nsIFile* aRootDir, nsIFile* aLocalDir,
|
||||
nsIToolkitProfile** aResult) {
|
||||
RefPtr<nsToolkitProfile> result;
|
||||
GetProfileByDir(aRootDir, aLocalDir, getter_AddRefs(result));
|
||||
result.forget(aResult);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult NS_LockProfilePath(nsIFile* aPath, nsIFile* aTempPath,
|
||||
nsIProfileUnlocker** aUnlocker,
|
||||
nsIProfileLock** aResult) {
|
||||
|
||||
42
toolkit/profile/test/xpcshell/test_restore_storeID.js
Normal file
42
toolkit/profile/test/xpcshell/test_restore_storeID.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*
|
||||
* Tests that the StoreID is restored if available in prefs.
|
||||
*/
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => !AppConstants.MOZ_SELECTABLE_PROFILES,
|
||||
},
|
||||
async () => {
|
||||
let hash = xreDirProvider.getInstallHash();
|
||||
let defaultProfile = makeRandomProfileDir("default");
|
||||
let profilesIni = {
|
||||
profiles: [
|
||||
{
|
||||
name: "default",
|
||||
path: defaultProfile.leafName,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
installs: {
|
||||
[hash]: {
|
||||
default: defaultProfile.leafName,
|
||||
},
|
||||
},
|
||||
};
|
||||
writeProfilesIni(profilesIni);
|
||||
|
||||
Services.prefs.setCharPref("toolkit.profiles.storeID", "bishbashbosh");
|
||||
|
||||
let service = getProfileService();
|
||||
let { profile } = selectStartupProfile();
|
||||
|
||||
Assert.equal(profile.rootDir.path, defaultProfile.path);
|
||||
Assert.equal(service.currentProfile, profile);
|
||||
Assert.equal(service.groupProfile, profile);
|
||||
Assert.equal(profile.storeID, "bishbashbosh");
|
||||
|
||||
checkProfileService();
|
||||
}
|
||||
);
|
||||
@@ -40,6 +40,8 @@ skip-if = ["os == 'android'"]
|
||||
|
||||
["test_remove_default.js"]
|
||||
|
||||
["test_restore_storeID.js"]
|
||||
|
||||
["test_select_backgroundtasks_ephemeral.js"]
|
||||
|
||||
["test_select_backgroundtasks_not_ephemeral_create.js"]
|
||||
|
||||
Reference in New Issue
Block a user