summaryrefslogtreecommitdiffstats
path: root/toolkit/profile
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/profile/ProfileUnlockerAndroid.cpp37
-rw-r--r--toolkit/profile/ProfileUnlockerAndroid.h26
-rw-r--r--toolkit/profile/ProfileUnlockerWin.cpp245
-rw-r--r--toolkit/profile/ProfileUnlockerWin.h56
-rw-r--r--toolkit/profile/content/createProfileWizard.js266
-rw-r--r--toolkit/profile/content/createProfileWizard.xhtml90
-rw-r--r--toolkit/profile/content/profileDowngrade.js39
-rw-r--r--toolkit/profile/content/profileDowngrade.xhtml42
-rw-r--r--toolkit/profile/content/profileSelection.js355
-rw-r--r--toolkit/profile/content/profileSelection.xhtml104
-rw-r--r--toolkit/profile/gtest/TestProfileLock.cpp35
-rw-r--r--toolkit/profile/gtest/TestProfileLockRetry.cpp74
-rw-r--r--toolkit/profile/gtest/moz.build20
-rw-r--r--toolkit/profile/jar.mn13
-rw-r--r--toolkit/profile/moz.build49
-rw-r--r--toolkit/profile/notifications.txt52
-rw-r--r--toolkit/profile/nsIProfileMigrator.idl69
-rw-r--r--toolkit/profile/nsIProfileUnlocker.idl21
-rw-r--r--toolkit/profile/nsIToolkitProfile.idl99
-rw-r--r--toolkit/profile/nsIToolkitProfileService.idl147
-rw-r--r--toolkit/profile/nsProfileLock.cpp578
-rw-r--r--toolkit/profile/nsProfileLock.h106
-rw-r--r--toolkit/profile/nsToolkitProfileService.cpp2240
-rw-r--r--toolkit/profile/nsToolkitProfileService.h180
-rw-r--r--toolkit/profile/test/chrome.toml3
-rw-r--r--toolkit/profile/test/test_create_profile.xhtml126
-rw-r--r--toolkit/profile/xpcshell/head.js623
-rw-r--r--toolkit/profile/xpcshell/test_check_backup.js56
-rw-r--r--toolkit/profile/xpcshell/test_claim_locked.js68
-rw-r--r--toolkit/profile/xpcshell/test_clean.js165
-rw-r--r--toolkit/profile/xpcshell/test_conflict_installs.js40
-rw-r--r--toolkit/profile/xpcshell/test_conflict_profiles.js57
-rw-r--r--toolkit/profile/xpcshell/test_create_default.js52
-rw-r--r--toolkit/profile/xpcshell/test_fix_directory_case.js113
-rw-r--r--toolkit/profile/xpcshell/test_ignore_legacy_directory.js129
-rw-r--r--toolkit/profile/xpcshell/test_invalid_descriptor.js55
-rw-r--r--toolkit/profile/xpcshell/test_legacy_empty.js28
-rw-r--r--toolkit/profile/xpcshell/test_legacy_select.js67
-rw-r--r--toolkit/profile/xpcshell/test_lock.js72
-rw-r--r--toolkit/profile/xpcshell/test_missing_profilesini.js67
-rw-r--r--toolkit/profile/xpcshell/test_new_default.js129
-rw-r--r--toolkit/profile/xpcshell/test_previous_dedicated.js62
-rw-r--r--toolkit/profile/xpcshell/test_profile_reset.js59
-rw-r--r--toolkit/profile/xpcshell/test_remove.js103
-rw-r--r--toolkit/profile/xpcshell/test_remove_default.js79
-rw-r--r--toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js30
-rw-r--r--toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js78
-rw-r--r--toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js66
-rw-r--r--toolkit/profile/xpcshell/test_select_default.js71
-rw-r--r--toolkit/profile/xpcshell/test_select_environment.js45
-rw-r--r--toolkit/profile/xpcshell/test_select_environment_named.js61
-rw-r--r--toolkit/profile/xpcshell/test_select_missing.js34
-rw-r--r--toolkit/profile/xpcshell/test_select_named.js43
-rw-r--r--toolkit/profile/xpcshell/test_select_noname.js35
-rw-r--r--toolkit/profile/xpcshell/test_select_profile_argument.js50
-rw-r--r--toolkit/profile/xpcshell/test_select_profile_argument_new.js43
-rw-r--r--toolkit/profile/xpcshell/test_select_profilemanager.js34
-rw-r--r--toolkit/profile/xpcshell/test_single_profile_selected.js73
-rw-r--r--toolkit/profile/xpcshell/test_single_profile_unselected.js74
-rw-r--r--toolkit/profile/xpcshell/test_skip_locked_environment.js131
-rw-r--r--toolkit/profile/xpcshell/test_snap.js65
-rw-r--r--toolkit/profile/xpcshell/test_snap_empty.js26
-rw-r--r--toolkit/profile/xpcshell/test_snatch_environment.js101
-rw-r--r--toolkit/profile/xpcshell/test_snatch_environment_default.js101
-rw-r--r--toolkit/profile/xpcshell/test_startswithlast.js28
-rw-r--r--toolkit/profile/xpcshell/test_steal_inuse.js77
-rw-r--r--toolkit/profile/xpcshell/test_update_selected_dedicated.js80
-rw-r--r--toolkit/profile/xpcshell/test_update_unknown_dedicated.js85
-rw-r--r--toolkit/profile/xpcshell/test_update_unselected_dedicated.js89
-rw-r--r--toolkit/profile/xpcshell/test_use_dedicated.js107
-rw-r--r--toolkit/profile/xpcshell/xpcshell.toml97
71 files changed, 8820 insertions, 0 deletions
diff --git a/toolkit/profile/ProfileUnlockerAndroid.cpp b/toolkit/profile/ProfileUnlockerAndroid.cpp
new file mode 100644
index 0000000000..58836c7b8e
--- /dev/null
+++ b/toolkit/profile/ProfileUnlockerAndroid.cpp
@@ -0,0 +1,37 @@
+/* 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/. */
+
+#include "ProfileUnlockerAndroid.h"
+#include "nsPrintfCString.h"
+#include <unistd.h>
+
+namespace mozilla {
+
+ProfileUnlockerAndroid::ProfileUnlockerAndroid(const pid_t aPid) : mPid(aPid) {}
+
+ProfileUnlockerAndroid::~ProfileUnlockerAndroid() {}
+
+NS_IMPL_ISUPPORTS(ProfileUnlockerAndroid, nsIProfileUnlocker)
+
+NS_IMETHODIMP
+ProfileUnlockerAndroid::Unlock(uint32_t aSeverity) {
+ if (aSeverity != FORCE_QUIT) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_WARNING(nsPrintfCString("Process %d has the profile "
+ "lock, will try to kill it.",
+ mPid)
+ .get());
+ if (mPid == getpid()) {
+ NS_ERROR("Lock held by current process !?");
+ return NS_ERROR_FAILURE;
+ }
+ if (kill(mPid, SIGKILL) != 0) {
+ NS_WARNING("Could not kill process.");
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/profile/ProfileUnlockerAndroid.h b/toolkit/profile/ProfileUnlockerAndroid.h
new file mode 100644
index 0000000000..a8dc33bcb5
--- /dev/null
+++ b/toolkit/profile/ProfileUnlockerAndroid.h
@@ -0,0 +1,26 @@
+/* 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/. */
+
+#ifndef ProfileUnlockerAndroid_h
+#define ProfileUnlockerAndroid_h
+
+#include "nsIProfileUnlocker.h"
+
+namespace mozilla {
+
+class ProfileUnlockerAndroid final : public nsIProfileUnlocker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROFILEUNLOCKER
+
+ explicit ProfileUnlockerAndroid(const pid_t aPid);
+
+ private:
+ ~ProfileUnlockerAndroid();
+ pid_t mPid;
+};
+
+} // namespace mozilla
+
+#endif // ProfileUnlockerAndroid_h
diff --git a/toolkit/profile/ProfileUnlockerWin.cpp b/toolkit/profile/ProfileUnlockerWin.cpp
new file mode 100644
index 0000000000..d7ab5e0a30
--- /dev/null
+++ b/toolkit/profile/ProfileUnlockerWin.cpp
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ProfileUnlockerWin.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsTArray.h"
+#include "nsXPCOM.h"
+
+namespace mozilla {
+
+/**
+ * RAII class to obtain and manage a handle to a Restart Manager session.
+ * It opens a new handle upon construction and releases it upon destruction.
+ */
+class MOZ_STACK_CLASS ScopedRestartManagerSession {
+ public:
+ explicit ScopedRestartManagerSession(ProfileUnlockerWin& aUnlocker)
+ : mError(ERROR_INVALID_HANDLE),
+ mHandle((DWORD)-1) // 0 is a valid restart manager handle
+ ,
+ mUnlocker(aUnlocker) {
+ mError = mUnlocker.StartSession(mHandle);
+ }
+
+ ~ScopedRestartManagerSession() {
+ if (mError == ERROR_SUCCESS) {
+ mUnlocker.EndSession(mHandle);
+ }
+ }
+
+ /**
+ * @return true if the handle is a valid Restart Ranager handle.
+ */
+ inline bool ok() { return mError == ERROR_SUCCESS; }
+
+ /**
+ * @return the Restart Manager handle to pass to other Restart Manager APIs.
+ */
+ inline DWORD handle() { return mHandle; }
+
+ private:
+ DWORD mError;
+ DWORD mHandle;
+ ProfileUnlockerWin& mUnlocker;
+};
+
+ProfileUnlockerWin::ProfileUnlockerWin(const nsAString& aFileName)
+ : mRmStartSession(nullptr),
+ mRmRegisterResources(nullptr),
+ mRmGetList(nullptr),
+ mRmEndSession(nullptr),
+ mFileName(aFileName) {}
+
+ProfileUnlockerWin::~ProfileUnlockerWin() {}
+
+NS_IMPL_ISUPPORTS(ProfileUnlockerWin, nsIProfileUnlocker)
+
+nsresult ProfileUnlockerWin::Init() {
+ MOZ_ASSERT(!mRestartMgrModule);
+ if (mFileName.IsEmpty()) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ nsModuleHandle module(::LoadLibraryW(L"Rstrtmgr.dll"));
+ if (!module) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mRmStartSession = reinterpret_cast<RMSTARTSESSION>(
+ ::GetProcAddress(module, "RmStartSession"));
+ if (!mRmStartSession) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mRmRegisterResources = reinterpret_cast<RMREGISTERRESOURCES>(
+ ::GetProcAddress(module, "RmRegisterResources"));
+ if (!mRmRegisterResources) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mRmGetList =
+ reinterpret_cast<RMGETLIST>(::GetProcAddress(module, "RmGetList"));
+ if (!mRmGetList) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mRmEndSession =
+ reinterpret_cast<RMENDSESSION>(::GetProcAddress(module, "RmEndSession"));
+ if (!mRmEndSession) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mRestartMgrModule.steal(module);
+ return NS_OK;
+}
+
+DWORD
+ProfileUnlockerWin::StartSession(DWORD& aHandle) {
+ WCHAR sessionKey[CCH_RM_SESSION_KEY + 1] = {0};
+ return mRmStartSession(&aHandle, 0, sessionKey);
+}
+
+void ProfileUnlockerWin::EndSession(DWORD aHandle) { mRmEndSession(aHandle); }
+
+NS_IMETHODIMP
+ProfileUnlockerWin::Unlock(uint32_t aSeverity) {
+ if (!mRestartMgrModule) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (aSeverity != FORCE_QUIT) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ ScopedRestartManagerSession session(*this);
+ if (!session.ok()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ LPCWSTR resources[] = {mFileName.get()};
+ DWORD error = mRmRegisterResources(session.handle(), 1, resources, 0, nullptr,
+ 0, nullptr);
+ if (error != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Using a AutoTArray here because we expect the required size to be 1.
+ AutoTArray<RM_PROCESS_INFO, 1> info;
+ UINT numEntries;
+ UINT numEntriesNeeded = 1;
+ error = ERROR_MORE_DATA;
+ DWORD reason = RmRebootReasonNone;
+ while (error == ERROR_MORE_DATA) {
+ info.SetLength(numEntriesNeeded);
+ numEntries = numEntriesNeeded;
+ error = mRmGetList(session.handle(), &numEntriesNeeded, &numEntries,
+ &info[0], &reason);
+ }
+ if (error != ERROR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+ if (numEntries == 0) {
+ // Nobody else is locking the file; the other process must have terminated
+ return NS_OK;
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+ for (UINT i = 0; i < numEntries; ++i) {
+ rv = TryToTerminate(info[i].Process);
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+ }
+
+ // If nothing could be unlocked then we return the error code of the last
+ // failure that was returned.
+ return rv;
+}
+
+nsresult ProfileUnlockerWin::TryToTerminate(RM_UNIQUE_PROCESS& aProcess) {
+ // Subtle: If the target process terminated before this call to OpenProcess,
+ // this call will still succeed. This is because the restart manager session
+ // internally retains a handle to the target process. The rules for Windows
+ // PIDs state that the PID of a terminated process remains valid as long as
+ // at least one handle to that process remains open, so when we reach this
+ // point the PID is still valid and the process will open successfully.
+ DWORD accessRights = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE;
+ nsAutoHandle otherProcess(
+ ::OpenProcess(accessRights, FALSE, aProcess.dwProcessId));
+ if (!otherProcess) {
+ return NS_ERROR_FAILURE;
+ }
+
+ FILETIME creationTime, exitTime, kernelTime, userTime;
+ if (!::GetProcessTimes(otherProcess, &creationTime, &exitTime, &kernelTime,
+ &userTime)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (::CompareFileTime(&aProcess.ProcessStartTime, &creationTime)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ WCHAR imageName[MAX_PATH];
+ DWORD imageNameLen = MAX_PATH;
+ if (!::QueryFullProcessImageNameW(otherProcess, 0, imageName,
+ &imageNameLen)) {
+ // The error codes for this function are not very descriptive. There are
+ // actually two failure cases here: Either the call failed because the
+ // process is no longer running, or it failed for some other reason. We
+ // need to know which case that is.
+ DWORD otherProcessExitCode;
+ if (!::GetExitCodeProcess(otherProcess, &otherProcessExitCode) ||
+ otherProcessExitCode == STILL_ACTIVE) {
+ // The other process is still running.
+ return NS_ERROR_FAILURE;
+ }
+ // The other process must have terminated. We should return NS_OK so that
+ // this process may proceed with startup.
+ return NS_OK;
+ }
+ nsCOMPtr<nsIFile> otherProcessImageName;
+ if (NS_FAILED(NS_NewLocalFile(nsDependentString(imageName, imageNameLen),
+ false,
+ getter_AddRefs(otherProcessImageName)))) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoString otherProcessLeafName;
+ if (NS_FAILED(otherProcessImageName->GetLeafName(otherProcessLeafName))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ imageNameLen = MAX_PATH;
+ if (!::QueryFullProcessImageNameW(::GetCurrentProcess(), 0, imageName,
+ &imageNameLen)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIFile> thisProcessImageName;
+ if (NS_FAILED(NS_NewLocalFile(nsDependentString(imageName, imageNameLen),
+ false, getter_AddRefs(thisProcessImageName)))) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoString thisProcessLeafName;
+ if (NS_FAILED(thisProcessImageName->GetLeafName(thisProcessLeafName))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure the image leaf names match
+ if (_wcsicmp(otherProcessLeafName.get(), thisProcessLeafName.get())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We know that another process holds the lock and that it shares the same
+ // image name as our process. Let's kill it.
+ // Subtle: TerminateProcess returning ERROR_ACCESS_DENIED is actually an
+ // indicator that the target process managed to shut down on its own. In that
+ // case we should return NS_OK since we may proceed with startup.
+ if (!::TerminateProcess(otherProcess, 1) &&
+ ::GetLastError() != ERROR_ACCESS_DENIED) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/profile/ProfileUnlockerWin.h b/toolkit/profile/ProfileUnlockerWin.h
new file mode 100644
index 0000000000..6429301394
--- /dev/null
+++ b/toolkit/profile/ProfileUnlockerWin.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef ProfileUnlockerWin_h
+#define ProfileUnlockerWin_h
+
+#include <windows.h>
+#include <restartmanager.h>
+
+#include "nsIProfileUnlocker.h"
+#include "nsString.h"
+#include "nsWindowsHelpers.h"
+
+namespace mozilla {
+
+class ProfileUnlockerWin final : public nsIProfileUnlocker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROFILEUNLOCKER
+
+ explicit ProfileUnlockerWin(const nsAString& aFileName);
+
+ nsresult Init();
+
+ DWORD StartSession(DWORD& aHandle);
+ void EndSession(DWORD aHandle);
+
+ private:
+ ~ProfileUnlockerWin();
+ nsresult TryToTerminate(RM_UNIQUE_PROCESS& aProcess);
+
+ private:
+ typedef DWORD(WINAPI* RMSTARTSESSION)(DWORD*, DWORD, WCHAR[]);
+ typedef DWORD(WINAPI* RMREGISTERRESOURCES)(DWORD, UINT, LPCWSTR[], UINT,
+ RM_UNIQUE_PROCESS[], UINT,
+ LPCWSTR[]);
+ typedef DWORD(WINAPI* RMGETLIST)(DWORD, UINT*, UINT*, RM_PROCESS_INFO[],
+ LPDWORD);
+ typedef DWORD(WINAPI* RMENDSESSION)(DWORD);
+
+ private:
+ nsModuleHandle mRestartMgrModule;
+ RMSTARTSESSION mRmStartSession;
+ RMREGISTERRESOURCES mRmRegisterResources;
+ RMGETLIST mRmGetList;
+ RMENDSESSION mRmEndSession;
+
+ nsString mFileName;
+};
+
+} // namespace mozilla
+
+#endif // ProfileUnlockerWin_h
diff --git a/toolkit/profile/content/createProfileWizard.js b/toolkit/profile/content/createProfileWizard.js
new file mode 100644
index 0000000000..9e87fb4220
--- /dev/null
+++ b/toolkit/profile/content/createProfileWizard.js
@@ -0,0 +1,266 @@
+/* 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 C = Cc;
+const I = Ci;
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const ToolkitProfileService = "@mozilla.org/toolkit/profile-service;1";
+
+var gProfileService;
+var gProfileManagerBundle;
+
+var gDefaultProfileParent;
+
+// The directory where the profile will be created.
+var gProfileRoot;
+
+// Text node to display the location and name of the profile to create.
+var gProfileDisplay;
+
+// Called once when the wizard is opened.
+function initWizard() {
+ try {
+ gProfileService = C[ToolkitProfileService].getService(
+ I.nsIToolkitProfileService
+ );
+ gProfileManagerBundle = document.getElementById("bundle_profileManager");
+
+ gDefaultProfileParent = Services.dirsvc.get("DefProfRt", I.nsIFile);
+
+ // Initialize the profile location display.
+ gProfileDisplay = document.getElementById("profileDisplay").firstChild;
+ document.addEventListener("wizardfinish", onFinish);
+ document
+ .getElementById("explanation")
+ .addEventListener("pageshow", enableNextButton);
+ document
+ .getElementById("createProfile")
+ .addEventListener("pageshow", initSecondWizardPage);
+ setDisplayToDefaultFolder();
+ } catch (e) {
+ window.close();
+ throw e;
+ }
+}
+
+// Called every time the second wizard page is displayed.
+function initSecondWizardPage() {
+ var profileName = document.getElementById("profileName");
+ profileName.select();
+ profileName.focus();
+
+ // Initialize profile name validation.
+ checkCurrentInput(profileName.value);
+}
+
+const kSaltTable = [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+ "q",
+ "r",
+ "s",
+ "t",
+ "u",
+ "v",
+ "w",
+ "x",
+ "y",
+ "z",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0",
+];
+
+var kSaltString = "";
+for (var i = 0; i < 8; ++i) {
+ kSaltString += kSaltTable[Math.floor(Math.random() * kSaltTable.length)];
+}
+
+function saltName(aName) {
+ return kSaltString + "." + aName;
+}
+
+function setDisplayToDefaultFolder() {
+ var defaultProfileDir = gDefaultProfileParent.clone();
+ defaultProfileDir.append(
+ saltName(document.getElementById("profileName").value)
+ );
+ gProfileRoot = defaultProfileDir;
+ document.getElementById("useDefault").disabled = true;
+}
+
+function updateProfileDisplay() {
+ gProfileDisplay.data = gProfileRoot.path;
+}
+
+// Invoke a folder selection dialog for choosing the directory of profile storage.
+function chooseProfileFolder() {
+ var newProfileRoot;
+
+ var dirChooser = C["@mozilla.org/filepicker;1"].createInstance(
+ I.nsIFilePicker
+ );
+ dirChooser.init(
+ window,
+ gProfileManagerBundle.getString("chooseFolder"),
+ I.nsIFilePicker.modeGetFolder
+ );
+ dirChooser.appendFilters(I.nsIFilePicker.filterAll);
+
+ // default to the Profiles folder
+ dirChooser.displayDirectory = gDefaultProfileParent;
+
+ dirChooser.open(() => {
+ newProfileRoot = dirChooser.file;
+
+ // Disable the "Default Folder..." button when the default profile folder
+ // was selected manually in the File Picker.
+ document.getElementById("useDefault").disabled =
+ newProfileRoot.parent.equals(gDefaultProfileParent);
+
+ gProfileRoot = newProfileRoot;
+ updateProfileDisplay();
+ });
+}
+
+// Checks the current user input for validity and triggers an error message accordingly.
+function checkCurrentInput(currentInput) {
+ let wizard = document.querySelector("wizard");
+ var finishButton = wizard.getButton("finish");
+ var finishText = document.getElementById("finishText");
+ var canAdvance;
+
+ var errorMessage = checkProfileName(currentInput);
+
+ if (!errorMessage) {
+ finishText.className = "";
+ if (AppConstants.platform == "macosx") {
+ finishText.firstChild.data = gProfileManagerBundle.getString(
+ "profileFinishTextMac"
+ );
+ } else {
+ finishText.firstChild.data =
+ gProfileManagerBundle.getString("profileFinishText");
+ }
+ canAdvance = true;
+ } else {
+ finishText.className = "error";
+ finishText.firstChild.data = errorMessage;
+ canAdvance = false;
+ }
+
+ wizard.canAdvance = canAdvance;
+ finishButton.disabled = !canAdvance;
+
+ updateProfileDisplay();
+
+ return canAdvance;
+}
+
+function updateProfileName(aNewName) {
+ if (checkCurrentInput(aNewName)) {
+ gProfileRoot.leafName = saltName(aNewName);
+ updateProfileDisplay();
+ }
+}
+
+// Checks whether the given string is a valid profile name.
+// Returns an error message describing the error in the name or "" when it's valid.
+function checkProfileName(profileNameToCheck) {
+ // Check for emtpy profile name.
+ if (!/\S/.test(profileNameToCheck)) {
+ return gProfileManagerBundle.getString("profileNameEmpty");
+ }
+
+ // Check whether all characters in the profile name are allowed.
+ if (/([\\*:?<>|\/\"])/.test(profileNameToCheck)) {
+ return gProfileManagerBundle.getFormattedString("invalidChar", [RegExp.$1]);
+ }
+
+ // Check whether a profile with the same name already exists.
+ if (profileExists(profileNameToCheck)) {
+ return gProfileManagerBundle.getString("profileExists");
+ }
+
+ // profileNameToCheck is valid.
+ return "";
+}
+
+function profileExists(aName) {
+ for (let profile of gProfileService.profiles) {
+ if (profile.name.toLowerCase() == aName.toLowerCase()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Called when the first wizard page is shown.
+function enableNextButton() {
+ document.querySelector("wizard").canAdvance = true;
+}
+
+function onFinish(event) {
+ var profileName = document.getElementById("profileName").value;
+ var profile;
+
+ // Create profile named profileName in profileRoot.
+ try {
+ profile = gProfileService.createProfile(gProfileRoot, profileName);
+ } catch (e) {
+ var profileCreationFailed = gProfileManagerBundle.getString(
+ "profileCreationFailed"
+ );
+ var profileCreationFailedTitle = gProfileManagerBundle.getString(
+ "profileCreationFailedTitle"
+ );
+ Services.prompt.alert(
+ window,
+ profileCreationFailedTitle,
+ profileCreationFailed + "\n" + e
+ );
+
+ event.preventDefault();
+ return;
+ }
+
+ if (window.arguments && window.arguments[1]) {
+ // Add new profile to the list in the Profile Manager.
+ window.arguments[1].CreateProfile(profile);
+ } else {
+ // Use the newly created Profile.
+ var profileLock = profile.lock(null);
+
+ var dialogParams = window.arguments[0].QueryInterface(
+ I.nsIDialogParamBlock
+ );
+ dialogParams.objects.insertElementAt(profileLock, 0);
+ }
+}
diff --git a/toolkit/profile/content/createProfileWizard.xhtml b/toolkit/profile/content/createProfileWizard.xhtml
new file mode 100644
index 0000000000..dc984a0432
--- /dev/null
+++ b/toolkit/profile/content/createProfileWizard.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE wizard>
+
+<window
+ id="createProfileWizard"
+ data-l10n-id="create-profile-window2"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="initWizard();"
+ data-l10n-attrs="style"
+>
+ <linkset>
+ <html:link rel="stylesheet" href="chrome://global/skin/global.css" />
+
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="toolkit/global/createProfileWizard.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://global/content/customElements.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://mozapps/content/profile/createProfileWizard.js" />
+
+ <wizard>
+ <stringbundle
+ id="bundle_profileManager"
+ src="chrome://mozapps/locale/profile/profileSelection.properties"
+ />
+
+ <wizardpage
+ id="explanation"
+ data-header-label-id="create-profile-first-page-header2"
+ >
+ <description data-l10n-id="profile-creation-explanation-1"></description>
+ <description data-l10n-id="profile-creation-explanation-2"></description>
+ <description data-l10n-id="profile-creation-explanation-3"></description>
+ <spacer flex="1" />
+ <description data-l10n-id="profile-creation-explanation-4"></description>
+ </wizardpage>
+
+ <wizardpage
+ id="createProfile"
+ data-header-label-id="create-profile-last-page-header2"
+ >
+ <description data-l10n-id="profile-creation-intro"></description>
+
+ <label data-l10n-id="profile-prompt" control="ProfileName"></label>
+ <html:input
+ id="profileName"
+ data-l10n-id="profile-default-name"
+ data-l10n-attrs="value"
+ oninput="updateProfileName(this.value);"
+ />
+
+ <separator />
+
+ <description data-l10n-id="profile-directory-explanation"></description>
+
+ <vbox class="indent" flex="1" style="overflow: auto">
+ <description id="profileDisplay">*</description>
+ </vbox>
+
+ <hbox>
+ <button
+ data-l10n-id="create-profile-choose-folder"
+ oncommand="chooseProfileFolder();"
+ />
+
+ <button
+ id="useDefault"
+ data-l10n-id="create-profile-use-default"
+ oncommand="setDisplayToDefaultFolder(); updateProfileDisplay();"
+ disabled="true"
+ />
+ </hbox>
+
+ <separator />
+
+ <description id="finishText">*</description>
+ </wizardpage>
+ </wizard>
+</window>
diff --git a/toolkit/profile/content/profileDowngrade.js b/toolkit/profile/content/profileDowngrade.js
new file mode 100644
index 0000000000..ba1f723ec2
--- /dev/null
+++ b/toolkit/profile/content/profileDowngrade.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+let gParams;
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function init() {
+ /*
+ * The C++ code passes a dialog param block using its integers as in and out
+ * arguments for this UI. The following are the uses of the integers:
+ *
+ * 0: A set of flags from nsIToolkitProfileService.downgradeUIFlags.
+ * 1: A return argument, one of nsIToolkitProfileService.downgradeUIChoice.
+ */
+ gParams = window.arguments[0].QueryInterface(Ci.nsIDialogParamBlock);
+ if (AppConstants.MOZ_SERVICES_SYNC) {
+ let hasSync = gParams.GetInt(0) & Ci.nsIToolkitProfileService.hasSync;
+
+ document.getElementById("sync").hidden = !hasSync;
+ document.getElementById("nosync").hidden = hasSync;
+ }
+
+ document.addEventListener("dialogextra1", createProfile);
+ document.addEventListener("dialogaccept", quit);
+ document.addEventListener("dialogcancel", quit);
+}
+
+function quit() {
+ gParams.SetInt(1, Ci.nsIToolkitProfileService.quit);
+}
+
+function createProfile() {
+ gParams.SetInt(1, Ci.nsIToolkitProfileService.createNewProfile);
+ window.close();
+}
diff --git a/toolkit/profile/content/profileDowngrade.xhtml b/toolkit/profile/content/profileDowngrade.xhtml
new file mode 100644
index 0000000000..71cb0b613c
--- /dev/null
+++ b/toolkit/profile/content/profileDowngrade.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE window>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ prefwidth="min-width"
+ data-l10n-id="profiledowngrade-window2"
+ data-l10n-attrs="title,style"
+ onload="init()">
+<linkset>
+ <html:link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <html:link
+ rel="stylesheet"
+ href="chrome://mozapps/skin/profileDowngrade.css"
+ />
+
+ <html:link rel="localization" href="branding/brand.ftl"/>
+ <html:link rel="localization" href="toolkit/global/profileDowngrade.ftl"/>
+</linkset>
+<dialog buttons="accept,extra1" buttonpack="end"
+ buttonidextra1="profiledowngrade-window-create"
+ buttonidaccept="profiledowngrade-quit">
+
+ <script src="profileDowngrade.js"/>
+ <script src="chrome://global/content/customElements.js"/>
+
+ <hbox flex="1" align="start">
+ <image id="info" role="presentation"/>
+ <vbox flex="1">
+ <description data-l10n-id="profiledowngrade-nosync"></description>
+#ifdef MOZ_SERVICES_SYNC
+ <description data-l10n-id="profiledowngrade-sync2"></description>
+#endif
+ </vbox>
+ </hbox>
+
+</dialog>
+</window>
diff --git a/toolkit/profile/content/profileSelection.js b/toolkit/profile/content/profileSelection.js
new file mode 100644
index 0000000000..b5bbfd5ab1
--- /dev/null
+++ b/toolkit/profile/content/profileSelection.js
@@ -0,0 +1,355 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ *
+ * 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const C = Cc;
+const I = Ci;
+
+const ToolkitProfileService = "@mozilla.org/toolkit/profile-service;1";
+
+const fluentStrings = new Localization([
+ "branding/brand.ftl",
+ "toolkit/global/profileSelection.ftl",
+]);
+
+var gDialogParams;
+var gProfileManagerBundle;
+var gBrandBundle;
+var gProfileService;
+var gNeedsFlush = false;
+
+function getFluentString(str) {
+ return fluentStrings.formatValue(str);
+}
+
+function startup() {
+ try {
+ gDialogParams = window.arguments[0].QueryInterface(I.nsIDialogParamBlock);
+
+ gProfileService = C[ToolkitProfileService].getService(
+ I.nsIToolkitProfileService
+ );
+
+ gProfileManagerBundle = document.getElementById("bundle_profileManager");
+ gBrandBundle = document.getElementById("bundle_brand");
+
+ document.getElementById("profileWindow").centerWindowOnScreen();
+
+ var profilesElement = document.getElementById("profiles");
+
+ for (let profile of gProfileService.profiles.entries(I.nsIToolkitProfile)) {
+ var listitem = profilesElement.appendItem(profile.name, "");
+
+ var tooltiptext = gProfileManagerBundle.getFormattedString(
+ "profileTooltip",
+ [profile.name, profile.rootDir.path]
+ );
+ listitem.setAttribute("tooltiptext", tooltiptext);
+ listitem.profile = profile;
+ try {
+ if (profile === gProfileService.defaultProfile) {
+ setTimeout(
+ function (a) {
+ profilesElement.ensureElementIsVisible(a);
+ profilesElement.selectItem(a);
+ },
+ 0,
+ listitem
+ );
+ }
+ } catch (e) {}
+ }
+
+ var autoSelectLastProfile = document.getElementById(
+ "autoSelectLastProfile"
+ );
+ autoSelectLastProfile.checked = gProfileService.startWithLastProfile;
+ profilesElement.focus();
+ } catch (e) {
+ window.close();
+ throw e;
+ }
+ document.addEventListener("dialogaccept", acceptDialog);
+ document.addEventListener("dialogcancel", exitDialog);
+}
+
+async function flush(cancelled) {
+ updateStartupPrefs();
+
+ gDialogParams.SetInt(
+ 1,
+ document.getElementById("offlineState").checked ? 1 : 0
+ );
+
+ if (gNeedsFlush) {
+ try {
+ gProfileService.flush();
+ } catch (e) {
+ let appName = gBrandBundle.getString("brandShortName");
+
+ let title = gProfileManagerBundle.getString("flushFailTitle");
+ let restartButton = gProfileManagerBundle.getFormattedString(
+ "flushFailRestartButton",
+ [appName]
+ );
+ let exitButton = gProfileManagerBundle.getString("flushFailExitButton");
+
+ let message;
+ if (e.result == undefined) {
+ message = await getFluentString("profile-selection-conflict-message");
+ } else {
+ message = gProfileManagerBundle.getString("flushFailMessage");
+ }
+
+ const PS = Ci.nsIPromptService;
+ let result = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ PS.BUTTON_POS_0 * PS.BUTTON_TITLE_IS_STRING +
+ PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING,
+ restartButton,
+ exitButton,
+ null,
+ null,
+ {}
+ );
+
+ gDialogParams.SetInt(
+ 0,
+ result == 0
+ ? Ci.nsIToolkitProfileService.restart
+ : Ci.nsIToolkitProfileService.exit
+ );
+ return;
+ }
+ gNeedsFlush = false;
+ }
+
+ gDialogParams.SetInt(
+ 0,
+ cancelled
+ ? Ci.nsIToolkitProfileService.exit
+ : Ci.nsIToolkitProfileService.launchWithProfile
+ );
+}
+
+function acceptDialog(event) {
+ var appName = gBrandBundle.getString("brandShortName");
+
+ var profilesElement = document.getElementById("profiles");
+ var selectedProfile = profilesElement.selectedItem;
+ if (!selectedProfile) {
+ var pleaseSelectTitle =
+ gProfileManagerBundle.getString("pleaseSelectTitle");
+ var pleaseSelect = gProfileManagerBundle.getFormattedString(
+ "pleaseSelect",
+ [appName]
+ );
+ Services.prompt.alert(window, pleaseSelectTitle, pleaseSelect);
+ event.preventDefault();
+ return;
+ }
+
+ gDialogParams.objects.insertElementAt(selectedProfile.profile.rootDir, 0);
+ gDialogParams.objects.insertElementAt(selectedProfile.profile.localDir, 1);
+
+ if (gProfileService.defaultProfile != selectedProfile.profile) {
+ try {
+ gProfileService.defaultProfile = selectedProfile.profile;
+ gNeedsFlush = true;
+ } catch (e) {
+ // This can happen on dev-edition. We'll still restart with the selected
+ // profile based on the lock's directories.
+ }
+ }
+ flush(false);
+}
+
+function exitDialog() {
+ flush(true);
+}
+
+function updateStartupPrefs() {
+ var autoSelectLastProfile = document.getElementById("autoSelectLastProfile");
+ if (gProfileService.startWithLastProfile != autoSelectLastProfile.checked) {
+ gProfileService.startWithLastProfile = autoSelectLastProfile.checked;
+ gNeedsFlush = true;
+ }
+}
+
+// handle key event on listboxes
+function onProfilesKey(aEvent) {
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ if (AppConstants.platform != "macosx") {
+ break;
+ }
+ // fall through
+ case KeyEvent.DOM_VK_DELETE:
+ ConfirmDelete();
+ break;
+ case KeyEvent.DOM_VK_F2:
+ RenameProfile();
+ break;
+ }
+}
+
+function onProfilesDblClick(aEvent) {
+ if (aEvent.target.closest("richlistitem")) {
+ document.getElementById("profileWindow").acceptDialog();
+ }
+}
+
+// invoke the createProfile Wizard
+function CreateProfileWizard() {
+ window.openDialog(
+ "chrome://mozapps/content/profile/createProfileWizard.xhtml",
+ "",
+ "centerscreen,chrome,modal,titlebar",
+ gProfileService,
+ { CreateProfile }
+ );
+}
+
+/**
+ * Called from createProfileWizard to update the display.
+ */
+function CreateProfile(aProfile) {
+ var profilesElement = document.getElementById("profiles");
+
+ var listitem = profilesElement.appendItem(aProfile.name, "");
+
+ var tooltiptext = gProfileManagerBundle.getFormattedString("profileTooltip", [
+ aProfile.name,
+ aProfile.rootDir.path,
+ ]);
+ listitem.setAttribute("tooltiptext", tooltiptext);
+ listitem.profile = aProfile;
+
+ profilesElement.ensureElementIsVisible(listitem);
+ profilesElement.selectItem(listitem);
+
+ gNeedsFlush = true;
+}
+
+// rename the selected profile
+function RenameProfile() {
+ var profilesElement = document.getElementById("profiles");
+ var selectedItem = profilesElement.selectedItem;
+ if (!selectedItem) {
+ return false;
+ }
+
+ var selectedProfile = selectedItem.profile;
+
+ var oldName = selectedProfile.name;
+ var newName = { value: oldName };
+
+ var dialogTitle = gProfileManagerBundle.getString("renameProfileTitle");
+ var msg = gProfileManagerBundle.getFormattedString("renameProfilePrompt", [
+ oldName,
+ ]);
+
+ if (
+ Services.prompt.prompt(window, dialogTitle, msg, newName, null, {
+ value: 0,
+ })
+ ) {
+ newName = newName.value;
+
+ // User hasn't changed the profile name. Treat as if cancel was pressed.
+ if (newName == oldName) {
+ return false;
+ }
+
+ try {
+ selectedProfile.name = newName;
+ gNeedsFlush = true;
+ } catch (e) {
+ var alTitle = gProfileManagerBundle.getString("profileNameInvalidTitle");
+ var alMsg = gProfileManagerBundle.getFormattedString(
+ "profileNameInvalid",
+ [newName]
+ );
+ Services.prompt.alert(window, alTitle, alMsg);
+ return false;
+ }
+
+ selectedItem.firstChild.setAttribute("value", newName);
+ var tiptext = gProfileManagerBundle.getFormattedString("profileTooltip", [
+ newName,
+ selectedProfile.rootDir.path,
+ ]);
+ selectedItem.setAttribute("tooltiptext", tiptext);
+
+ return true;
+ }
+
+ return false;
+}
+
+function ConfirmDelete() {
+ var profileList = document.getElementById("profiles");
+
+ var selectedItem = profileList.selectedItem;
+ if (!selectedItem) {
+ return false;
+ }
+
+ var selectedProfile = selectedItem.profile;
+ var deleteFiles = false;
+
+ if (selectedProfile.rootDir.exists()) {
+ var dialogTitle = gProfileManagerBundle.getString("deleteTitle");
+ var dialogText = gProfileManagerBundle.getFormattedString(
+ "deleteProfileConfirm",
+ [selectedProfile.rootDir.path]
+ );
+
+ var buttonPressed = Services.prompt.confirmEx(
+ window,
+ dialogTitle,
+ dialogText,
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2,
+ gProfileManagerBundle.getString("dontDeleteFiles"),
+ null,
+ gProfileManagerBundle.getString("deleteFiles"),
+ null,
+ { value: 0 }
+ );
+ if (buttonPressed == 1) {
+ return false;
+ }
+
+ if (buttonPressed == 2) {
+ deleteFiles = true;
+ }
+ }
+
+ try {
+ selectedProfile.remove(deleteFiles);
+ gNeedsFlush = true;
+ } catch (e) {
+ let title = gProfileManagerBundle.getString("profileDeletionFailedTitle");
+ let msg = gProfileManagerBundle.getString("profileDeletionFailed");
+ Services.prompt.alert(window, title, msg);
+
+ return true;
+ }
+
+ profileList.removeChild(selectedItem);
+ if (profileList.firstChild != undefined) {
+ profileList.selectItem(profileList.firstChild);
+ }
+
+ return true;
+}
diff --git a/toolkit/profile/content/profileSelection.xhtml b/toolkit/profile/content/profileSelection.xhtml
new file mode 100644
index 0000000000..201abfc259
--- /dev/null
+++ b/toolkit/profile/content/profileSelection.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: SGML; indent-tabs-mode: nil; -*- -->
+<!--
+
+ 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/. -->
+
+<!DOCTYPE window>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ class="non-resizable"
+ data-l10n-id="profile-selection-window"
+ orient="vertical"
+ prefwidth="min-width"
+ style="min-width: 30em"
+ onload="startup();"
+>
+ <dialog
+ id="profileWindow"
+ buttons="accept,cancel"
+ buttonidaccept="profile-selection-button-accept"
+ buttonidcancel="profile-selection-button-cancel"
+ >
+ <linkset>
+ <html:link
+ rel="stylesheet"
+ href="chrome://mozapps/skin/profileSelection.css"
+ />
+
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="toolkit/global/profileSelection.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://global/content/customElements.js" />
+
+ <stringbundle
+ id="bundle_profileManager"
+ src="chrome://mozapps/locale/profile/profileSelection.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+
+ <script src="chrome://mozapps/content/profile/profileSelection.js" />
+
+ <description
+ class="label"
+ data-l10n-id="profile-manager-description"
+ ></description>
+
+ <separator class="thin" />
+
+ <hbox class="indent">
+ <vbox id="managebuttons">
+ <button
+ id="newbutton"
+ data-l10n-id="profile-selection-new-button"
+ oncommand="CreateProfileWizard();"
+ />
+ <button
+ id="renbutton"
+ data-l10n-id="profile-selection-rename-button"
+ oncommand="RenameProfile();"
+ />
+ <button
+ id="delbutton"
+ data-l10n-id="profile-selection-delete-button"
+ oncommand="ConfirmDelete();"
+ />
+ </vbox>
+
+ <vbox id="profilesContainer">
+ <richlistbox
+ id="profiles"
+ class="theme-listbox"
+ seltype="single"
+ ondblclick="onProfilesDblClick(event)"
+ onkeypress="onProfilesKey(event);"
+ >
+ </richlistbox>
+
+ <!-- Bug 257777 -->
+ <checkbox
+ id="offlineState"
+ data-l10n-id="profile-manager-work-offline"
+ native="true"
+ />
+
+ <checkbox
+ id="autoSelectLastProfile"
+ data-l10n-id="profile-manager-use-selected"
+ native="true"
+ />
+ </vbox>
+ </hbox>
+ </dialog>
+</window>
diff --git a/toolkit/profile/gtest/TestProfileLock.cpp b/toolkit/profile/gtest/TestProfileLock.cpp
new file mode 100644
index 0000000000..918a41bf7d
--- /dev/null
+++ b/toolkit/profile/gtest/TestProfileLock.cpp
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsProfileLock.h"
+#include "nsString.h"
+#include "nsDirectoryServiceDefs.h"
+
+TEST(ProfileLock, BasicLock)
+{
+ char tmpExt[] = "profilebasiclocktest";
+
+ nsProfileLock myLock;
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> tmpDir;
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir));
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = tmpDir->AppendNative(nsCString(tmpExt));
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = tmpDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700);
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = myLock.Lock(tmpDir, nullptr);
+ EXPECT_EQ(NS_SUCCEEDED(rv), true);
+
+ myLock.Cleanup();
+}
diff --git a/toolkit/profile/gtest/TestProfileLockRetry.cpp b/toolkit/profile/gtest/TestProfileLockRetry.cpp
new file mode 100644
index 0000000000..8301103493
--- /dev/null
+++ b/toolkit/profile/gtest/TestProfileLockRetry.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+
+#include <sys/eventfd.h>
+#include <sched.h>
+
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsProfileLock.h"
+#include "nsString.h"
+
+TEST(ProfileLock, RetryLock)
+{
+ char templ[] = "/tmp/profilelocktest.XXXXXX";
+ char* tmpdir = mkdtemp(templ);
+ ASSERT_NE(tmpdir, nullptr);
+
+ nsProfileLock myLock;
+ nsProfileLock myLock2;
+ nsresult rv;
+ nsCOMPtr<nsIFile> dir(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ rv = dir->InitWithNativePath(nsCString(tmpdir));
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ int eventfd_fd = eventfd(0, 0);
+ ASSERT_GE(eventfd_fd, 0);
+
+ // fcntl advisory locks are per process, so it's hard
+ // to avoid using fork here.
+ pid_t childpid = fork();
+
+ if (childpid) {
+ // parent
+
+ // blocking read causing us to lose the race
+ eventfd_t value;
+ EXPECT_EQ(0, eventfd_read(eventfd_fd, &value));
+
+ rv = myLock2.Lock(dir, nullptr);
+ EXPECT_EQ(NS_ERROR_FILE_ACCESS_DENIED, rv);
+
+ // kill the child
+ EXPECT_EQ(0, kill(childpid, SIGTERM));
+
+ // reap zombie (required to collect the lock)
+ int status;
+ EXPECT_EQ(childpid, waitpid(childpid, &status, 0));
+
+ rv = myLock2.Lock(dir, nullptr);
+ EXPECT_EQ(NS_SUCCEEDED(rv), true);
+ } else {
+ // child
+ rv = myLock.Lock(dir, nullptr);
+ ASSERT_EQ(NS_SUCCEEDED(rv), true);
+
+ // unblock parent
+ EXPECT_EQ(0, eventfd_write(eventfd_fd, 1));
+
+ // parent will kill us
+ for (;;) sleep(1);
+ }
+
+ close(eventfd_fd);
+
+ myLock.Cleanup();
+ myLock2.Cleanup();
+}
diff --git a/toolkit/profile/gtest/moz.build b/toolkit/profile/gtest/moz.build
new file mode 100644
index 0000000000..d9219b4113
--- /dev/null
+++ b/toolkit/profile/gtest/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+LOCAL_INCLUDES += [
+ "..",
+]
+
+UNIFIED_SOURCES += [
+ "TestProfileLock.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "Linux" and CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestProfileLockRetry.cpp",
+ ]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/profile/jar.mn b/toolkit/profile/jar.mn
new file mode 100644
index 0000000000..4ddead64cb
--- /dev/null
+++ b/toolkit/profile/jar.mn
@@ -0,0 +1,13 @@
+# 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/.
+
+toolkit.jar:
+ content/mozapps/profile/createProfileWizard.js (content/createProfileWizard.js)
+ content/mozapps/profile/createProfileWizard.xhtml (content/createProfileWizard.xhtml)
+ content/mozapps/profile/profileSelection.js (content/profileSelection.js)
+ content/mozapps/profile/profileSelection.xhtml (content/profileSelection.xhtml)
+#ifdef MOZ_BLOCK_PROFILE_DOWNGRADE
+ content/mozapps/profile/profileDowngrade.js (content/profileDowngrade.js)
+* content/mozapps/profile/profileDowngrade.xhtml (content/profileDowngrade.xhtml)
+#endif
diff --git a/toolkit/profile/moz.build b/toolkit/profile/moz.build
new file mode 100644
index 0000000000..d898c909a3
--- /dev/null
+++ b/toolkit/profile/moz.build
@@ -0,0 +1,49 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome.toml"]
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.toml"]
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["gtest"]
+
+XPIDL_SOURCES += [
+ "nsIProfileMigrator.idl",
+ "nsIProfileUnlocker.idl",
+ "nsIToolkitProfile.idl",
+ "nsIToolkitProfileService.idl",
+]
+
+XPIDL_MODULE = "toolkitprofile"
+
+UNIFIED_SOURCES += ["nsProfileLock.cpp"]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ UNIFIED_SOURCES += ["ProfileUnlockerWin.cpp"]
+
+if CONFIG["OS_TARGET"] == "Android":
+ UNIFIED_SOURCES += ["ProfileUnlockerAndroid.cpp"]
+
+UNIFIED_SOURCES += ["nsToolkitProfileService.cpp"]
+
+LOCAL_INCLUDES += [
+ "../xre",
+]
+
+FINAL_LIBRARY = "xul"
+
+for var in ("MOZ_APP_NAME", "MOZ_APP_BASENAME"):
+ DEFINES[var] = '"%s"' % CONFIG[var]
+
+for var in ("MOZ_CREATE_LEGACY_PROFILE",):
+ if CONFIG[var]:
+ DEFINES[var] = True
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Startup and Profile System")
diff --git a/toolkit/profile/notifications.txt b/toolkit/profile/notifications.txt
new file mode 100644
index 0000000000..e6703b274c
--- /dev/null
+++ b/toolkit/profile/notifications.txt
@@ -0,0 +1,52 @@
+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/.
+
+nsIObserver topics for profile changing. Profile changing happens in phases
+in the order given below. An observer may register separately for each phase
+of the process depending on its needs.
+
+"profile-change-teardown"
+ All async activity must be stopped in this phase. Typically,
+ the application level observer will close all open windows.
+ This is the last phase in which the subject's vetoChange()
+ method may still be called.
+ The next notification will be either
+ profile-change-teardown-veto or profile-before-change.
+
+"profile-before-change"
+ Called before the profile has changed. Use this notification
+ to prepare for the profile going away. If a component is
+ holding any state which needs to be flushed to a profile-relative
+ location, it should be done here.
+
+"profile-do-change"
+ Called after the profile has changed. Do the work to
+ respond to having a new profile. Any change which
+ affects others must be done in this phase.
+
+"profile-after-change"
+ Called after the profile has changed. Use this notification
+ to make changes that are dependent on what some other listener
+ did during its profile-do-change. For example, to respond to
+ new preferences.
+
+"profile-initial-state"
+ Called after all phases of a change have completed. Typically
+ in this phase, an application level observer will open a new window.
+
+Contexts for profile changes. These are passed as the someData param to the
+observer's Observe() method.
+
+"startup"
+ Going from no profile to a profile.
+ The following topics happen in this context:
+ profile-do-change
+ profile-after-change
+
+See https://wiki.mozilla.org/XPCOM_Shutdown for more details about the shutdown
+process.
+
+NOTE: Long ago there was be a "shutdown-cleanse" version of shutdown which was
+intended to clear profile data. This is no longer sent and observer code should
+remove support for it.
diff --git a/toolkit/profile/nsIProfileMigrator.idl b/toolkit/profile/nsIProfileMigrator.idl
new file mode 100644
index 0000000000..6936ce4242
--- /dev/null
+++ b/toolkit/profile/nsIProfileMigrator.idl
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+interface nsIFile;
+
+/**
+ * Helper interface for nsIProfileMigrator.
+ *
+ * @provider Toolkit (Startup code)
+ * @client Application (Profile-migration code)
+ * @obtainable nsIProfileMigrator.migrate
+ */
+[scriptable, uuid(048e5ca1-0eb7-4bb1-a9a2-a36f7d4e0e3c)]
+interface nsIProfileStartup : nsISupports
+{
+ /**
+ * The root directory of the semi-current profile, during profile migration.
+ * After nsIProfileMigrator.migrate has returned, this object will not be
+ * useful.
+ */
+ readonly attribute nsIFile directory;
+
+ /**
+ * Do profile-startup by setting NS_APP_USER_PROFILE_50_DIR in the directory
+ * service and notifying the profile-startup observer topics.
+ */
+ void doStartup();
+};
+
+/**
+ * Migrate application settings from an outside source.
+ *
+ * @provider Application (Profile-migration code)
+ * @client Toolkit (Startup code)
+ * @obtainable service, contractid("@mozilla.org/toolkit/profile-migrator;1")
+ */
+[scriptable, uuid(3df284a5-2258-4d46-a664-761ecdc04c22)]
+interface nsIProfileMigrator : nsISupports
+{
+ /**
+ * Migrate data from an outside source, if possible. Does nothing
+ * otherwise.
+ *
+ * When this method is called, a default profile has been created;
+ * XPCOM has been initialized such that compreg.dat is in the
+ * profile; the directory service does *not* return a key for
+ * NS_APP_USER_PROFILE_50_DIR or any of the keys depending on an active
+ * profile. To figure out the directory of the "current" profile, use
+ * aStartup.directory.
+ *
+ * If your migrator needs to access services that use the profile (to
+ * set profile prefs or bookmarks, for example), use aStartup.doStartup.
+ *
+ * @param aStartup nsIProfileStartup object to use during migration.
+ * @param aKey optional key of a migrator to use to skip source selection.
+ * @param aProfileName optional name of the profile to use for migration.
+ *
+ * @note The startup code ignores COM exceptions thrown from this method.
+ */
+ void migrate(in nsIProfileStartup aStartup, in ACString aKey,
+ [optional] in AUTF8String aProfileName);
+};
+
+%{C++
+#define NS_PROFILEMIGRATOR_CONTRACTID "@mozilla.org/toolkit/profile-migrator;1"
+%}
diff --git a/toolkit/profile/nsIProfileUnlocker.idl b/toolkit/profile/nsIProfileUnlocker.idl
new file mode 100644
index 0000000000..cd1a71051f
--- /dev/null
+++ b/toolkit/profile/nsIProfileUnlocker.idl
@@ -0,0 +1,21 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(08923af1-e7a3-4fae-ba02-128502193994)]
+interface nsIProfileUnlocker : nsISupports
+{
+ const unsigned long ATTEMPT_QUIT = 0;
+ const unsigned long FORCE_QUIT = 1;
+
+ /**
+ * Try to unlock the specified profile by attempting or forcing the
+ * process that currently holds the lock to quit.
+ *
+ * @param aSeverity either ATTEMPT_QUIT or FORCE_QUIT
+ * @throws NS_ERROR_FAILURE if unlocking failed.
+ */
+ void unlock(in unsigned long aSeverity);
+};
diff --git a/toolkit/profile/nsIToolkitProfile.idl b/toolkit/profile/nsIToolkitProfile.idl
new file mode 100644
index 0000000000..b194cd9563
--- /dev/null
+++ b/toolkit/profile/nsIToolkitProfile.idl
@@ -0,0 +1,99 @@
+/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIProfileUnlocker;
+
+/**
+ * Hold on to a profile lock. Once you release the last reference to this
+ * interface, the profile lock is released.
+ */
+[scriptable, uuid(7c58c703-d245-4864-8d75-9648ca4a6139)]
+interface nsIProfileLock : nsISupports
+{
+ /**
+ * The main profile directory.
+ */
+ readonly attribute nsIFile directory;
+
+ /**
+ * A directory corresponding to the main profile directory that exists for
+ * the purpose of storing data on the local filesystem, including cache
+ * files or other data files that may not represent critical user data.
+ * (e.g., this directory may not be included as part of a backup scheme.)
+ *
+ * In some cases, this directory may just be the main profile directory.
+ */
+ readonly attribute nsIFile localDirectory;
+
+ /**
+ * The timestamp of an existing profile lock at lock time.
+ */
+ readonly attribute PRTime replacedLockTime;
+
+ /**
+ * Unlock the profile.
+ */
+ void unlock();
+};
+
+/**
+ * A interface representing a profile.
+ * @note THIS INTERFACE SHOULD BE IMPLEMENTED BY THE TOOLKIT CODE ONLY! DON'T
+ * EVEN THINK ABOUT IMPLEMENTING THIS IN JAVASCRIPT!
+ */
+[scriptable, builtinclass, uuid(7422b090-4a86-4407-972e-75468a625388)]
+interface nsIToolkitProfile : nsISupports
+{
+ /**
+ * The location of the profile directory.
+ */
+ readonly attribute nsIFile rootDir;
+
+ /**
+ * The location of the profile local directory, which may be the same as
+ * the root directory. See nsIProfileLock::localDirectory.
+ */
+ readonly attribute nsIFile localDir;
+
+ /**
+ * The name of the profile.
+ */
+ attribute AUTF8String name;
+
+ /**
+ * Removes the profile from the registry of profiles.
+ *
+ * @param removeFiles
+ * Indicates whether or not the profile directory should be
+ * removed in addition.
+ */
+ void remove(in boolean removeFiles);
+
+ /**
+ * Removes the profile from the registry of profiles.
+ * The profile directory is removed in the stream transport thread.
+ *
+ * @param removeFiles
+ * Indicates whether or not the profile directory should be
+ * removed in addition.
+ */
+ void removeInBackground(in boolean removeFiles);
+
+ /**
+ * Lock this profile using platform-specific locking methods.
+ *
+ * @param lockFile If locking fails, this may return a lockFile object
+ * which can be used in platform-specific ways to
+ * determine which process has the file locked. Null
+ * may be passed.
+ * @return An interface which holds a profile lock as long as you reference
+ * it.
+ * @throws NS_ERROR_FILE_ACCESS_DENIED if the profile was already locked.
+ */
+ nsIProfileLock lock(out nsIProfileUnlocker aUnlocker);
+};
diff --git a/toolkit/profile/nsIToolkitProfileService.idl b/toolkit/profile/nsIToolkitProfileService.idl
new file mode 100644
index 0000000000..f8519f1101
--- /dev/null
+++ b/toolkit/profile/nsIToolkitProfileService.idl
@@ -0,0 +1,147 @@
+/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsISimpleEnumerator;
+interface nsIFile;
+interface nsIToolkitProfile;
+interface nsIProfileLock;
+
+[scriptable, builtinclass, uuid(1947899b-f369-48fa-89da-f7c37bb1e6bc)]
+interface nsIToolkitProfileService : nsISupports
+{
+ /**
+ * Tests whether the profile lists on disk have changed since they were
+ * loaded. When this is true attempts to flush changes to disk will fail.
+ */
+ [infallible] readonly attribute boolean isListOutdated;
+
+ /**
+ * When a downgrade is detected UI is presented to the user to ask how to
+ * proceed. These flags are used to pass some information to the UI.
+ */
+ cenum downgradeUIFlags: 8 {
+ hasSync = 1,
+ };
+
+ /**
+ * When a downgrade is detected UI is presented to the user to ask how to
+ * proceed. These are the possible options the user can choose.
+ */
+ cenum downgradeUIChoice: 8 {
+ quit = 0,
+ createNewProfile = 1,
+ };
+
+ cenum profileManagerResult: 8 {
+ exit = 0,
+ launchWithProfile = 1,
+ restart = 2,
+ };
+
+ attribute boolean startWithLastProfile;
+
+ readonly attribute nsISimpleEnumerator /*nsIToolkitProfile*/ profiles;
+
+ /**
+ * The profile currently in use if it is a named profile. This will return
+ * null if the current profile path doesn't match a profile in the database.
+ */
+ readonly attribute nsIToolkitProfile currentProfile;
+
+ /**
+ * The default profile for this build.
+ * On startup this is the profile selected unless overridden by command line
+ * arguments or environment variables. Setting this will change the profile
+ * used by default the next time the application is started.
+ * Attempting to change the default may throw an exception on builds that do
+ * not support changing the default profile, such as developer edition.
+ */
+ attribute nsIToolkitProfile defaultProfile;
+
+ /**
+ * Selects or creates a profile to use based on the profiles database, any
+ * environment variables and any command line arguments. Will not create
+ * a profile if aIsResetting is true. The profile is selected based on this
+ * order of preference:
+ * * Environment variables (set when restarting the application).
+ * * --profile command line argument.
+ * * --createprofile command line argument (this also causes the app to exit).
+ * * -p command line argument.
+ * * A new profile created if this is the first run of the application.
+ * * The default profile.
+ * aRootDir and aLocalDir are set to the data and local directories for the
+ * profile data. If a profile from the database was selected it will be
+ * returned in aProfile.
+ * This returns true if a new profile was created.
+ * This method is primarily for testing. It can be called only once.
+ */
+ bool selectStartupProfile(in Array<ACString> aArgv,
+ in boolean aIsResetting, in AUTF8String aUpdateChannel,
+ in AUTF8String aLegacyInstallHash,
+ out nsIFile aRootDir, out nsIFile aLocalDir,
+ out nsIToolkitProfile aProfile);
+
+ /**
+ * Get a profile by name. This is mainly for use by the -P
+ * commandline flag.
+ *
+ * @param aName The profile name to find.
+ */
+ nsIToolkitProfile getProfileByName(in AUTF8String aName);
+
+ /**
+ * Create a new profile.
+ *
+ * The profile temporary directory will be chosen based on where the
+ * profile directory is located.
+ *
+ * If a profile with the given name already exists it will be returned
+ * instead of creating a new profile.
+ *
+ * @param aRootDir
+ * The profile directory. May be null, in which case a suitable
+ * default will be chosen based on the profile name.
+ * @param aName
+ * The profile name.
+ */
+ nsIToolkitProfile createProfile(in nsIFile aRootDir,
+ in AUTF8String aName);
+
+ /**
+ * Create a new profile with a unique name.
+ *
+ * As above however the name given will be altered to make it a unique
+ * profile name.
+ *
+ * @param aRootDir
+ * The profile directory. May be null, in which case a suitable
+ * default will be chosen based on the profile name.
+ * @param aNamePrefix
+ * The prefix to use for the profile name. If unused this will be
+ * used as the profile name otherwise additional characters will be
+ * added to make the name unique.
+ */
+ nsIToolkitProfile createUniqueProfile(in nsIFile aRootDir,
+ in AUTF8String aNamePrefix);
+
+ /**
+ * Returns the number of profiles.
+ * @return the number of profiles.
+ */
+ readonly attribute unsigned long profileCount;
+
+ /**
+ * Flush the profiles list file. This will fail with
+ * NS_ERROR_DATABASE_CHANGED if the files on disk have changed since the
+ * profiles were loaded.
+ */
+ void flush();
+};
+
+%{C++
+#define NS_PROFILESERVICE_CONTRACTID "@mozilla.org/toolkit/profile-service;1"
+%}
diff --git a/toolkit/profile/nsProfileLock.cpp b/toolkit/profile/nsProfileLock.cpp
new file mode 100644
index 0000000000..0a809642aa
--- /dev/null
+++ b/toolkit/profile/nsProfileLock.cpp
@@ -0,0 +1,578 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsProfileLock.h"
+#include "nsCOMPtr.h"
+#include "nsQueryObject.h"
+#include "nsString.h"
+#include "nsPrintfCString.h"
+#include "nsDebug.h"
+
+#if defined(XP_WIN)
+# include "ProfileUnlockerWin.h"
+#endif
+
+#if defined(XP_MACOSX)
+# include <Carbon/Carbon.h>
+# include <CoreFoundation/CoreFoundation.h>
+#endif
+
+#if defined(MOZ_WIDGET_ANDROID)
+# include "ProfileUnlockerAndroid.h"
+#endif
+
+#ifdef XP_UNIX
+# include <unistd.h>
+# include <fcntl.h>
+# include <errno.h>
+# include <signal.h>
+# include <stdlib.h>
+# include "prnetdb.h"
+# include "prsystem.h"
+# include "prenv.h"
+# include "mozilla/Printf.h"
+#endif
+
+// **********************************************************************
+// class nsProfileLock
+//
+// This code was moved from profile/src/nsProfileAccess.
+// **********************************************************************
+
+#if defined(XP_UNIX)
+static bool sDisableSignalHandling = false;
+#endif
+
+nsProfileLock::nsProfileLock()
+ : mHaveLock(false),
+ mReplacedLockTime(0)
+#if defined(XP_WIN)
+ ,
+ mLockFileHandle(INVALID_HANDLE_VALUE)
+#elif defined(XP_UNIX)
+ ,
+ mPidLockFileName(nullptr),
+ mLockFileDesc(-1)
+#endif
+{
+#if defined(XP_UNIX)
+ next = prev = this;
+ sDisableSignalHandling = PR_GetEnv("MOZ_DISABLE_SIG_HANDLER") ? true : false;
+#endif
+}
+
+nsProfileLock::nsProfileLock(nsProfileLock& src) { *this = src; }
+
+nsProfileLock& nsProfileLock::operator=(nsProfileLock& rhs) {
+ Unlock();
+
+ mHaveLock = rhs.mHaveLock;
+ rhs.mHaveLock = false;
+
+#if defined(XP_WIN)
+ mLockFileHandle = rhs.mLockFileHandle;
+ rhs.mLockFileHandle = INVALID_HANDLE_VALUE;
+#elif defined(XP_UNIX)
+ mLockFileDesc = rhs.mLockFileDesc;
+ rhs.mLockFileDesc = -1;
+ mPidLockFileName = rhs.mPidLockFileName;
+ rhs.mPidLockFileName = nullptr;
+ if (mPidLockFileName) {
+ // rhs had a symlink lock, therefore it was on the list.
+ PR_REMOVE_LINK(&rhs);
+ PR_APPEND_LINK(this, &mPidLockList);
+ }
+#endif
+
+ return *this;
+}
+
+nsProfileLock::~nsProfileLock() {
+ Unlock();
+ // Note that we don't clean up by default here so on next startup we know when
+ // the profile was last used based on the modification time of the lock file.
+}
+
+#if defined(XP_UNIX)
+
+static int setupPidLockCleanup;
+
+PRCList nsProfileLock::mPidLockList =
+ PR_INIT_STATIC_CLIST(&nsProfileLock::mPidLockList);
+
+void nsProfileLock::RemovePidLockFiles(bool aFatalSignal) {
+ while (!PR_CLIST_IS_EMPTY(&mPidLockList)) {
+ nsProfileLock* lock = static_cast<nsProfileLock*>(mPidLockList.next);
+ lock->Unlock(aFatalSignal);
+ }
+}
+
+static struct sigaction SIGHUP_oldact;
+static struct sigaction SIGINT_oldact;
+static struct sigaction SIGQUIT_oldact;
+static struct sigaction SIGILL_oldact;
+static struct sigaction SIGABRT_oldact;
+static struct sigaction SIGSEGV_oldact;
+static struct sigaction SIGTERM_oldact;
+
+void nsProfileLock::FatalSignalHandler(int signo
+# ifdef SA_SIGINFO
+ ,
+ siginfo_t* info, void* context
+# endif
+) {
+ // Remove any locks still held.
+ RemovePidLockFiles(true);
+
+ // Chain to the old handler, which may exit.
+ struct sigaction* oldact = nullptr;
+
+ switch (signo) {
+ case SIGHUP:
+ oldact = &SIGHUP_oldact;
+ break;
+ case SIGINT:
+ oldact = &SIGINT_oldact;
+ break;
+ case SIGQUIT:
+ oldact = &SIGQUIT_oldact;
+ break;
+ case SIGILL:
+ oldact = &SIGILL_oldact;
+ break;
+ case SIGABRT:
+ oldact = &SIGABRT_oldact;
+ break;
+ case SIGSEGV:
+ oldact = &SIGSEGV_oldact;
+ break;
+ case SIGTERM:
+ oldact = &SIGTERM_oldact;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("bad signo");
+ break;
+ }
+
+ if (oldact) {
+ if (oldact->sa_handler == SIG_DFL) {
+ // Make sure the default sig handler is executed
+ // We need it to get Mozilla to dump core.
+ sigaction(signo, oldact, nullptr);
+
+ // Now that we've restored the default handler, unmask the
+ // signal and invoke it.
+
+ sigset_t unblock_sigs;
+ sigemptyset(&unblock_sigs);
+ sigaddset(&unblock_sigs, signo);
+
+ sigprocmask(SIG_UNBLOCK, &unblock_sigs, nullptr);
+
+ raise(signo);
+ }
+# ifdef SA_SIGINFO
+ else if (oldact->sa_sigaction &&
+ (oldact->sa_flags & SA_SIGINFO) == SA_SIGINFO) {
+ oldact->sa_sigaction(signo, info, context);
+ }
+# endif
+ else if (oldact->sa_handler && oldact->sa_handler != SIG_IGN) {
+ oldact->sa_handler(signo);
+ }
+ }
+
+ // Backstop exit call, just in case.
+ _exit(signo);
+}
+
+nsresult nsProfileLock::LockWithFcntl(nsIFile* aLockFile,
+ nsIProfileUnlocker** aUnlocker) {
+ nsresult rv = NS_OK;
+
+ nsAutoCString lockFilePath;
+ rv = aLockFile->GetNativePath(lockFilePath);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Could not get native path");
+ return rv;
+ }
+
+ aLockFile->GetLastModifiedTime(&mReplacedLockTime);
+
+ mLockFileDesc = open(lockFilePath.get(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (mLockFileDesc != -1) {
+ struct flock lock;
+ lock.l_start = 0;
+ lock.l_len = 0; // len = 0 means entire file
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+
+ // If fcntl(F_GETLK) fails then the server does not support/allow fcntl(),
+ // return failure rather than access denied in this case so we fallback
+ // to using a symlink lock, bug 303633.
+ struct flock testlock = lock;
+ if (fcntl(mLockFileDesc, F_GETLK, &testlock) == -1) {
+ close(mLockFileDesc);
+ mLockFileDesc = -1;
+ rv = NS_ERROR_FAILURE;
+ } else if (fcntl(mLockFileDesc, F_SETLK, &lock) == -1) {
+# ifdef MOZ_WIDGET_ANDROID
+ MOZ_ASSERT(aUnlocker);
+ RefPtr<mozilla::ProfileUnlockerAndroid> unlocker(
+ new mozilla::ProfileUnlockerAndroid(testlock.l_pid));
+ nsCOMPtr<nsIProfileUnlocker> unlockerInterface(do_QueryObject(unlocker));
+ unlockerInterface.forget(aUnlocker);
+# endif
+
+ close(mLockFileDesc);
+ mLockFileDesc = -1;
+
+ // With OS X, on NFS, errno == ENOTSUP
+ // XXX Check for that and return specific rv for it?
+# ifdef DEBUG
+ printf("fcntl(F_SETLK) failed. errno = %d\n", errno);
+# endif
+ if (errno == EAGAIN || errno == EACCES)
+ rv = NS_ERROR_FILE_ACCESS_DENIED;
+ else
+ rv = NS_ERROR_FAILURE;
+ }
+ } else {
+ NS_ERROR("Failed to open lock file.");
+ rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+static bool IsSymlinkStaleLock(struct in_addr* aAddr, const char* aFileName,
+ bool aHaveFcntlLock) {
+ // the link exists; see if it's from this machine, and if
+ // so if the process is still active
+ char buf[1024];
+ int len = readlink(aFileName, buf, sizeof buf - 1);
+ if (len > 0) {
+ buf[len] = '\0';
+ char* colon = strchr(buf, ':');
+ if (colon) {
+ *colon++ = '\0';
+ unsigned long addr = inet_addr(buf);
+ if (addr != (unsigned long)-1) {
+ if (colon[0] == '+' && aHaveFcntlLock) {
+ // This lock was placed by a Firefox build which would have
+ // taken the fnctl lock, and we've already taken the fcntl lock,
+ // so the process that created this obsolete lock must be gone
+ return true;
+ }
+
+ char* after = nullptr;
+ pid_t pid = strtol(colon, &after, 0);
+ if (pid != 0 && *after == '\0') {
+ if (addr != aAddr->s_addr) {
+ // Remote lock: give up even if stuck.
+ return false;
+ }
+
+ // kill(pid,0) is a neat trick to check if a
+ // process exists
+ if (kill(pid, 0) == 0 || errno != ESRCH) {
+ // Local process appears to be alive, ass-u-me it
+ // is another Mozilla instance, or a compatible
+ // derivative, that's currently using the profile.
+ // XXX need an "are you Mozilla?" protocol
+ return false;
+ }
+ }
+ }
+ }
+ }
+ return true;
+}
+
+nsresult nsProfileLock::LockWithSymlink(nsIFile* aLockFile,
+ bool aHaveFcntlLock) {
+ nsresult rv;
+ nsAutoCString lockFilePath;
+ rv = aLockFile->GetNativePath(lockFilePath);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Could not get native path");
+ return rv;
+ }
+
+ // don't replace an existing lock time if fcntl already got one
+ if (!mReplacedLockTime)
+ aLockFile->GetLastModifiedTimeOfLink(&mReplacedLockTime);
+
+ struct in_addr inaddr;
+ inaddr.s_addr = htonl(INADDR_LOOPBACK);
+
+ // We still have not loaded the profile, so we may not have proxy information.
+ // Avoiding a DNS lookup in this stage makes sure any proxy is not bypassed.
+ // By default, the lookup is enabled, but when it is not, we use 127.0.0.1
+ // for the IP address portion of the lock signature.
+ // However, this may cause the browser to refuse to start in the rare case
+ // that all of the following conditions are met:
+ // 1. The browser profile is on a network file system.
+ // 2. The file system does not support fcntl() locking.
+ // 3. The browser is run from two different computers at the same time.
+# ifndef MOZ_PROXY_BYPASS_PROTECTION
+ char hostname[256];
+ PRStatus status = PR_GetSystemInfo(PR_SI_HOSTNAME, hostname, sizeof hostname);
+ if (status == PR_SUCCESS) {
+ char netdbbuf[PR_NETDB_BUF_SIZE];
+ PRHostEnt hostent;
+ status = PR_GetHostByName(hostname, netdbbuf, sizeof netdbbuf, &hostent);
+ if (status == PR_SUCCESS) memcpy(&inaddr, hostent.h_addr, sizeof inaddr);
+ }
+# endif
+
+ mozilla::SmprintfPointer signature =
+ mozilla::Smprintf("%s:%s%lu", inet_ntoa(inaddr),
+ aHaveFcntlLock ? "+" : "", (unsigned long)getpid());
+ const char* fileName = lockFilePath.get();
+ int symlink_rv, symlink_errno = 0, tries = 0;
+
+ // use ns4.x-compatible symlinks if the FS supports them
+ while ((symlink_rv = symlink(signature.get(), fileName)) < 0) {
+ symlink_errno = errno;
+ if (symlink_errno != EEXIST) break;
+
+ if (!IsSymlinkStaleLock(&inaddr, fileName, aHaveFcntlLock)) break;
+
+ // Lock seems to be bogus: try to claim it. Give up after a large
+ // number of attempts (100 comes from the 4.x codebase).
+ (void)unlink(fileName);
+ if (++tries > 100) break;
+ }
+
+ if (symlink_rv == 0) {
+ // We exclusively created the symlink: record its name for eventual
+ // unlock-via-unlink.
+ rv = NS_OK;
+ mPidLockFileName = strdup(fileName);
+ if (mPidLockFileName) {
+ PR_APPEND_LINK(this, &mPidLockList);
+ if (!setupPidLockCleanup++) {
+ // Clean up on normal termination.
+ // This instanciates a dummy class, and will trigger the class
+ // destructor when libxul is unloaded. This is equivalent to atexit(),
+ // but gracefully handles dlclose().
+ static RemovePidLockFilesExiting r;
+
+ // Clean up on abnormal termination, using POSIX sigaction.
+ // Don't arm a handler if the signal is being ignored, e.g.,
+ // because mozilla is run via nohup.
+ if (!sDisableSignalHandling) {
+ struct sigaction act, oldact;
+# ifdef SA_SIGINFO
+ act.sa_sigaction = FatalSignalHandler;
+ act.sa_flags = SA_SIGINFO | SA_ONSTACK;
+# else
+ act.sa_handler = FatalSignalHandler;
+# endif
+ sigfillset(&act.sa_mask);
+
+# define CATCH_SIGNAL(signame) \
+ PR_BEGIN_MACRO \
+ if (sigaction(signame, nullptr, &oldact) == 0 && \
+ oldact.sa_handler != SIG_IGN) { \
+ sigaction(signame, &act, &signame##_oldact); \
+ } \
+ PR_END_MACRO
+
+ CATCH_SIGNAL(SIGHUP);
+ CATCH_SIGNAL(SIGINT);
+ CATCH_SIGNAL(SIGQUIT);
+ CATCH_SIGNAL(SIGILL);
+ CATCH_SIGNAL(SIGABRT);
+ CATCH_SIGNAL(SIGSEGV);
+ CATCH_SIGNAL(SIGTERM);
+
+# undef CATCH_SIGNAL
+ }
+ }
+ }
+ } else if (symlink_errno == EEXIST)
+ rv = NS_ERROR_FILE_ACCESS_DENIED;
+ else {
+# ifdef DEBUG
+ printf("symlink() failed. errno = %d\n", errno);
+# endif
+ rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+#endif /* XP_UNIX */
+
+nsresult nsProfileLock::GetReplacedLockTime(PRTime* aResult) {
+ *aResult = mReplacedLockTime;
+ return NS_OK;
+}
+
+#if defined(XP_MACOSX)
+constexpr auto LOCKFILE_NAME = u".parentlock"_ns;
+constexpr auto OLD_LOCKFILE_NAME = u"parent.lock"_ns;
+#elif defined(XP_UNIX)
+constexpr auto OLD_LOCKFILE_NAME = u"lock"_ns;
+constexpr auto LOCKFILE_NAME = u".parentlock"_ns;
+#else
+constexpr auto LOCKFILE_NAME = u"parent.lock"_ns;
+#endif
+
+bool nsProfileLock::IsMaybeLockFile(nsIFile* aFile) {
+ nsAutoString tmp;
+ if (NS_SUCCEEDED(aFile->GetLeafName(tmp))) {
+ if (tmp.Equals(LOCKFILE_NAME)) return true;
+#if (defined(XP_MACOSX) || defined(XP_UNIX))
+ if (tmp.Equals(OLD_LOCKFILE_NAME)) return true;
+#endif
+ }
+ return false;
+}
+
+nsresult nsProfileLock::Lock(nsIFile* aProfileDir,
+ nsIProfileUnlocker** aUnlocker) {
+ nsresult rv;
+ if (aUnlocker) *aUnlocker = nullptr;
+
+ NS_ENSURE_STATE(!mHaveLock);
+
+ bool isDir;
+ rv = aProfileDir->IsDirectory(&isDir);
+ if (NS_FAILED(rv)) return rv;
+ if (!isDir) return NS_ERROR_FILE_NOT_DIRECTORY;
+
+ nsCOMPtr<nsIFile> lockFile;
+ rv = aProfileDir->Clone(getter_AddRefs(lockFile));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = lockFile->Append(LOCKFILE_NAME);
+ if (NS_FAILED(rv)) return rv;
+
+ // Remember the name we're using so we can clean up
+ rv = lockFile->Clone(getter_AddRefs(mLockFile));
+ if (NS_FAILED(rv)) return rv;
+
+#if defined(XP_MACOSX)
+ // First, try locking using fcntl. It is more reliable on
+ // a local machine, but may not be supported by an NFS server.
+
+ rv = LockWithFcntl(lockFile);
+ if (NS_FAILED(rv) && (rv != NS_ERROR_FILE_ACCESS_DENIED)) {
+ // If that failed for any reason other than NS_ERROR_FILE_ACCESS_DENIED,
+ // assume we tried an NFS that does not support it. Now, try with symlink.
+ rv = LockWithSymlink(lockFile, false);
+ }
+#elif defined(XP_UNIX)
+ // Get the old lockfile name
+ nsCOMPtr<nsIFile> oldLockFile;
+ rv = aProfileDir->Clone(getter_AddRefs(oldLockFile));
+ if (NS_FAILED(rv)) return rv;
+ rv = oldLockFile->Append(OLD_LOCKFILE_NAME);
+ if (NS_FAILED(rv)) return rv;
+
+ // First, try locking using fcntl. It is more reliable on
+ // a local machine, but may not be supported by an NFS server.
+ rv = LockWithFcntl(lockFile, aUnlocker);
+ if (NS_SUCCEEDED(rv)) {
+ // Check to see whether there is a symlink lock held by an older
+ // Firefox build, and also place our own symlink lock --- but
+ // mark it "obsolete" so that other newer builds can break the lock
+ // if they obtain the fcntl lock
+ rv = LockWithSymlink(oldLockFile, true);
+
+ // If the symlink failed for some reason other than it already
+ // exists, then something went wrong e.g. the file system
+ // doesn't support symlinks, or we don't have permission to
+ // create a symlink there. In such cases we should just
+ // continue because it's unlikely there is an old build
+ // running with a symlink there and we've already successfully
+ // placed a fcntl lock.
+ if (rv != NS_ERROR_FILE_ACCESS_DENIED) rv = NS_OK;
+ } else if (rv != NS_ERROR_FILE_ACCESS_DENIED) {
+ // If that failed for any reason other than NS_ERROR_FILE_ACCESS_DENIED,
+ // assume we tried an NFS that does not support it. Now, try with symlink
+ // using the old symlink path
+ rv = LockWithSymlink(oldLockFile, false);
+ }
+
+#elif defined(XP_WIN)
+ nsAutoString filePath;
+ rv = lockFile->GetPath(filePath);
+ if (NS_FAILED(rv)) return rv;
+
+ lockFile->GetLastModifiedTime(&mReplacedLockTime);
+
+ // always create the profile lock and never delete it so we can use its
+ // modification timestamp to detect startup crashes
+ mLockFileHandle = CreateFileW(filePath.get(), GENERIC_READ | GENERIC_WRITE,
+ 0, // no sharing - of course
+ nullptr, CREATE_ALWAYS, 0, nullptr);
+ if (mLockFileHandle == INVALID_HANDLE_VALUE) {
+ if (aUnlocker) {
+ RefPtr<mozilla::ProfileUnlockerWin> unlocker(
+ new mozilla::ProfileUnlockerWin(filePath));
+ if (NS_SUCCEEDED(unlocker->Init())) {
+ nsCOMPtr<nsIProfileUnlocker> unlockerInterface(
+ do_QueryObject(unlocker));
+ unlockerInterface.forget(aUnlocker);
+ }
+ }
+ return NS_ERROR_FILE_ACCESS_DENIED;
+ }
+#endif
+
+ if (NS_SUCCEEDED(rv)) mHaveLock = true;
+
+ return rv;
+}
+
+nsresult nsProfileLock::Unlock(bool aFatalSignal) {
+ nsresult rv = NS_OK;
+
+ if (mHaveLock) {
+#if defined(XP_WIN)
+ if (mLockFileHandle != INVALID_HANDLE_VALUE) {
+ CloseHandle(mLockFileHandle);
+ mLockFileHandle = INVALID_HANDLE_VALUE;
+ }
+#elif defined(XP_UNIX)
+ if (mPidLockFileName) {
+ PR_REMOVE_LINK(this);
+ (void)unlink(mPidLockFileName);
+
+ // Only free mPidLockFileName if we're not in the fatal signal
+ // handler. The problem is that a call to free() might be the
+ // cause of this fatal signal. If so, calling free() might cause
+ // us to wait on the malloc implementation's lock. We're already
+ // holding this lock, so we'll deadlock. See bug 522332.
+ if (!aFatalSignal) free(mPidLockFileName);
+ mPidLockFileName = nullptr;
+ }
+ if (mLockFileDesc != -1) {
+ close(mLockFileDesc);
+ mLockFileDesc = -1;
+ // Don't remove it
+ }
+#endif
+
+ mHaveLock = false;
+ }
+
+ return rv;
+}
+
+nsresult nsProfileLock::Cleanup() {
+ if (mHaveLock) {
+ return NS_ERROR_FILE_IS_LOCKED;
+ }
+
+ if (mLockFile) {
+ nsresult rv = mLockFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mLockFile = nullptr;
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/profile/nsProfileLock.h b/toolkit/profile/nsProfileLock.h
new file mode 100644
index 0000000000..932ce567bf
--- /dev/null
+++ b/toolkit/profile/nsProfileLock.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __nsProfileLock_h___
+#define __nsProfileLock_h___
+
+#include "nsIFile.h"
+
+class nsIProfileUnlocker;
+
+#if defined(XP_WIN)
+# include <windows.h>
+#endif
+
+#if defined(XP_UNIX)
+# include <signal.h>
+# include "prclist.h"
+#endif
+
+class nsProfileLock
+#if defined(XP_UNIX)
+ : public PRCList
+#endif
+{
+ public:
+ nsProfileLock();
+ nsProfileLock(nsProfileLock& src);
+
+ ~nsProfileLock();
+
+ nsProfileLock& operator=(nsProfileLock& rhs);
+
+ /**
+ * Attempt to lock a profile directory.
+ *
+ * @param aProfileDir [in] The profile directory to lock.
+ * @param aUnlocker [out] Optional. This is only returned when locking
+ * fails with NS_ERROR_FILE_ACCESS_DENIED, and may not
+ * be returned at all.
+ * @throws NS_ERROR_FILE_ACCESS_DENIED if the profile is locked.
+ */
+ nsresult Lock(nsIFile* aProfileDir, nsIProfileUnlocker** aUnlocker);
+
+ /**
+ * Unlock a profile directory. If you're unlocking the directory because
+ * the application is in the process of shutting down because of a fatal
+ * signal, set aFatalSignal to true.
+ */
+ nsresult Unlock(bool aFatalSignal = false);
+
+ /**
+ * Checks, if the given file has a name that matches potential lock file
+ * names. It does not check, if it actually is the currently active lock.
+ */
+ static bool IsMaybeLockFile(nsIFile* aFile);
+
+ /**
+ * Clean up any left over files in the directory.
+ */
+ nsresult Cleanup();
+
+ /**
+ * Get the modification time of a replaced profile lock, otherwise 0.
+ */
+ nsresult GetReplacedLockTime(PRTime* aResult);
+
+ private:
+ bool mHaveLock;
+ PRTime mReplacedLockTime;
+ nsCOMPtr<nsIFile> mLockFile;
+
+#if defined(XP_WIN)
+ HANDLE mLockFileHandle;
+#elif defined(XP_UNIX)
+
+ struct RemovePidLockFilesExiting {
+ RemovePidLockFilesExiting() = default;
+ ~RemovePidLockFilesExiting() { RemovePidLockFiles(false); }
+ };
+
+ static void RemovePidLockFiles(bool aFatalSignal);
+ static void FatalSignalHandler(int signo
+# ifdef SA_SIGINFO
+ ,
+ siginfo_t* info, void* context
+# endif
+ );
+ static PRCList mPidLockList;
+
+ nsresult LockWithFcntl(nsIFile* aLockFile,
+ nsIProfileUnlocker** aUnlocker = nullptr);
+
+ /**
+ * @param aHaveFcntlLock if true, we've already acquired an fcntl lock so this
+ * lock is merely an "obsolete" lock to keep out old Firefoxes
+ */
+ nsresult LockWithSymlink(nsIFile* aLockFile, bool aHaveFcntlLock);
+
+ char* mPidLockFileName;
+ int mLockFileDesc;
+#endif
+};
+
+#endif /* __nsProfileLock_h___ */
diff --git a/toolkit/profile/nsToolkitProfileService.cpp b/toolkit/profile/nsToolkitProfileService.cpp
new file mode 100644
index 0000000000..aeab25c61f
--- /dev/null
+++ b/toolkit/profile/nsToolkitProfileService.cpp
@@ -0,0 +1,2240 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/WidgetUtils.h"
+#include "nsProfileLock.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <prprf.h>
+#include <prtime.h>
+
+#ifdef XP_WIN
+# include <windows.h>
+# include <shlobj.h>
+# include "mozilla/PolicyChecks.h"
+#endif
+#ifdef XP_UNIX
+# include <unistd.h>
+#endif
+
+#include "nsToolkitProfileService.h"
+#include "CmdLineAndEnvUtils.h"
+#include "nsIFile.h"
+
+#ifdef XP_MACOSX
+# include <CoreFoundation/CoreFoundation.h>
+# include "nsILocalFileMac.h"
+#endif
+
+#ifdef MOZ_WIDGET_GTK
+# include "mozilla/WidgetUtilsGtk.h"
+#endif
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsNetCID.h"
+#include "nsXULAppAPI.h"
+#include "nsThreadUtils.h"
+
+#include "nsIRunnable.h"
+#include "nsXREDirProvider.h"
+#include "nsAppRunner.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsNativeCharsetUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Sprintf.h"
+#include "nsPrintfCString.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIToolkitShellService.h"
+#include "mozilla/Telemetry.h"
+#include "nsProxyRelease.h"
+#include "prinrval.h"
+#include "prthread.h"
+#ifdef MOZ_BACKGROUNDTASKS
+# include "mozilla/BackgroundTasks.h"
+# include "SpecialSystemDirectory.h"
+#endif
+
+using namespace mozilla;
+
+#define DEV_EDITION_NAME "dev-edition-default"
+#define DEFAULT_NAME "default"
+#define COMPAT_FILE u"compatibility.ini"_ns
+#define PROFILE_DB_VERSION "2"
+#define INSTALL_PREFIX "Install"
+#define INSTALL_PREFIX_LENGTH 7
+
+struct KeyValue {
+ KeyValue(const char* aKey, const char* aValue) : key(aKey), value(aValue) {}
+
+ nsCString key;
+ nsCString value;
+};
+
+static bool GetStrings(const char* aString, const char* aValue,
+ void* aClosure) {
+ nsTArray<UniquePtr<KeyValue>>* array =
+ static_cast<nsTArray<UniquePtr<KeyValue>>*>(aClosure);
+ array->AppendElement(MakeUnique<KeyValue>(aString, aValue));
+
+ return true;
+}
+
+/**
+ * Returns an array of the strings inside a section of an ini file.
+ */
+nsTArray<UniquePtr<KeyValue>> GetSectionStrings(nsINIParser* aParser,
+ const char* aSection) {
+ nsTArray<UniquePtr<KeyValue>> result;
+ aParser->GetStrings(aSection, &GetStrings, &result);
+ return result;
+}
+
+void RemoveProfileRecursion(const nsCOMPtr<nsIFile>& aDirectoryOrFile,
+ bool aIsIgnoreRoot, bool aIsIgnoreLockfile,
+ nsTArray<nsCOMPtr<nsIFile>>& aOutUndeletedFiles) {
+ auto guardDeletion = MakeScopeExit(
+ [&] { aOutUndeletedFiles.AppendElement(aDirectoryOrFile); });
+
+ // We actually would not expect to see links in our profiles, but still.
+ bool isLink = false;
+ NS_ENSURE_SUCCESS_VOID(aDirectoryOrFile->IsSymlink(&isLink));
+
+ // Only check to see if we have a directory if it isn't a link.
+ bool isDir = false;
+ if (!isLink) {
+ NS_ENSURE_SUCCESS_VOID(aDirectoryOrFile->IsDirectory(&isDir));
+ }
+
+ if (isDir) {
+ nsCOMPtr<nsIDirectoryEnumerator> dirEnum;
+ NS_ENSURE_SUCCESS_VOID(
+ aDirectoryOrFile->GetDirectoryEntries(getter_AddRefs(dirEnum)));
+
+ bool more = false;
+ while (NS_SUCCEEDED(dirEnum->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsISupports> item;
+ dirEnum->GetNext(getter_AddRefs(item));
+ nsCOMPtr<nsIFile> file = do_QueryInterface(item);
+ if (file) {
+ // Do not delete the profile lock.
+ if (aIsIgnoreLockfile && nsProfileLock::IsMaybeLockFile(file)) continue;
+ // If some children's remove fails, we still continue the loop.
+ RemoveProfileRecursion(file, false, false, aOutUndeletedFiles);
+ }
+ }
+ }
+ // Do not delete the root directory (yet).
+ if (!aIsIgnoreRoot) {
+ NS_ENSURE_SUCCESS_VOID(aDirectoryOrFile->Remove(false));
+ }
+ guardDeletion.release();
+}
+
+void RemoveProfileFiles(nsIToolkitProfile* aProfile, bool aInBackground) {
+ nsCOMPtr<nsIFile> rootDir;
+ aProfile->GetRootDir(getter_AddRefs(rootDir));
+ nsCOMPtr<nsIFile> localDir;
+ aProfile->GetLocalDir(getter_AddRefs(localDir));
+
+ // XXX If we get here with an active quota manager,
+ // something went very wrong. We want to assert this.
+
+ // Just lock the directories, don't mark the profile as locked or the lock
+ // will attempt to release its reference to the profile on the background
+ // thread which will assert.
+ nsCOMPtr<nsIProfileLock> lock;
+ NS_ENSURE_SUCCESS_VOID(
+ NS_LockProfilePath(rootDir, localDir, nullptr, getter_AddRefs(lock)));
+
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "nsToolkitProfile::RemoveProfileFiles",
+ [rootDir, localDir, lock]() mutable {
+ // We try to remove every single file and directory and collect
+ // those whose removal failed.
+ nsTArray<nsCOMPtr<nsIFile>> undeletedFiles;
+ // The root dir might contain the temp dir, so remove the temp dir
+ // first.
+ bool equals;
+ nsresult rv = rootDir->Equals(localDir, &equals);
+ if (NS_SUCCEEDED(rv) && !equals) {
+ RemoveProfileRecursion(localDir,
+ /* aIsIgnoreRoot */ false,
+ /* aIsIgnoreLockfile */ false, undeletedFiles);
+ }
+ // Now remove the content of the profile dir (except lockfile)
+ RemoveProfileRecursion(rootDir,
+ /* aIsIgnoreRoot */ true,
+ /* aIsIgnoreLockfile */ true, undeletedFiles);
+
+ // Retry loop if something was not deleted
+ if (undeletedFiles.Length() > 0) {
+ uint32_t retries = 1;
+ // XXX: Until bug 1716291 is fixed we just make one retry
+ while (undeletedFiles.Length() > 0 && retries <= 1) {
+ Unused << PR_Sleep(PR_MillisecondsToInterval(10 * retries));
+ for (auto&& file :
+ std::exchange(undeletedFiles, nsTArray<nsCOMPtr<nsIFile>>{})) {
+ RemoveProfileRecursion(file,
+ /* aIsIgnoreRoot */ false,
+ /* aIsIgnoreLockfile */ true,
+ undeletedFiles);
+ }
+ retries++;
+ }
+ }
+
+#ifdef DEBUG
+ // XXX: Until bug 1716291 is fixed, we do not want to spam release
+ if (undeletedFiles.Length() > 0) {
+ NS_WARNING("Unable to remove all files from the profile directory:");
+ // Log the file names of those we could not remove
+ for (auto&& file : undeletedFiles) {
+ nsAutoString leafName;
+ if (NS_SUCCEEDED(file->GetLeafName(leafName))) {
+ NS_WARNING(NS_LossyConvertUTF16toASCII(leafName).get());
+ }
+ }
+ }
+#endif
+ // XXX: Activate this assert once bug 1716291 is fixed
+ // MOZ_ASSERT(undeletedFiles.Length() == 0);
+
+ // Now we can unlock the profile safely.
+ lock->Unlock();
+ // nsIProfileLock is not threadsafe so release our reference to it on
+ // the main thread.
+ NS_ReleaseOnMainThread("nsToolkitProfile::RemoveProfileFiles::Unlock",
+ lock.forget());
+
+ if (undeletedFiles.Length() == 0) {
+ // We can safely remove the (empty) remaining profile directory
+ // and lockfile, no other files are here.
+ // As we do this only if we had no other blockers, this is as safe
+ // as deleting the lockfile explicitely after unlocking.
+ Unused << rootDir->Remove(true);
+ }
+ });
+
+ if (aInBackground) {
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ } else {
+ runnable->Run();
+ }
+}
+
+nsToolkitProfile::nsToolkitProfile(const nsACString& aName, nsIFile* aRootDir,
+ nsIFile* aLocalDir, bool aFromDB)
+ : mName(aName),
+ mRootDir(aRootDir),
+ mLocalDir(aLocalDir),
+ mLock(nullptr),
+ mIndex(0),
+ mSection("Profile") {
+ NS_ASSERTION(aRootDir, "No file!");
+
+ RefPtr<nsToolkitProfile> prev =
+ nsToolkitProfileService::gService->mProfiles.getLast();
+ if (prev) {
+ mIndex = prev->mIndex + 1;
+ }
+ mSection.AppendInt(mIndex);
+
+ nsToolkitProfileService::gService->mProfiles.insertBack(this);
+
+ // If this profile isn't in the database already add it.
+ if (!aFromDB) {
+ nsINIParser* db = &nsToolkitProfileService::gService->mProfileDB;
+ db->SetString(mSection.get(), "Name", mName.get());
+
+ bool isRelative = false;
+ nsCString descriptor;
+ nsToolkitProfileService::gService->GetProfileDescriptor(this, descriptor,
+ &isRelative);
+
+ db->SetString(mSection.get(), "IsRelative", isRelative ? "1" : "0");
+ db->SetString(mSection.get(), "Path", descriptor.get());
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsToolkitProfile, nsIToolkitProfile)
+
+NS_IMETHODIMP
+nsToolkitProfile::GetRootDir(nsIFile** aResult) {
+ NS_ADDREF(*aResult = mRootDir);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfile::GetLocalDir(nsIFile** aResult) {
+ NS_ADDREF(*aResult = mLocalDir);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfile::GetName(nsACString& aResult) {
+ aResult = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfile::SetName(const nsACString& aName) {
+ NS_ASSERTION(nsToolkitProfileService::gService, "Where did my service go?");
+
+ if (mName.Equals(aName)) {
+ return NS_OK;
+ }
+
+ // Changing the name from the dev-edition default profile name makes this
+ // profile no longer the dev-edition default.
+ if (mName.EqualsLiteral(DEV_EDITION_NAME) &&
+ nsToolkitProfileService::gService->mDevEditionDefault == this) {
+ nsToolkitProfileService::gService->mDevEditionDefault = nullptr;
+ }
+
+ mName = aName;
+
+ nsresult rv = nsToolkitProfileService::gService->mProfileDB.SetString(
+ mSection.get(), "Name", mName.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setting the name to the dev-edition default profile name will cause this
+ // profile to become the dev-edition default.
+ if (aName.EqualsLiteral(DEV_EDITION_NAME) &&
+ !nsToolkitProfileService::gService->mDevEditionDefault) {
+ nsToolkitProfileService::gService->mDevEditionDefault = this;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsToolkitProfile::RemoveInternal(bool aRemoveFiles,
+ bool aInBackground) {
+ NS_ASSERTION(nsToolkitProfileService::gService, "Whoa, my service is gone.");
+
+ if (mLock) return NS_ERROR_FILE_IS_LOCKED;
+
+ if (!isInList()) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (aRemoveFiles) {
+ RemoveProfileFiles(this, aInBackground);
+ }
+
+ nsINIParser* db = &nsToolkitProfileService::gService->mProfileDB;
+ db->DeleteSection(mSection.get());
+
+ // We make some assumptions that the profile's index in the database is based
+ // on its position in the linked list. Removing a profile means we have to fix
+ // the index of later profiles in the list. The easiest way to do that is just
+ // to move the last profile into the profile's position and just update its
+ // index.
+ RefPtr<nsToolkitProfile> last =
+ nsToolkitProfileService::gService->mProfiles.getLast();
+ if (last != this) {
+ // Update the section in the db.
+ last->mIndex = mIndex;
+ db->RenameSection(last->mSection.get(), mSection.get());
+ last->mSection = mSection;
+
+ if (last != getNext()) {
+ last->remove();
+ setNext(last);
+ }
+ }
+
+ remove();
+
+ if (nsToolkitProfileService::gService->mNormalDefault == this) {
+ nsToolkitProfileService::gService->mNormalDefault = nullptr;
+ }
+ if (nsToolkitProfileService::gService->mDevEditionDefault == this) {
+ nsToolkitProfileService::gService->mDevEditionDefault = nullptr;
+ }
+ if (nsToolkitProfileService::gService->mDedicatedProfile == this) {
+ nsToolkitProfileService::gService->SetDefaultProfile(nullptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfile::Remove(bool removeFiles) {
+ return RemoveInternal(removeFiles, false /* in background */);
+}
+
+NS_IMETHODIMP
+nsToolkitProfile::RemoveInBackground(bool removeFiles) {
+ return RemoveInternal(removeFiles, true /* in background */);
+}
+
+NS_IMETHODIMP
+nsToolkitProfile::Lock(nsIProfileUnlocker** aUnlocker,
+ nsIProfileLock** aResult) {
+ if (mLock) {
+ NS_ADDREF(*aResult = mLock);
+ return NS_OK;
+ }
+
+ RefPtr<nsToolkitProfileLock> lock = new nsToolkitProfileLock();
+
+ nsresult rv = lock->Init(this, aUnlocker);
+ if (NS_FAILED(rv)) return rv;
+
+ NS_ADDREF(*aResult = lock);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsToolkitProfileLock, nsIProfileLock)
+
+nsresult nsToolkitProfileLock::Init(nsToolkitProfile* aProfile,
+ nsIProfileUnlocker** aUnlocker) {
+ nsresult rv;
+ rv = Init(aProfile->mRootDir, aProfile->mLocalDir, aUnlocker);
+ if (NS_SUCCEEDED(rv)) mProfile = aProfile;
+
+ return rv;
+}
+
+nsresult nsToolkitProfileLock::Init(nsIFile* aDirectory,
+ nsIFile* aLocalDirectory,
+ nsIProfileUnlocker** aUnlocker) {
+ nsresult rv;
+
+ rv = mLock.Lock(aDirectory, aUnlocker);
+
+ if (NS_SUCCEEDED(rv)) {
+ mDirectory = aDirectory;
+ mLocalDirectory = aLocalDirectory;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileLock::GetDirectory(nsIFile** aResult) {
+ if (!mDirectory) {
+ NS_ERROR("Not initialized, or unlocked!");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ADDREF(*aResult = mDirectory);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileLock::GetLocalDirectory(nsIFile** aResult) {
+ if (!mLocalDirectory) {
+ NS_ERROR("Not initialized, or unlocked!");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ADDREF(*aResult = mLocalDirectory);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileLock::Unlock() {
+ if (!mDirectory) {
+ NS_ERROR("Unlocking a never-locked nsToolkitProfileLock!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // XXX If we get here with an active quota manager,
+ // something went very wrong. We want to assert this.
+
+ mLock.Unlock();
+
+ if (mProfile) {
+ mProfile->mLock = nullptr;
+ mProfile = nullptr;
+ }
+ mDirectory = nullptr;
+ mLocalDirectory = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileLock::GetReplacedLockTime(PRTime* aResult) {
+ mLock.GetReplacedLockTime(aResult);
+ return NS_OK;
+}
+
+nsToolkitProfileLock::~nsToolkitProfileLock() {
+ if (mDirectory) {
+ Unlock();
+ }
+}
+
+nsToolkitProfileService* nsToolkitProfileService::gService = nullptr;
+
+NS_IMPL_ISUPPORTS(nsToolkitProfileService, nsIToolkitProfileService)
+
+nsToolkitProfileService::nsToolkitProfileService()
+ : mStartupProfileSelected(false),
+ mStartWithLast(true),
+ mIsFirstRun(true),
+ mUseDevEditionProfile(false),
+#ifdef MOZ_DEDICATED_PROFILES
+ mUseDedicatedProfile(!IsSnapEnvironment() && !UseLegacyProfiles()),
+#else
+ mUseDedicatedProfile(false),
+#endif
+ mStartupReason(u"unknown"_ns),
+ mStartupFileVersion("0"_ns),
+ mMaybeLockProfile(false),
+ mUpdateChannel(MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL)),
+ mProfileDBExists(false),
+ mProfileDBFileSize(0),
+ mProfileDBModifiedTime(0) {
+#ifdef MOZ_DEV_EDITION
+ mUseDevEditionProfile = true;
+#endif
+}
+
+nsToolkitProfileService::~nsToolkitProfileService() {
+ gService = nullptr;
+ mProfiles.clear();
+}
+
+void nsToolkitProfileService::CompleteStartup() {
+ if (!mStartupProfileSelected) {
+ return;
+ }
+
+ ScalarSet(mozilla::Telemetry::ScalarID::STARTUP_PROFILE_SELECTION_REASON,
+ mStartupReason);
+ ScalarSet(mozilla::Telemetry::ScalarID::STARTUP_PROFILE_DATABASE_VERSION,
+ NS_ConvertUTF8toUTF16(mStartupFileVersion));
+ ScalarSet(mozilla::Telemetry::ScalarID::STARTUP_PROFILE_COUNT,
+ static_cast<uint32_t>(mProfiles.length()));
+
+ if (mMaybeLockProfile) {
+ nsCOMPtr<nsIToolkitShellService> shell =
+ do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID);
+ if (!shell) {
+ return;
+ }
+
+ 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());
+ }
+ }
+}
+
+// Tests whether the passed profile was last used by this install.
+bool nsToolkitProfileService::IsProfileForCurrentInstall(
+ nsIToolkitProfile* aProfile) {
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIFile> compatFile;
+ rv = profileDir->Clone(getter_AddRefs(compatFile));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = compatFile->Append(COMPAT_FILE);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsINIParser compatData;
+ rv = compatData.Init(compatFile);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ /**
+ * In xpcshell gDirServiceProvider doesn't have all the correct directories
+ * set so using NS_GetSpecialDirectory works better there. But in a normal
+ * app launch the component registry isn't initialized so
+ * NS_GetSpecialDirectory doesn't work. So we have to use two different
+ * paths to support testing.
+ */
+ nsCOMPtr<nsIFile> currentGreDir;
+ rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(currentGreDir));
+ if (rv == NS_ERROR_NOT_INITIALIZED) {
+ currentGreDir = gDirServiceProvider->GetGREDir();
+ MOZ_ASSERT(currentGreDir, "No GRE dir found.");
+ } else if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsCString lastGreDirStr;
+ rv = compatData.GetString("Compatibility", "LastPlatformDir", lastGreDirStr);
+ // If this string is missing then this profile is from an ancient version.
+ // We'll opt to use it in this case.
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+
+ nsCOMPtr<nsIFile> lastGreDir;
+ rv = NS_NewNativeLocalFile(""_ns, false, getter_AddRefs(lastGreDir));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = lastGreDir->SetPersistentDescriptor(lastGreDirStr);
+ NS_ENSURE_SUCCESS(rv, false);
+
+#ifdef XP_WIN
+# if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE)
+ mozilla::PathString lastGreDirPath, currentGreDirPath;
+ lastGreDirPath = lastGreDir->NativePath();
+ currentGreDirPath = currentGreDir->NativePath();
+ if (lastGreDirPath.Equals(currentGreDirPath,
+ nsCaseInsensitiveStringComparator)) {
+ return true;
+ }
+
+ // Convert a 64-bit install path to what would have been the 32-bit install
+ // path to allow users to migrate their profiles from one to the other.
+ PWSTR pathX86 = nullptr;
+ HRESULT hres =
+ SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, 0, nullptr, &pathX86);
+ if (SUCCEEDED(hres)) {
+ nsDependentString strPathX86(pathX86);
+ if (!StringBeginsWith(currentGreDirPath, strPathX86,
+ nsCaseInsensitiveStringComparator)) {
+ PWSTR path = nullptr;
+ hres = SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &path);
+ if (SUCCEEDED(hres)) {
+ if (StringBeginsWith(currentGreDirPath, nsDependentString(path),
+ nsCaseInsensitiveStringComparator)) {
+ currentGreDirPath.Replace(0, wcslen(path), strPathX86);
+ }
+ }
+ CoTaskMemFree(path);
+ }
+ }
+ CoTaskMemFree(pathX86);
+
+ return lastGreDirPath.Equals(currentGreDirPath,
+ nsCaseInsensitiveStringComparator);
+# endif
+#endif
+
+ bool equal;
+ rv = lastGreDir->Equals(currentGreDir, &equal);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return equal;
+}
+
+/**
+ * Used the first time an install with dedicated profile support runs. Decides
+ * whether to mark the passed profile as the default for this install.
+ *
+ * The goal is to reduce disruption but ideally end up with the OS default
+ * install using the old default profile.
+ *
+ * If the decision is to use the profile then it will be unassigned as the
+ * dedicated default for other installs.
+ *
+ * We won't attempt to use the profile if it was last used by a different
+ * install.
+ *
+ * If the profile is currently in use by an install that was either the OS
+ * default install or the profile has been explicitely chosen by some other
+ * means then we won't use it.
+ *
+ * aResult will be set to true if we chose to make the profile the new dedicated
+ * default.
+ */
+nsresult nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
+ nsIToolkitProfile* aProfile, bool* aResult) {
+ nsresult rv;
+ *aResult = false;
+
+ // If the profile was last used by a different install then we won't use it.
+ if (!IsProfileForCurrentInstall(aProfile)) {
+ return NS_OK;
+ }
+
+ nsCString descriptor;
+ rv = GetProfileDescriptor(aProfile, descriptor, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get a list of all the installs.
+ nsTArray<nsCString> installs = GetKnownInstalls();
+
+ // Cache the installs that use the profile.
+ nsTArray<nsCString> inUseInstalls;
+
+ // See if the profile is already in use by an install that hasn't locked it.
+ for (uint32_t i = 0; i < installs.Length(); i++) {
+ const nsCString& install = installs[i];
+
+ nsCString path;
+ rv = mProfileDB.GetString(install.get(), "Default", path);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ // Is this install using the profile we care about?
+ if (!descriptor.Equals(path)) {
+ continue;
+ }
+
+ // Is this profile locked to this other install?
+ nsCString isLocked;
+ rv = mProfileDB.GetString(install.get(), "Locked", isLocked);
+ if (NS_SUCCEEDED(rv) && isLocked.Equals("1")) {
+ return NS_OK;
+ }
+
+ inUseInstalls.AppendElement(install);
+ }
+
+ // At this point we've decided to take the profile. Strip it from other
+ // installs.
+ for (uint32_t i = 0; i < inUseInstalls.Length(); i++) {
+ // Removing the default setting entirely will make the install go through
+ // the first run process again at startup and create itself a new profile.
+ mProfileDB.DeleteString(inUseInstalls[i].get(), "Default");
+ }
+
+ // Set this as the default profile for this install.
+ SetDefaultProfile(aProfile);
+
+ // SetDefaultProfile will have locked this profile to this install so no
+ // other installs will steal it, but this was auto-selected so we want to
+ // unlock it so that other installs can potentially take it.
+ mProfileDB.DeleteString(mInstallSection.get(), "Locked");
+
+ // Persist the changes.
+ rv = Flush();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Once XPCOM is available check if this is the default application and if so
+ // lock the profile again.
+ mMaybeLockProfile = true;
+ *aResult = true;
+
+ return NS_OK;
+}
+
+bool IsFileOutdated(nsIFile* aFile, bool aExists, PRTime aLastModified,
+ int64_t aLastSize) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = aFile->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ bool exists;
+ rv = aFile->Exists(&exists);
+ if (NS_FAILED(rv) || exists != aExists) {
+ return true;
+ }
+
+ if (!exists) {
+ return false;
+ }
+
+ int64_t size;
+ rv = aFile->GetFileSize(&size);
+ if (NS_FAILED(rv) || size != aLastSize) {
+ return true;
+ }
+
+ PRTime time;
+ rv = aFile->GetLastModifiedTime(&time);
+ if (NS_FAILED(rv) || time != aLastModified) {
+ return true;
+ }
+
+ return false;
+}
+
+nsresult UpdateFileStats(nsIFile* aFile, bool* aExists, PRTime* aLastModified,
+ int64_t* aLastSize) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = aFile->Clone(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = file->Exists(aExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!(*aExists)) {
+ *aLastModified = 0;
+ *aLastSize = 0;
+ return NS_OK;
+ }
+
+ rv = file->GetFileSize(aLastSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = file->GetLastModifiedTime(aLastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetIsListOutdated(bool* aResult) {
+ if (IsFileOutdated(mProfileDBFile, mProfileDBExists, mProfileDBModifiedTime,
+ mProfileDBFileSize)) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ *aResult = false;
+ return NS_OK;
+}
+
+struct ImportInstallsClosure {
+ nsINIParser* backupData;
+ nsINIParser* profileDB;
+};
+
+static bool ImportInstalls(const char* aSection, void* aClosure) {
+ ImportInstallsClosure* closure =
+ static_cast<ImportInstallsClosure*>(aClosure);
+
+ nsTArray<UniquePtr<KeyValue>> strings =
+ GetSectionStrings(closure->backupData, aSection);
+ if (strings.IsEmpty()) {
+ return true;
+ }
+
+ nsCString newSection(INSTALL_PREFIX);
+ newSection.Append(aSection);
+ nsCString buffer;
+
+ for (uint32_t i = 0; i < strings.Length(); i++) {
+ closure->profileDB->SetString(newSection.get(), strings[i]->key.get(),
+ strings[i]->value.get());
+ }
+
+ return true;
+}
+
+nsresult nsToolkitProfileService::Init() {
+ NS_ASSERTION(gDirServiceProvider, "No dirserviceprovider!");
+ nsresult rv;
+
+ rv = nsXREDirProvider::GetUserAppDataDirectory(getter_AddRefs(mAppData));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsXREDirProvider::GetUserLocalDataDirectory(getter_AddRefs(mTempData));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mAppData->Clone(getter_AddRefs(mProfileDBFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mProfileDBFile->AppendNative("profiles.ini"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mAppData->Clone(getter_AddRefs(mInstallDBFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mInstallDBFile->AppendNative("installs.ini"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString buffer;
+
+ rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists,
+ &mProfileDBModifiedTime, &mProfileDBFileSize);
+ if (NS_SUCCEEDED(rv) && mProfileDBExists) {
+ rv = mProfileDB.Init(mProfileDBFile);
+ // Init does not fail on parsing errors, only on OOM/really unexpected
+ // conditions.
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = mProfileDB.GetString("General", "StartWithLastProfile", buffer);
+ if (NS_SUCCEEDED(rv)) {
+ mStartWithLast = !buffer.EqualsLiteral("0");
+ }
+
+ rv = mProfileDB.GetString("General", "Version", mStartupFileVersion);
+ if (NS_FAILED(rv)) {
+ // This is a profiles.ini written by an older version. We must restore
+ // any install data from the backup. We consider this old format to be
+ // a version 1 file.
+ mStartupFileVersion.AssignLiteral("1");
+ nsINIParser installDB;
+
+ if (NS_SUCCEEDED(installDB.Init(mInstallDBFile))) {
+ // There is install data to import.
+ ImportInstallsClosure closure = {&installDB, &mProfileDB};
+ installDB.GetSections(&ImportInstalls, &closure);
+ }
+
+ rv = mProfileDB.SetString("General", "Version", PROFILE_DB_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ rv = mProfileDB.SetString("General", "StartWithLastProfile",
+ mStartWithLast ? "1" : "0");
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mProfileDB.SetString("General", "Version", PROFILE_DB_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString installProfilePath;
+
+ if (mUseDedicatedProfile) {
+ nsString installHash;
+ rv = gDirServiceProvider->GetInstallHash(installHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF16toUTF8(installHash, mInstallSection);
+ mInstallSection.Insert(INSTALL_PREFIX, 0);
+
+ // Try to find the descriptor for the default profile for this install.
+ rv = mProfileDB.GetString(mInstallSection.get(), "Default",
+ installProfilePath);
+
+ // Not having a value means this install doesn't appear in installs.ini so
+ // this is the first run for this install.
+ if (NS_FAILED(rv)) {
+ mIsFirstRun = true;
+
+ // Gets the install section that would have been created if the install
+ // path has incorrect casing (see bug 1555319). We use this later during
+ // profile selection.
+ rv = gDirServiceProvider->GetLegacyInstallHash(installHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF16toUTF8(installHash, mLegacyInstallSection);
+ mLegacyInstallSection.Insert(INSTALL_PREFIX, 0);
+ } else {
+ mIsFirstRun = false;
+ }
+ }
+
+ nsToolkitProfile* currentProfile = nullptr;
+
+#ifdef MOZ_DEV_EDITION
+ nsCOMPtr<nsIFile> ignoreDevEditionProfile;
+ rv = mAppData->Clone(getter_AddRefs(ignoreDevEditionProfile));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = ignoreDevEditionProfile->AppendNative("ignore-dev-edition-profile"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ bool shouldIgnoreSeparateProfile;
+ rv = ignoreDevEditionProfile->Exists(&shouldIgnoreSeparateProfile);
+ if (NS_FAILED(rv)) return rv;
+
+ mUseDevEditionProfile = !shouldIgnoreSeparateProfile;
+#endif
+
+ nsCOMPtr<nsIToolkitProfile> autoSelectProfile;
+
+ unsigned int nonDevEditionProfiles = 0;
+ unsigned int c = 0;
+ for (c = 0; true; ++c) {
+ nsAutoCString profileID("Profile");
+ profileID.AppendInt(c);
+
+ rv = mProfileDB.GetString(profileID.get(), "IsRelative", buffer);
+ if (NS_FAILED(rv)) break;
+
+ bool isRelative = buffer.EqualsLiteral("1");
+
+ nsAutoCString filePath;
+
+ rv = mProfileDB.GetString(profileID.get(), "Path", filePath);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Malformed profiles.ini: Path= not found");
+ continue;
+ }
+
+ nsAutoCString name;
+
+ rv = mProfileDB.GetString(profileID.get(), "Name", name);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Malformed profiles.ini: Name= not found");
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> rootDir;
+ rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(rootDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isRelative) {
+ rv = rootDir->SetRelativeDescriptor(mAppData, filePath);
+ } else {
+ rv = rootDir->SetPersistentDescriptor(filePath);
+ }
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIFile> localDir;
+ if (isRelative) {
+ rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(localDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = localDir->SetRelativeDescriptor(mTempData, filePath);
+ } else {
+ localDir = rootDir;
+ }
+
+ currentProfile = new nsToolkitProfile(name, rootDir, localDir, true);
+
+ // If a user has modified the ini file path it may make for a valid profile
+ // path but not match what we would have serialised and so may not match
+ // the path in the install section. Re-serialise it to get it in the
+ // expected form again.
+ bool nowRelative;
+ nsCString descriptor;
+ GetProfileDescriptor(currentProfile, descriptor, &nowRelative);
+
+ if (isRelative != nowRelative || !descriptor.Equals(filePath)) {
+ mProfileDB.SetString(profileID.get(), "IsRelative",
+ nowRelative ? "1" : "0");
+ mProfileDB.SetString(profileID.get(), "Path", descriptor.get());
+
+ // Should we flush now? It costs some startup time and we will fix it on
+ // the next startup anyway. If something else causes a flush then it will
+ // be fixed in the ini file then.
+ }
+
+ rv = mProfileDB.GetString(profileID.get(), "Default", buffer);
+ if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("1")) {
+ mNormalDefault = currentProfile;
+ }
+
+ // Is this the default profile for this install?
+ if (mUseDedicatedProfile && !mDedicatedProfile &&
+ installProfilePath.Equals(descriptor)) {
+ // Found a profile for this install.
+ mDedicatedProfile = currentProfile;
+ }
+
+ if (name.EqualsLiteral(DEV_EDITION_NAME)) {
+ mDevEditionDefault = currentProfile;
+ } else {
+ nonDevEditionProfiles++;
+ autoSelectProfile = currentProfile;
+ }
+ }
+
+ // If there is only one non-dev-edition profile then mark it as the default.
+ if (!mNormalDefault && nonDevEditionProfiles == 1) {
+ SetNormalDefault(autoSelectProfile);
+ }
+
+ if (!mUseDedicatedProfile) {
+ if (mUseDevEditionProfile) {
+ // When using the separate dev-edition profile not finding it means this
+ // is a first run.
+ mIsFirstRun = !mDevEditionDefault;
+ } else {
+ // If there are no normal profiles then this is a first run.
+ mIsFirstRun = nonDevEditionProfiles == 0;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::SetStartWithLastProfile(bool aValue) {
+ if (mStartWithLast != aValue) {
+ // Note: the skeleton ui (see PreXULSkeletonUI.cpp) depends on this
+ // having this name and being under General. If that ever changes,
+ // the skeleton UI will just need to be updated. If it changes frequently,
+ // it's probably best we just mirror the value to the registry here.
+ nsresult rv = mProfileDB.SetString("General", "StartWithLastProfile",
+ aValue ? "1" : "0");
+ NS_ENSURE_SUCCESS(rv, rv);
+ mStartWithLast = aValue;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetStartWithLastProfile(bool* aResult) {
+ *aResult = mStartWithLast;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetProfiles(nsISimpleEnumerator** aResult) {
+ *aResult = new ProfileEnumerator(mProfiles.getFirst());
+
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::ProfileEnumerator::HasMoreElements(bool* aResult) {
+ *aResult = mCurrent ? true : false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::ProfileEnumerator::GetNext(nsISupports** aResult) {
+ if (!mCurrent) return NS_ERROR_FAILURE;
+
+ NS_ADDREF(*aResult = mCurrent);
+
+ mCurrent = mCurrent->getNext();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetCurrentProfile(nsIToolkitProfile** aResult) {
+ NS_IF_ADDREF(*aResult = mCurrent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile** aResult) {
+ if (mUseDedicatedProfile) {
+ NS_IF_ADDREF(*aResult = mDedicatedProfile);
+ return NS_OK;
+ }
+
+ if (mUseDevEditionProfile) {
+ NS_IF_ADDREF(*aResult = mDevEditionDefault);
+ return NS_OK;
+ }
+
+ NS_IF_ADDREF(*aResult = mNormalDefault);
+ return NS_OK;
+}
+
+void nsToolkitProfileService::SetNormalDefault(nsIToolkitProfile* aProfile) {
+ if (mNormalDefault == aProfile) {
+ return;
+ }
+
+ if (mNormalDefault) {
+ nsToolkitProfile* profile =
+ static_cast<nsToolkitProfile*>(mNormalDefault.get());
+ mProfileDB.DeleteString(profile->mSection.get(), "Default");
+ }
+
+ mNormalDefault = aProfile;
+
+ if (mNormalDefault) {
+ nsToolkitProfile* profile =
+ static_cast<nsToolkitProfile*>(mNormalDefault.get());
+ mProfileDB.SetString(profile->mSection.get(), "Default", "1");
+ }
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile) {
+ if (mUseDedicatedProfile) {
+ if (mDedicatedProfile != aProfile) {
+ if (!aProfile) {
+ // Setting this to the empty string means no profile will be found on
+ // startup but we'll recognise that this install has been used
+ // previously.
+ mProfileDB.SetString(mInstallSection.get(), "Default", "");
+ } else {
+ nsCString profilePath;
+ nsresult rv = GetProfileDescriptor(aProfile, profilePath, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mProfileDB.SetString(mInstallSection.get(), "Default",
+ profilePath.get());
+ }
+ mDedicatedProfile = aProfile;
+
+ // Some kind of choice has happened here, lock this profile to this
+ // install.
+ mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
+ }
+ return NS_OK;
+ }
+
+ if (mUseDevEditionProfile && aProfile != mDevEditionDefault) {
+ // The separate profile is hardcoded.
+ return NS_ERROR_FAILURE;
+ }
+
+ SetNormalDefault(aProfile);
+
+ return NS_OK;
+}
+
+// Gets the profile root directory descriptor for storing in profiles.ini or
+// installs.ini.
+nsresult nsToolkitProfileService::GetProfileDescriptor(
+ nsIToolkitProfile* aProfile, nsACString& aDescriptor, bool* aIsRelative) {
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if the profile dir is relative to appdir...
+ bool isRelative;
+ rv = mAppData->Contains(profileDir, &isRelative);
+
+ nsCString profilePath;
+ if (NS_SUCCEEDED(rv) && isRelative) {
+ // we use a relative descriptor
+ rv = profileDir->GetRelativeDescriptor(mAppData, profilePath);
+ } else {
+ // otherwise, a persistent descriptor
+ rv = profileDir->GetPersistentDescriptor(profilePath);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aDescriptor.Assign(profilePath);
+ if (aIsRelative) {
+ *aIsRelative = isRelative;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsToolkitProfileService::CreateDefaultProfile(
+ nsIToolkitProfile** aResult) {
+ // Create a new default profile
+ nsAutoCString name;
+ if (mUseDevEditionProfile) {
+ name.AssignLiteral(DEV_EDITION_NAME);
+ } else if (mUseDedicatedProfile) {
+ name.AppendPrintf("default-%s", mUpdateChannel.get());
+ } else {
+ name.AssignLiteral(DEFAULT_NAME);
+ }
+
+ nsresult rv = CreateUniqueProfile(nullptr, name, aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mUseDedicatedProfile) {
+ SetDefaultProfile(mCurrent);
+ } else if (mUseDevEditionProfile) {
+ mDevEditionDefault = mCurrent;
+ } else {
+ SetNormalDefault(mCurrent);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * An implementation of SelectStartupProfile callable from JavaScript via XPCOM.
+ * See nsIToolkitProfileService.idl.
+ */
+NS_IMETHODIMP
+nsToolkitProfileService::SelectStartupProfile(
+ const nsTArray<nsCString>& aArgv, bool aIsResetting,
+ const nsACString& aUpdateChannel, const nsACString& aLegacyInstallHash,
+ nsIFile** aRootDir, nsIFile** aLocalDir, nsIToolkitProfile** aProfile,
+ bool* aDidCreate) {
+ int argc = aArgv.Length();
+ // Our command line handling expects argv to be null-terminated so construct
+ // an appropriate array.
+ auto argv = MakeUnique<char*[]>(argc + 1);
+ // Also, our command line handling removes things from the array without
+ // freeing them so keep track of what we've created separately.
+ auto allocated = MakeUnique<UniqueFreePtr<char>[]>(argc);
+
+ for (int i = 0; i < argc; i++) {
+ allocated[i].reset(ToNewCString(aArgv[i]));
+ argv[i] = allocated[i].get();
+ }
+ argv[argc] = nullptr;
+
+ mUpdateChannel = aUpdateChannel;
+ if (!aLegacyInstallHash.IsEmpty()) {
+ mLegacyInstallSection.Assign(aLegacyInstallHash);
+ mLegacyInstallSection.Insert(INSTALL_PREFIX, 0);
+ }
+
+ bool wasDefault;
+ nsresult rv =
+ SelectStartupProfile(&argc, argv.get(), aIsResetting, aRootDir, aLocalDir,
+ aProfile, aDidCreate, &wasDefault);
+
+ // Since we were called outside of the normal startup path complete any
+ // startup tasks.
+ if (NS_SUCCEEDED(rv)) {
+ CompleteStartup();
+ }
+
+ return rv;
+}
+
+static void SaltProfileName(nsACString& aName);
+
+/**
+ * Selects or creates a profile to use based on the profiles database, any
+ * environment variables and any command line arguments. Will not create
+ * a profile if aIsResetting is true. The profile is selected based on this
+ * order of preference:
+ * * Environment variables (set when restarting the application).
+ * * --profile command line argument.
+ * * --createprofile command line argument (this also causes the app to exit).
+ * * -p command line argument.
+ * * A new profile created if this is the first run of the application.
+ * * The default profile.
+ * aRootDir and aLocalDir are set to the data and local directories for the
+ * profile data. If a profile from the database was selected it will be
+ * returned in aProfile.
+ * aDidCreate will be set to true if a new profile was created.
+ * This function should be called once at startup and will fail if called again.
+ * aArgv should be an array of aArgc + 1 strings, the last element being null.
+ * Both aArgv and aArgc will be mutated.
+ */
+nsresult nsToolkitProfileService::SelectStartupProfile(
+ int* aArgc, char* aArgv[], bool aIsResetting, nsIFile** aRootDir,
+ nsIFile** aLocalDir, nsIToolkitProfile** aProfile, bool* aDidCreate,
+ bool* aWasDefaultSelection) {
+ if (mStartupProfileSelected) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ mStartupProfileSelected = true;
+ *aDidCreate = false;
+ *aWasDefaultSelection = false;
+
+ nsresult rv;
+ const char* arg;
+
+ // Use the profile specified in the environment variables (generally from an
+ // app initiated restart).
+ nsCOMPtr<nsIFile> lf = GetFileFromEnv("XRE_PROFILE_PATH");
+ if (lf) {
+ nsCOMPtr<nsIFile> localDir = GetFileFromEnv("XRE_PROFILE_LOCAL_PATH");
+ if (!localDir) {
+ localDir = lf;
+ }
+
+ // Clear out flags that we handled (or should have handled!) last startup.
+ const char* dummy;
+ CheckArg(*aArgc, aArgv, "p", &dummy);
+ CheckArg(*aArgc, aArgv, "profile", &dummy);
+ CheckArg(*aArgc, aArgv, "profilemanager");
+
+ nsCOMPtr<nsIToolkitProfile> profile;
+ GetProfileByDir(lf, localDir, getter_AddRefs(profile));
+
+ if (profile && mIsFirstRun && mUseDedicatedProfile) {
+ if (profile ==
+ (mUseDevEditionProfile ? mDevEditionDefault : mNormalDefault)) {
+ // This is the first run of a dedicated profile build where the selected
+ // profile is the previous default so we should either make it the
+ // default profile for this install or push the user to a new profile.
+
+ bool result;
+ rv = MaybeMakeDefaultDedicatedProfile(profile, &result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (result) {
+ mStartupReason = u"restart-claimed-default"_ns;
+
+ mCurrent = profile;
+ } else {
+ rv = CreateDefaultProfile(getter_AddRefs(mCurrent));
+ if (NS_FAILED(rv)) {
+ *aProfile = nullptr;
+ return rv;
+ }
+
+ rv = Flush();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mStartupReason = u"restart-skipped-default"_ns;
+ *aDidCreate = true;
+ }
+
+ NS_IF_ADDREF(*aProfile = mCurrent);
+ mCurrent->GetRootDir(aRootDir);
+ mCurrent->GetLocalDir(aLocalDir);
+
+ return NS_OK;
+ }
+ }
+
+ if (EnvHasValue("XRE_RESTARTED_BY_PROFILE_MANAGER")) {
+ mStartupReason = u"profile-manager"_ns;
+ } else if (aIsResetting) {
+ mStartupReason = u"profile-reset"_ns;
+ } else {
+ mStartupReason = u"restart"_ns;
+ }
+
+ mCurrent = profile;
+ lf.forget(aRootDir);
+ localDir.forget(aLocalDir);
+ NS_IF_ADDREF(*aProfile = profile);
+ return NS_OK;
+ }
+
+ // Check the -profile command line argument. It accepts a single argument that
+ // gives the path to use for the profile.
+ ArgResult ar = CheckArg(*aArgc, aArgv, "profile", &arg);
+ if (ar == ARG_BAD) {
+ PR_fprintf(PR_STDERR, "Error: argument --profile requires a path\n");
+ return NS_ERROR_FAILURE;
+ }
+ if (ar) {
+ nsCOMPtr<nsIFile> lf;
+ rv = XRE_GetFileFromPath(arg, getter_AddRefs(lf));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make sure that the profile path exists and it's a directory.
+ bool exists;
+ rv = lf->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) {
+ rv = lf->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ bool isDir;
+ rv = lf->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) {
+ PR_fprintf(
+ PR_STDERR,
+ "Error: argument --profile requires a path to a directory\n");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ mStartupReason = u"argument-profile"_ns;
+
+ GetProfileByDir(lf, nullptr, getter_AddRefs(mCurrent));
+ NS_ADDREF(*aRootDir = lf);
+ // If the root dir matched a profile then use its local dir, otherwise use
+ // the root dir as the local dir.
+ if (mCurrent) {
+ mCurrent->GetLocalDir(aLocalDir);
+ } else {
+ lf.forget(aLocalDir);
+ }
+
+ NS_IF_ADDREF(*aProfile = mCurrent);
+ return NS_OK;
+ }
+
+ // Check the -createprofile command line argument. It accepts a single
+ // argument that is either the name for the new profile or the name followed
+ // by the path to use.
+ ar = CheckArg(*aArgc, aArgv, "createprofile", &arg, CheckArgFlag::RemoveArg);
+ if (ar == ARG_BAD) {
+ PR_fprintf(PR_STDERR,
+ "Error: argument --createprofile requires a profile name\n");
+ return NS_ERROR_FAILURE;
+ }
+ if (ar) {
+ const char* delim = strchr(arg, ' ');
+ nsCOMPtr<nsIToolkitProfile> profile;
+ if (delim) {
+ nsCOMPtr<nsIFile> lf;
+ rv = NS_NewNativeLocalFile(nsDependentCString(delim + 1), true,
+ getter_AddRefs(lf));
+ if (NS_FAILED(rv)) {
+ PR_fprintf(PR_STDERR, "Error: profile path not valid.\n");
+ return rv;
+ }
+
+ // As with --profile, assume that the given path will be used for the
+ // main profile directory.
+ rv = CreateProfile(lf, nsDependentCSubstring(arg, delim),
+ getter_AddRefs(profile));
+ } else {
+ rv = CreateProfile(nullptr, nsDependentCString(arg),
+ getter_AddRefs(profile));
+ }
+ // Some pathological arguments can make it this far
+ if (NS_FAILED(rv) || NS_FAILED(Flush())) {
+ PR_fprintf(PR_STDERR, "Error creating profile.\n");
+ }
+ return NS_ERROR_ABORT;
+ }
+
+ // Check the -p command line argument. It either accepts a profile name and
+ // uses that named profile or without a name it opens the profile manager.
+ ar = CheckArg(*aArgc, aArgv, "p", &arg);
+ if (ar == ARG_BAD) {
+ return NS_ERROR_SHOW_PROFILE_MANAGER;
+ }
+ if (ar) {
+ rv = GetProfileByName(nsDependentCString(arg), getter_AddRefs(mCurrent));
+ if (NS_SUCCEEDED(rv)) {
+ mStartupReason = u"argument-p"_ns;
+
+ mCurrent->GetRootDir(aRootDir);
+ mCurrent->GetLocalDir(aLocalDir);
+
+ NS_ADDREF(*aProfile = mCurrent);
+ return NS_OK;
+ }
+
+ return NS_ERROR_SHOW_PROFILE_MANAGER;
+ }
+
+ ar = CheckArg(*aArgc, aArgv, "profilemanager");
+ if (ar == ARG_FOUND) {
+ return NS_ERROR_SHOW_PROFILE_MANAGER;
+ }
+
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ // There are two cases:
+ // 1. ephemeral profile: create a new one in temporary directory.
+ // 2. non-ephemeral (persistent) profile:
+ // a. if no salted profile is known, create a new one in
+ // background task-specific directory.
+ // b. if salted profile is know, use salted path.
+ nsString installHash;
+ rv = gDirServiceProvider->GetInstallHash(installHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString profilePrefix(BackgroundTasks::GetProfilePrefix(
+ NS_LossyConvertUTF16toASCII(installHash)));
+
+ nsCString taskName(BackgroundTasks::GetBackgroundTasks().ref());
+
+ nsCOMPtr<nsIFile> file;
+
+ if (BackgroundTasks::IsEphemeralProfileTaskName(taskName)) {
+ // Background task mode does not enable legacy telemetry, so this is for
+ // completeness and testing only.
+ mStartupReason = u"backgroundtask-ephemeral"_ns;
+
+ nsCOMPtr<nsIFile> rootDir;
+ rv = GetSpecialSystemDirectory(OS_TemporaryDirectory,
+ getter_AddRefs(rootDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsresult rv = BackgroundTasks::CreateEphemeralProfileDirectory(
+ rootDir, profilePrefix, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // In background task mode, NS_ERROR_UNEXPECTED is handled specially to
+ // exit with a non-zero exit code.
+ return NS_ERROR_UNEXPECTED;
+ }
+ *aDidCreate = true;
+ } else {
+ // Background task mode does not enable legacy telemetry, so this is for
+ // completeness and testing only.
+ mStartupReason = u"backgroundtask-not-ephemeral"_ns;
+
+ // A non-ephemeral profile is required.
+ nsCOMPtr<nsIFile> rootDir;
+ nsresult rv = gDirServiceProvider->GetBackgroundTasksProfilesRootDir(
+ getter_AddRefs(rootDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString buffer;
+ rv = mProfileDB.GetString("BackgroundTasksProfiles", profilePrefix.get(),
+ buffer);
+ if (NS_SUCCEEDED(rv)) {
+ // We have a record of one! Use it.
+ rv = rootDir->Clone(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = file->AppendNative(buffer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCString saltedProfilePrefix = profilePrefix;
+ SaltProfileName(saltedProfilePrefix);
+
+ nsresult rv = BackgroundTasks::CreateNonEphemeralProfileDirectory(
+ rootDir, saltedProfilePrefix, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // In background task mode, NS_ERROR_UNEXPECTED is handled specially
+ // to exit with a non-zero exit code.
+ return NS_ERROR_UNEXPECTED;
+ }
+ *aDidCreate = true;
+
+ // Keep a record of the salted name. It's okay if this doesn't succeed:
+ // not great, but it's better for tasks (particularly,
+ // `backgroundupdate`) to run and not persist state correctly than to
+ // not run at all.
+ rv =
+ mProfileDB.SetString("BackgroundTasksProfiles", profilePrefix.get(),
+ saltedProfilePrefix.get());
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = Flush();
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+ }
+ }
+
+ nsCOMPtr<nsIFile> localDir = file;
+ file.forget(aRootDir);
+ localDir.forget(aLocalDir);
+
+ // Background tasks never use profiles known to the profile service.
+ *aProfile = nullptr;
+
+ return NS_OK;
+ }
+#endif
+
+ if (mIsFirstRun && mUseDedicatedProfile &&
+ !mInstallSection.Equals(mLegacyInstallSection)) {
+ // The default profile could be assigned to a hash generated from an
+ // incorrectly cased version of the installation directory (see bug
+ // 1555319). Ideally we'd do all this while loading profiles.ini but we
+ // can't override the legacy section value before that for tests.
+ nsCString defaultDescriptor;
+ rv = mProfileDB.GetString(mLegacyInstallSection.get(), "Default",
+ defaultDescriptor);
+
+ if (NS_SUCCEEDED(rv)) {
+ // There is a default here, need to see if it matches any profiles.
+ bool isRelative;
+ nsCString descriptor;
+
+ for (RefPtr<nsToolkitProfile> profile : mProfiles) {
+ GetProfileDescriptor(profile, descriptor, &isRelative);
+
+ if (descriptor.Equals(defaultDescriptor)) {
+ // Found the default profile. Copy the install section over to
+ // the correct location. We leave the old info in place for older
+ // versions of Firefox to use.
+ nsTArray<UniquePtr<KeyValue>> strings =
+ GetSectionStrings(&mProfileDB, mLegacyInstallSection.get());
+ for (const auto& kv : strings) {
+ mProfileDB.SetString(mInstallSection.get(), kv->key.get(),
+ kv->value.get());
+ }
+
+ // Flush now. This causes a small blip in startup but it should be
+ // one time only whereas not flushing means we have to do this search
+ // on every startup.
+ Flush();
+
+ // Now start up with the found profile.
+ mDedicatedProfile = profile;
+ mIsFirstRun = false;
+ break;
+ }
+ }
+ }
+ }
+
+ // If this is a first run then create a new profile.
+ if (mIsFirstRun) {
+ // If we're configured to always show the profile manager then don't create
+ // a new profile to use.
+ if (!mStartWithLast) {
+ return NS_ERROR_SHOW_PROFILE_MANAGER;
+ }
+
+ bool skippedDefaultProfile = false;
+
+ if (mUseDedicatedProfile) {
+ // This is the first run of a dedicated profile install. We have to decide
+ // whether to use the default profile used by non-dedicated-profile
+ // installs or to create a new profile.
+
+ // Find what would have been the default profile for old installs.
+ nsCOMPtr<nsIToolkitProfile> profile = mNormalDefault;
+ if (mUseDevEditionProfile) {
+ profile = mDevEditionDefault;
+ }
+
+ if (profile) {
+ nsCOMPtr<nsIFile> rootDir;
+ profile->GetRootDir(getter_AddRefs(rootDir));
+
+ nsCOMPtr<nsIFile> compat;
+ rootDir->Clone(getter_AddRefs(compat));
+ compat->Append(COMPAT_FILE);
+
+ bool exists;
+ rv = compat->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If the file is missing then either this is an empty profile (likely
+ // generated by bug 1518591) or it is from an ancient version. We'll opt
+ // to leave it for older versions in this case.
+ if (exists) {
+ bool result;
+ rv = MaybeMakeDefaultDedicatedProfile(profile, &result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (result) {
+ mStartupReason = u"firstrun-claimed-default"_ns;
+
+ mCurrent = profile;
+ rootDir.forget(aRootDir);
+ profile->GetLocalDir(aLocalDir);
+ profile.forget(aProfile);
+ return NS_OK;
+ }
+
+ // We're going to create a new profile for this install even though
+ // another default exists.
+ skippedDefaultProfile = true;
+ }
+ }
+ }
+
+ rv = CreateDefaultProfile(getter_AddRefs(mCurrent));
+ if (NS_SUCCEEDED(rv)) {
+#ifdef MOZ_CREATE_LEGACY_PROFILE
+ // If there is only one profile and it isn't meant to be the profile that
+ // older versions of Firefox use then we must create a default profile
+ // for older versions of Firefox to avoid the existing profile being
+ // auto-selected.
+ if ((mUseDedicatedProfile || mUseDevEditionProfile) &&
+ mProfiles.getFirst() == mProfiles.getLast()) {
+ nsCOMPtr<nsIToolkitProfile> newProfile;
+ CreateProfile(nullptr, nsLiteralCString(DEFAULT_NAME),
+ getter_AddRefs(newProfile));
+ SetNormalDefault(newProfile);
+ }
+#endif
+
+ rv = Flush();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (skippedDefaultProfile) {
+ mStartupReason = u"firstrun-skipped-default"_ns;
+ } else {
+ mStartupReason = u"firstrun-created-default"_ns;
+ }
+
+ // Use the new profile.
+ mCurrent->GetRootDir(aRootDir);
+ mCurrent->GetLocalDir(aLocalDir);
+ NS_ADDREF(*aProfile = mCurrent);
+
+ *aDidCreate = true;
+ return NS_OK;
+ }
+ }
+
+ GetDefaultProfile(getter_AddRefs(mCurrent));
+
+ // None of the profiles was marked as default (generally only happens if the
+ // user modifies profiles.ini manually). Let the user choose.
+ if (!mCurrent) {
+ return NS_ERROR_SHOW_PROFILE_MANAGER;
+ }
+
+ // Let the caller know that the profile was selected by default.
+ *aWasDefaultSelection = true;
+ mStartupReason = u"default"_ns;
+
+ // Use the selected profile.
+ mCurrent->GetRootDir(aRootDir);
+ mCurrent->GetLocalDir(aLocalDir);
+ NS_ADDREF(*aProfile = mCurrent);
+
+ return NS_OK;
+}
+
+/**
+ * Creates a new profile for reset and mark it as the current profile.
+ */
+nsresult nsToolkitProfileService::CreateResetProfile(
+ nsIToolkitProfile** aNewProfile) {
+ nsAutoCString oldProfileName;
+ mCurrent->GetName(oldProfileName);
+
+ nsCOMPtr<nsIToolkitProfile> newProfile;
+ // Make the new profile name the old profile (or "default-") + the time in
+ // seconds since epoch for uniqueness.
+ nsAutoCString newProfileName;
+ if (!oldProfileName.IsEmpty()) {
+ newProfileName.Assign(oldProfileName);
+ newProfileName.Append("-");
+ } else {
+ newProfileName.AssignLiteral("default-");
+ }
+ newProfileName.AppendPrintf("%" PRId64, PR_Now() / 1000);
+ nsresult rv = CreateProfile(nullptr, // choose a default dir for us
+ newProfileName, getter_AddRefs(newProfile));
+ if (NS_FAILED(rv)) return rv;
+
+ mCurrent = newProfile;
+ newProfile.forget(aNewProfile);
+
+ // Don't flush the changes yet. That will happen once the migration
+ // successfully completes.
+ return NS_OK;
+}
+
+/**
+ * This is responsible for deleting the old profile, copying its name to the
+ * current profile and if the old profile was default making the new profile
+ * default as well.
+ */
+nsresult nsToolkitProfileService::ApplyResetProfile(
+ nsIToolkitProfile* aOldProfile) {
+ // If the old profile would have been the default for old installs then mark
+ // the new profile as such.
+ if (mNormalDefault == aOldProfile) {
+ SetNormalDefault(mCurrent);
+ }
+
+ if (mUseDedicatedProfile && mDedicatedProfile == aOldProfile) {
+ bool wasLocked = false;
+ nsCString val;
+ if (NS_SUCCEEDED(
+ mProfileDB.GetString(mInstallSection.get(), "Locked", val))) {
+ wasLocked = val.Equals("1");
+ }
+
+ SetDefaultProfile(mCurrent);
+
+ // Make the locked state match if necessary.
+ if (!wasLocked) {
+ mProfileDB.DeleteString(mInstallSection.get(), "Locked");
+ }
+ }
+
+ nsCString name;
+ nsresult rv = aOldProfile->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't remove the old profile's files until after we've successfully flushed
+ // the profile changes to disk.
+ rv = aOldProfile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Switching the name will make this the default for dev-edition if
+ // appropriate.
+ rv = mCurrent->SetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Flush();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now that the profile changes are flushed, try to remove the old profile's
+ // files. If we fail the worst that will happen is that an orphan directory is
+ // left. Let this run in the background while we start up.
+ RemoveProfileFiles(aOldProfile, true);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetProfileByName(const nsACString& aName,
+ nsIToolkitProfile** aResult) {
+ for (RefPtr<nsToolkitProfile> profile : mProfiles) {
+ if (profile->mName.Equals(aName)) {
+ NS_ADDREF(*aResult = profile);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+/**
+ * Finds a profile from the database that uses the given root and local
+ * directories.
+ */
+void nsToolkitProfileService::GetProfileByDir(nsIFile* aRootDir,
+ nsIFile* aLocalDir,
+ nsIToolkitProfile** aResult) {
+ for (RefPtr<nsToolkitProfile> profile : mProfiles) {
+ bool equal;
+ nsresult rv = profile->mRootDir->Equals(aRootDir, &equal);
+ if (NS_SUCCEEDED(rv) && equal) {
+ if (!aLocalDir) {
+ // If no local directory was given then we will just use the normal
+ // local directory for the profile.
+ profile.forget(aResult);
+ return;
+ }
+
+ rv = profile->mLocalDir->Equals(aLocalDir, &equal);
+ if (NS_SUCCEEDED(rv) && equal) {
+ profile.forget(aResult);
+ return;
+ }
+ }
+ }
+}
+
+nsresult NS_LockProfilePath(nsIFile* aPath, nsIFile* aTempPath,
+ nsIProfileUnlocker** aUnlocker,
+ nsIProfileLock** aResult) {
+ RefPtr<nsToolkitProfileLock> lock = new nsToolkitProfileLock();
+
+ nsresult rv = lock->Init(aPath, aTempPath, aUnlocker);
+ if (NS_FAILED(rv)) return rv;
+
+ lock.forget(aResult);
+ return NS_OK;
+}
+
+static void SaltProfileName(nsACString& aName) {
+ char salt[9];
+ NS_MakeRandomString(salt, 8);
+ salt[8] = '.';
+
+ aName.Insert(salt, 0, 9);
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::CreateUniqueProfile(nsIFile* aRootDir,
+ const nsACString& aNamePrefix,
+ nsIToolkitProfile** aResult) {
+ nsCOMPtr<nsIToolkitProfile> profile;
+ nsresult rv = GetProfileByName(aNamePrefix, getter_AddRefs(profile));
+ if (NS_FAILED(rv)) {
+ return CreateProfile(aRootDir, aNamePrefix, aResult);
+ }
+
+ uint32_t suffix = 1;
+ while (true) {
+ nsPrintfCString name("%s-%d", PromiseFlatCString(aNamePrefix).get(),
+ suffix);
+ rv = GetProfileByName(name, getter_AddRefs(profile));
+ if (NS_FAILED(rv)) {
+ return CreateProfile(aRootDir, name, aResult);
+ }
+ suffix++;
+ }
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::CreateProfile(nsIFile* aRootDir,
+ const nsACString& aName,
+ nsIToolkitProfile** aResult) {
+ nsresult rv = GetProfileByName(aName, aResult);
+ if (NS_SUCCEEDED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIFile> rootDir(aRootDir);
+
+ nsAutoCString dirName;
+ if (!rootDir) {
+ rv = gDirServiceProvider->GetUserProfilesRootDir(getter_AddRefs(rootDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dirName = aName;
+ SaltProfileName(dirName);
+
+ if (NS_IsNativeUTF8()) {
+ rootDir->AppendNative(dirName);
+ } else {
+ rootDir->Append(NS_ConvertUTF8toUTF16(dirName));
+ }
+ }
+
+ nsCOMPtr<nsIFile> localDir;
+
+ bool isRelative;
+ rv = mAppData->Contains(rootDir, &isRelative);
+ if (NS_SUCCEEDED(rv) && isRelative) {
+ nsAutoCString path;
+ rv = rootDir->GetRelativeDescriptor(mAppData, path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(localDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = localDir->SetRelativeDescriptor(mTempData, path);
+ } else {
+ localDir = rootDir;
+ }
+
+ bool exists;
+ rv = rootDir->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (exists) {
+ rv = rootDir->IsDirectory(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists) return NS_ERROR_FILE_NOT_DIRECTORY;
+ } else {
+ nsCOMPtr<nsIFile> profileDirParent;
+ nsAutoString profileDirName;
+
+ rv = rootDir->GetParent(getter_AddRefs(profileDirParent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = rootDir->GetLeafName(profileDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // let's ensure that the profile directory exists.
+ rv = rootDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = rootDir->SetPermissions(0700);
+#ifndef ANDROID
+ // If the profile is on the sdcard, this will fail but its non-fatal
+ NS_ENSURE_SUCCESS(rv, rv);
+#endif
+ }
+
+ rv = localDir->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists) {
+ rv = localDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // We created a new profile dir. Let's store a creation timestamp.
+ // Note that this code path does not apply if the profile dir was
+ // created prior to launching.
+ rv = CreateTimesInternal(rootDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIToolkitProfile> profile =
+ new nsToolkitProfile(aName, rootDir, localDir, false);
+
+ if (aName.Equals(DEV_EDITION_NAME)) {
+ mDevEditionDefault = profile;
+ }
+
+ profile.forget(aResult);
+ return NS_OK;
+}
+
+/**
+ * Snaps (https://snapcraft.io/) use a different installation directory for
+ * every version of an application. Since dedicated profiles uses the
+ * installation directory to determine which profile to use this would lead
+ * snap users getting a new profile on every application update.
+ *
+ * However the only way to have multiple installation of a snap is to install
+ * a new snap instance. Different snap instances have different user data
+ * directories and so already will not share profiles, in fact one instance
+ * will not even be able to see the other instance's profiles since
+ * profiles.ini will be stored in different places.
+ *
+ * So we can just disable dedicated profile support in this case and revert
+ * back to the old method of just having a single default profile and still
+ * get essentially the same benefits as dedicated profiles provides.
+ */
+bool nsToolkitProfileService::IsSnapEnvironment() {
+#ifdef MOZ_WIDGET_GTK
+ return widget::IsRunningUnderSnap();
+#else
+ return false;
+#endif
+}
+
+/**
+ * In some situations dedicated profile support does not work well. This
+ * includes a handful of linux distributions which always install different
+ * application versions to different locations, some application sandboxing
+ * systems as well as enterprise deployments. This environment variable provides
+ * a way to opt out of dedicated profiles for these cases.
+ *
+ * For Windows, we provide a policy to accomplish the same thing.
+ */
+bool nsToolkitProfileService::UseLegacyProfiles() {
+ bool legacyProfiles = !!PR_GetEnv("MOZ_LEGACY_PROFILES");
+#ifdef XP_WIN
+ legacyProfiles |= PolicyCheckBoolean(L"LegacyProfiles");
+#endif
+ return legacyProfiles;
+}
+
+struct FindInstallsClosure {
+ nsINIParser* installData;
+ nsTArray<nsCString>* installs;
+};
+
+static bool FindInstalls(const char* aSection, void* aClosure) {
+ FindInstallsClosure* closure = static_cast<FindInstallsClosure*>(aClosure);
+
+ // Check if the section starts with "Install"
+ if (strncmp(aSection, INSTALL_PREFIX, INSTALL_PREFIX_LENGTH) != 0) {
+ return true;
+ }
+
+ nsCString install(aSection);
+ closure->installs->AppendElement(install);
+
+ return true;
+}
+
+nsTArray<nsCString> nsToolkitProfileService::GetKnownInstalls() {
+ nsTArray<nsCString> result;
+ FindInstallsClosure closure = {&mProfileDB, &result};
+
+ mProfileDB.GetSections(&FindInstalls, &closure);
+
+ return result;
+}
+
+nsresult nsToolkitProfileService::CreateTimesInternal(nsIFile* aProfileDir) {
+ nsresult rv = NS_ERROR_FAILURE;
+ nsCOMPtr<nsIFile> creationLog;
+ rv = aProfileDir->Clone(getter_AddRefs(creationLog));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = creationLog->AppendNative("times.json"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists = false;
+ creationLog->Exists(&exists);
+ if (exists) {
+ return NS_OK;
+ }
+
+ rv = creationLog->Create(nsIFile::NORMAL_FILE_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We don't care about microsecond resolution.
+ int64_t msec = PR_Now() / PR_USEC_PER_MSEC;
+
+ // Write it out.
+ PRFileDesc* writeFile;
+ rv = creationLog->OpenNSPRFileDesc(PR_WRONLY, 0700, &writeFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PR_fprintf(writeFile, "{\n\"created\": %lld,\n\"firstUse\": null\n}\n", msec);
+ PR_Close(writeFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::GetProfileCount(uint32_t* aResult) {
+ *aResult = 0;
+ for (nsToolkitProfile* profile : mProfiles) {
+ Unused << profile;
+ (*aResult)++;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsToolkitProfileService::Flush() {
+ if (GetIsListOutdated()) {
+ return NS_ERROR_DATABASE_CHANGED;
+ }
+
+ nsresult rv;
+
+ // If we aren't using dedicated profiles then nothing about the list of
+ // installs can have changed, so no need to update the backup.
+ if (mUseDedicatedProfile) {
+ // Export the installs to the backup.
+ nsTArray<nsCString> installs = GetKnownInstalls();
+
+ if (!installs.IsEmpty()) {
+ nsCString data;
+ nsCString buffer;
+
+ for (uint32_t i = 0; i < installs.Length(); i++) {
+ nsTArray<UniquePtr<KeyValue>> strings =
+ GetSectionStrings(&mProfileDB, installs[i].get());
+ if (strings.IsEmpty()) {
+ continue;
+ }
+
+ // Strip "Install" from the start.
+ const nsDependentCSubstring& install =
+ Substring(installs[i], INSTALL_PREFIX_LENGTH);
+ data.AppendPrintf("[%s]\n", PromiseFlatCString(install).get());
+
+ for (uint32_t j = 0; j < strings.Length(); j++) {
+ data.AppendPrintf("%s=%s\n", strings[j]->key.get(),
+ strings[j]->value.get());
+ }
+
+ data.Append("\n");
+ }
+
+ FILE* writeFile;
+ rv = mInstallDBFile->OpenANSIFileDesc("w", &writeFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t length = data.Length();
+ if (fwrite(data.get(), sizeof(char), length, writeFile) != length) {
+ fclose(writeFile);
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ fclose(writeFile);
+ } else {
+ rv = mInstallDBFile->Remove(false);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
+ return rv;
+ }
+ }
+ }
+
+ rv = mProfileDB.WriteToFile(mProfileDBFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists,
+ &mProfileDBModifiedTime, &mProfileDBFileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+already_AddRefed<nsToolkitProfileService> NS_GetToolkitProfileService() {
+ if (!nsToolkitProfileService::gService) {
+ nsToolkitProfileService::gService = new nsToolkitProfileService();
+ nsresult rv = nsToolkitProfileService::gService->Init();
+ if (NS_FAILED(rv)) {
+ NS_ERROR("nsToolkitProfileService::Init failed!");
+ delete nsToolkitProfileService::gService;
+ return nullptr;
+ }
+ }
+
+ return do_AddRef(nsToolkitProfileService::gService);
+}
+
+nsresult XRE_GetFileFromPath(const char* aPath, nsIFile** aResult) {
+#if defined(XP_MACOSX)
+ int32_t pathLen = strlen(aPath);
+ if (pathLen > MAXPATHLEN) return NS_ERROR_INVALID_ARG;
+
+ CFURLRef fullPath = CFURLCreateFromFileSystemRepresentation(
+ nullptr, (const UInt8*)aPath, pathLen, true);
+ if (!fullPath) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIFile> lf;
+ nsresult rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(lf));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsILocalFileMac> lfMac = do_QueryInterface(lf, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = lfMac->InitWithCFURL(fullPath);
+ if (NS_SUCCEEDED(rv)) {
+ lf.forget(aResult);
+ }
+ }
+ }
+ CFRelease(fullPath);
+ return rv;
+
+#elif defined(XP_UNIX)
+ char fullPath[MAXPATHLEN];
+
+ if (!realpath(aPath, fullPath)) return NS_ERROR_FAILURE;
+
+ return NS_NewNativeLocalFile(nsDependentCString(fullPath), true, aResult);
+#elif defined(XP_WIN)
+ WCHAR fullPath[MAXPATHLEN];
+
+ if (!_wfullpath(fullPath, NS_ConvertUTF8toUTF16(aPath).get(), MAXPATHLEN))
+ return NS_ERROR_FAILURE;
+
+ return NS_NewLocalFile(nsDependentString(fullPath), true, aResult);
+
+#else
+# error Platform-specific logic needed here.
+#endif
+}
diff --git a/toolkit/profile/nsToolkitProfileService.h b/toolkit/profile/nsToolkitProfileService.h
new file mode 100644
index 0000000000..9de18b8ae3
--- /dev/null
+++ b/toolkit/profile/nsToolkitProfileService.h
@@ -0,0 +1,180 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef nsToolkitProfileService_h
+#define nsToolkitProfileService_h
+
+#include "mozilla/Components.h"
+#include "mozilla/LinkedList.h"
+#include "nsIToolkitProfileService.h"
+#include "nsIToolkitProfile.h"
+#include "nsIFactory.h"
+#include "nsSimpleEnumerator.h"
+#include "nsProfileLock.h"
+#include "nsINIParser.h"
+
+class nsToolkitProfile final
+ : public nsIToolkitProfile,
+ public mozilla::LinkedListElement<RefPtr<nsToolkitProfile>> {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITOOLKITPROFILE
+
+ friend class nsToolkitProfileService;
+
+ private:
+ ~nsToolkitProfile() = default;
+
+ nsToolkitProfile(const nsACString& aName, nsIFile* aRootDir,
+ nsIFile* aLocalDir, bool aFromDB);
+
+ nsresult RemoveInternal(bool aRemoveFiles, bool aInBackground);
+
+ friend class nsToolkitProfileLock;
+
+ nsCString mName;
+ nsCOMPtr<nsIFile> mRootDir;
+ nsCOMPtr<nsIFile> mLocalDir;
+ nsIProfileLock* mLock;
+ uint32_t mIndex;
+ nsCString mSection;
+};
+
+class nsToolkitProfileLock final : public nsIProfileLock {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROFILELOCK
+
+ nsresult Init(nsToolkitProfile* aProfile, nsIProfileUnlocker** aUnlocker);
+ nsresult Init(nsIFile* aDirectory, nsIFile* aLocalDirectory,
+ nsIProfileUnlocker** aUnlocker);
+
+ nsToolkitProfileLock() = default;
+
+ private:
+ ~nsToolkitProfileLock();
+
+ RefPtr<nsToolkitProfile> mProfile;
+ nsCOMPtr<nsIFile> mDirectory;
+ nsCOMPtr<nsIFile> mLocalDirectory;
+
+ nsProfileLock mLock;
+};
+
+class nsToolkitProfileService final : public nsIToolkitProfileService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITOOLKITPROFILESERVICE
+
+ nsresult SelectStartupProfile(int* aArgc, char* aArgv[], bool aIsResetting,
+ nsIFile** aRootDir, nsIFile** aLocalDir,
+ nsIToolkitProfile** aProfile, bool* aDidCreate,
+ bool* aWasDefaultSelection);
+ nsresult CreateResetProfile(nsIToolkitProfile** aNewProfile);
+ nsresult ApplyResetProfile(nsIToolkitProfile* aOldProfile);
+ void CompleteStartup();
+
+ private:
+ friend class nsToolkitProfile;
+ friend already_AddRefed<nsToolkitProfileService>
+ NS_GetToolkitProfileService();
+
+ nsToolkitProfileService();
+ ~nsToolkitProfileService();
+
+ nsresult Init();
+
+ nsresult CreateTimesInternal(nsIFile* profileDir);
+ void GetProfileByDir(nsIFile* aRootDir, nsIFile* aLocalDir,
+ nsIToolkitProfile** aResult);
+
+ nsresult GetProfileDescriptor(nsIToolkitProfile* aProfile,
+ nsACString& aDescriptor, bool* aIsRelative);
+ bool IsProfileForCurrentInstall(nsIToolkitProfile* aProfile);
+ void ClearProfileFromOtherInstalls(nsIToolkitProfile* aProfile);
+ nsresult MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile,
+ bool* aResult);
+ bool IsSnapEnvironment();
+ bool UseLegacyProfiles();
+ nsresult CreateDefaultProfile(nsIToolkitProfile** aResult);
+ void SetNormalDefault(nsIToolkitProfile* aProfile);
+
+ // Returns the known install hashes from the installs database. Modifying the
+ // installs database is safe while iterating the returned array.
+ nsTArray<nsCString> GetKnownInstalls();
+
+ // Tracks whether SelectStartupProfile has been called.
+ bool mStartupProfileSelected;
+ // The profiles loaded from profiles.ini.
+ mozilla::LinkedList<RefPtr<nsToolkitProfile>> mProfiles;
+ // The profile selected for use at startup, if it exists in profiles.ini.
+ nsCOMPtr<nsIToolkitProfile> mCurrent;
+ // The profile selected for this install in installs.ini.
+ nsCOMPtr<nsIToolkitProfile> mDedicatedProfile;
+ // The default profile used by non-dev-edition builds.
+ nsCOMPtr<nsIToolkitProfile> mNormalDefault;
+ // The profile used if mUseDevEditionProfile is true (the default on
+ // dev-edition builds).
+ nsCOMPtr<nsIToolkitProfile> mDevEditionDefault;
+ // The directory that holds profiles.ini and profile directories.
+ nsCOMPtr<nsIFile> mAppData;
+ // The directory that holds the cache files for profiles.
+ nsCOMPtr<nsIFile> mTempData;
+ // The location of profiles.ini.
+ nsCOMPtr<nsIFile> mProfileDBFile;
+ // The location of installs.ini.
+ nsCOMPtr<nsIFile> mInstallDBFile;
+ // The data loaded from profiles.ini.
+ nsINIParser mProfileDB;
+ // The section in the profiles db for the current install.
+ nsCString mInstallSection;
+ // A legacy install section which may have been generated against an
+ // installation directory with an incorrect case (see bug 1555319). It is only
+ // really held here so that it can be overridden by tests.
+ nsCString mLegacyInstallSection;
+ // Whether to start with the selected profile by default.
+ bool mStartWithLast;
+ // True if during startup it appeared that this is the first run.
+ bool mIsFirstRun;
+ // True if the default profile is the separate dev-edition-profile.
+ bool mUseDevEditionProfile;
+ // True if this install should use a dedicated default profile.
+ const bool mUseDedicatedProfile;
+ nsString mStartupReason;
+ // Records the version of the profiles.ini file as it was when it was loaded
+ // during startup.
+ nsCString mStartupFileVersion;
+ bool mMaybeLockProfile;
+ // Holds the current application update channel. This is only really held
+ // so it can be overriden in tests.
+ nsCString mUpdateChannel;
+ // Keep track of some attributes of the databases so we can tell if another
+ // process has changed them.
+ bool mProfileDBExists;
+ int64_t mProfileDBFileSize;
+ PRTime mProfileDBModifiedTime;
+
+ static nsToolkitProfileService* gService;
+
+ class ProfileEnumerator final : public nsSimpleEnumerator {
+ public:
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ const nsID& DefaultInterface() override {
+ return NS_GET_IID(nsIToolkitProfile);
+ }
+
+ explicit ProfileEnumerator(nsToolkitProfile* first) { mCurrent = first; }
+
+ private:
+ RefPtr<nsToolkitProfile> mCurrent;
+ };
+};
+
+already_AddRefed<nsToolkitProfileService> NS_GetToolkitProfileService();
+
+#endif
diff --git a/toolkit/profile/test/chrome.toml b/toolkit/profile/test/chrome.toml
new file mode 100644
index 0000000000..c59a75ea67
--- /dev/null
+++ b/toolkit/profile/test/chrome.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_create_profile.xhtml"]
diff --git a/toolkit/profile/test/test_create_profile.xhtml b/toolkit/profile/test/test_create_profile.xhtml
new file mode 100644
index 0000000000..5967580382
--- /dev/null
+++ b/toolkit/profile/test/test_create_profile.xhtml
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=543854
+-->
+<window title="Mozilla Bug 543854"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=543854"
+ target="_blank">Mozilla Bug 543854</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ /** Test for Bug 543854 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ const ASCIIName = "myprofile";
+ const UnicodeName = "\u09A0\u09BE\u0995\u09C1\u09B0"; // A Bengali name
+
+ var gDirService = SpecialPowers.Services.dirsvc;
+ var gIOService = SpecialPowers.Services.io;
+ var gProfileService;
+ var gDefaultLocalProfileParent;
+
+ gProfileService = Cc["@mozilla.org/toolkit/profile-service;1"].
+ getService(Ci.nsIToolkitProfileService);
+
+ gDefaultLocalProfileParent = gDirService.get("DefProfLRt", Ci.nsIFile);
+
+ createProfile(ASCIIName);
+ createProfile(UnicodeName);
+ SimpleTest.finish();
+
+/**
+ * Read the contents of an nsIFile. Throws on error.
+
+ * @param file an nsIFile instance.
+ * @return string contents.
+ */
+function readFile(file) {
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ let sstream = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+
+ const RO = 0x01;
+ const READ_OTHERS = 4;
+
+ fstream.init(file, RO, READ_OTHERS, 0);
+ sstream.init(fstream);
+ let out = sstream.read(sstream.available());
+ sstream.close();
+ fstream.close();
+ return out;
+}
+
+function checkBounds(lowerBound, value, upperBound) {
+ ok(lowerBound <= value, "value " + value +
+ " is above lower bound " + lowerBound);
+ ok(upperBound >= value, "value " + value +
+ " is within upper bound " + upperBound);
+}
+
+function createProfile(profileName) {
+ // Filesystem precision is lower than Date precision.
+ let lowerBound = Date.now() - 1000;
+
+ let profile = gProfileService.createProfile(null, profileName);
+
+ // check that the directory was created
+ isnot(profile, null, "Profile " + profileName + " created");
+
+ let profileDir = profile.rootDir;
+
+ ok(profileDir.exists(), "Profile dir created");
+ ok(profileDir.isDirectory(), "Profile dir is a directory");
+
+ let profileDirPath = profileDir.path;
+
+ is(profileDirPath.substr(profileDirPath.length - profileName.length),
+ profileName, "Profile dir has expected name");
+
+ // Ensure that our timestamp file was created.
+ let jsonFile = profileDir.clone();
+ jsonFile.append("times.json");
+ ok(jsonFile.path, "Path is " + jsonFile.path);
+ ok(jsonFile.exists(), "Times file was created");
+ ok(jsonFile.isFile(), "Times file is a file");
+ let json = JSON.parse(readFile(jsonFile));
+
+ let upperBound = Date.now() + 1000;
+
+ let created = json.created;
+ ok(created, "created is set");
+
+ // Check against real clock time.
+ checkBounds(lowerBound, created, upperBound);
+
+ // Clean up the profile before local profile test.
+ profile.remove(true);
+ ok(!jsonFile.exists(), "Times file was removed");
+ ok(!profileDir.exists(), "Profile dir was removed");
+
+ // Create with non-null aRootDir
+ profile = gProfileService.createProfile(profileDir, profileName);
+
+ let localProfileDir = profile.localDir;
+ ok(gDefaultLocalProfileParent.contains(localProfileDir, false),
+ "Local profile dir created in DefProfLRt");
+
+ // Clean up the profile.
+ profile.remove(true);
+ ok(!profileDir.exists(), "Profile dir was removed");
+}
+
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/profile/xpcshell/head.js b/toolkit/profile/xpcshell/head.js
new file mode 100644
index 0000000000..dc354f5b62
--- /dev/null
+++ b/toolkit/profile/xpcshell/head.js
@@ -0,0 +1,623 @@
+/* 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);
+
+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",
+ default: false,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ 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 (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"),
+ };
+
+ 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."
+ );
+
+ 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."
+ );
+}
diff --git a/toolkit/profile/xpcshell/test_check_backup.js b/toolkit/profile/xpcshell/test_check_backup.js
new file mode 100644
index 0000000000..2f5a9dbf18
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_check_backup.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that when the profiles DB is missing the install data we reload it.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "Path1",
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ },
+ ],
+ };
+
+ let installs = {
+ [hash]: {
+ default: "Path2",
+ },
+ };
+
+ writeProfilesIni(profileData);
+ writeInstallsIni({ installs });
+
+ let { profile, didCreate } = selectStartupProfile();
+ checkStartupReason("default");
+ let { databaseVersion, profileCount } = getTelemetryScalars();
+ Assert.equal(
+ databaseVersion,
+ "1",
+ "Old database file was present at startup."
+ );
+ Assert.equal(profileCount, 2, "Should be two profiles.");
+
+ // Should have added the backup data to the service, check that is true.
+ profileData.installs = installs;
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.equal(
+ profile.name,
+ "Profile2",
+ "Should have selected the right profile"
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_claim_locked.js b/toolkit/profile/xpcshell/test_claim_locked.js
new file mode 100644
index 0000000000..bffdea39dc
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_claim_locked.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile already locked to a different install
+ * isn't claimed by this install.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "Foo",
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ installs: {
+ other: {
+ default: defaultProfile.leafName,
+ locked: true,
+ },
+ },
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 2,
+ "Should have the right number of profiles."
+ );
+
+ let hash = xreDirProvider.getInstallHash();
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 2,
+ "Should be two known installs."
+ );
+ Assert.notEqual(
+ profileData.installs[hash].default,
+ defaultProfile.leafName,
+ "Should not have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we created this profile for this install."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.ok(
+ !selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using a different directory."
+ );
+ Assert.equal(selectedProfile.name, DEDICATED_NAME);
+});
diff --git a/toolkit/profile/xpcshell/test_clean.js b/toolkit/profile/xpcshell/test_clean.js
new file mode 100644
index 0000000000..3132d5457d
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_clean.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests from a clean state.
+ * Then does some testing that creating new profiles and marking them as
+ * selected works.
+ */
+
+add_task(async () => {
+ let service = getProfileService();
+
+ let target = gDataHome.clone();
+ target.append("profiles.ini");
+ Assert.ok(!target.exists(), "profiles.ini should not exist yet.");
+ target.leafName = "installs.ini";
+ Assert.ok(!target.exists(), "installs.ini should not exist yet.");
+
+ // Create a new profile to use.
+ let newProfile = service.createProfile(null, "dedicated");
+ service.flush();
+
+ let profileData = readProfilesIni();
+
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, "dedicated", "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ // The new profile hasn't been marked as the default yet!
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 0,
+ "Should be no defaults for installs yet."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(
+ service.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ service.startWithLastProfile = false;
+ Assert.ok(
+ !service.startWithLastProfile,
+ "Should be set to not start with the last profile."
+ );
+
+ service.defaultProfile = newProfile;
+ service.flush();
+
+ profileData = readProfilesIni();
+
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ profile = profileData.profiles[0];
+ Assert.equal(profile.name, "dedicated", "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ let hash = xreDirProvider.getInstallHash();
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ profileData.profiles[0].path,
+ "Should have marked the new profile as the default for this install."
+ );
+
+ checkProfileService(profileData);
+
+ let otherProfile = service.createProfile(null, "another");
+ service.defaultProfile = otherProfile;
+
+ service.flush();
+
+ profileData = readProfilesIni();
+
+ Assert.equal(
+ profileData.profiles.length,
+ 2,
+ "Should have the right number of profiles."
+ );
+
+ profile = profileData.profiles[0];
+ Assert.equal(profile.name, "another", "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, "dedicated", "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ profileData.profiles[0].path,
+ "Should have marked the new profile as the default for this install."
+ );
+
+ checkProfileService(profileData);
+
+ newProfile.remove(true);
+ service.flush();
+
+ profileData = readProfilesIni();
+
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ profile = profileData.profiles[0];
+ Assert.equal(profile.name, "another", "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ profileData.profiles[0].path,
+ "Should have marked the new profile as the default for this install."
+ );
+
+ checkProfileService(profileData);
+
+ otherProfile.remove(true);
+ service.flush();
+
+ profileData = readProfilesIni();
+
+ Assert.equal(
+ profileData.profiles.length,
+ 0,
+ "Should have the right number of profiles."
+ );
+
+ // We leave a reference to the missing profile to stop us trying to steal the
+ // old-style default profile on next startup.
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+
+ checkProfileService(profileData);
+});
diff --git a/toolkit/profile/xpcshell/test_conflict_installs.js b/toolkit/profile/xpcshell/test_conflict_installs.js
new file mode 100644
index 0000000000..741e7ed70f
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_conflict_installs.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the profile service refuses to flush when the install.ini file
+ * has been modified.
+ */
+
+function check_unchanged(service) {
+ Assert.ok(
+ !service.isListOutdated,
+ "Should not have detected a modification."
+ );
+ try {
+ service.flush();
+ Assert.ok(true, "Should have flushed.");
+ } catch (e) {
+ Assert.ok(false, "Should have succeeded flushing.");
+ }
+}
+
+add_task(async () => {
+ let service = getProfileService();
+
+ Assert.ok(!service.isListOutdated, "Should not be modified yet.");
+
+ let installsini = gDataHome.clone();
+ installsini.append("installs.ini");
+
+ Assert.ok(!installsini.exists(), "File should not exist yet.");
+ installsini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ installsini.remove(false);
+ // We have to do profile selection to actually have any install data.
+ selectStartupProfile();
+ check_unchanged(service);
+
+ // We can't reset the modification time back to exactly what it was, so I
+ // guess we can't do much more here :(
+});
diff --git a/toolkit/profile/xpcshell/test_conflict_profiles.js b/toolkit/profile/xpcshell/test_conflict_profiles.js
new file mode 100644
index 0000000000..7028c1d5c2
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_conflict_profiles.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the profile service refuses to flush when the profiles.ini file
+ * has been modified.
+ */
+
+function check_unchanged(service) {
+ Assert.ok(
+ !service.isListOutdated,
+ "Should not have detected a modification."
+ );
+ try {
+ service.flush();
+ Assert.ok(true, "Should have flushed.");
+ } catch (e) {
+ Assert.ok(false, "Should have succeeded flushing.");
+ }
+}
+
+function check_outdated(service) {
+ Assert.ok(service.isListOutdated, "Should have detected a modification.");
+ try {
+ service.flush();
+ Assert.ok(false, "Should have failed to flush.");
+ } catch (e) {
+ Assert.equal(
+ e.result,
+ Cr.NS_ERROR_DATABASE_CHANGED,
+ "Should have refused to flush."
+ );
+ }
+}
+
+add_task(async () => {
+ let service = getProfileService();
+
+ Assert.ok(!service.isListOutdated, "Should not be modified yet.");
+
+ let profilesini = gDataHome.clone();
+ profilesini.append("profiles.ini");
+
+ Assert.ok(!profilesini.exists(), "File should not exist yet.");
+ profilesini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ check_outdated(service);
+
+ profilesini.remove(false);
+ check_unchanged(service);
+
+ let oldTime = profilesini.lastModifiedTime;
+ profilesini.lastModifiedTime = oldTime - 10000;
+ check_outdated(service);
+
+ // We can't reset the modification time back to exactly what it was, so I
+ // guess we can't do much more here :(
+});
diff --git a/toolkit/profile/xpcshell/test_create_default.js b/toolkit/profile/xpcshell/test_create_default.js
new file mode 100644
index 0000000000..cdac1832bf
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_create_default.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that from an empty database a default profile is created.
+ */
+
+add_task(async () => {
+ let service = getProfileService();
+ let { profile, didCreate } = selectStartupProfile();
+
+ checkStartupReason("firstrun-created-default");
+
+ let profileData = readProfilesIni();
+ checkProfileService(profileData);
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.equal(
+ profile,
+ service.defaultProfile,
+ "Should now be the default profile."
+ );
+ Assert.equal(
+ profile.name,
+ DEDICATED_NAME,
+ "Should have created a new profile with the right name."
+ );
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 2,
+ "Should have the right number of profiles."
+ );
+
+ profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ let hash = xreDirProvider.getInstallHash();
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked the profile"
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_fix_directory_case.js b/toolkit/profile/xpcshell/test_fix_directory_case.js
new file mode 100644
index 0000000000..60e3acbe4e
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_fix_directory_case.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the case where the user has an default profile set for the legacy
+ * install hash. This should be switched to the new hash and correctly used as
+ * the default.
+ */
+
+add_task(async () => {
+ let currentHash = xreDirProvider.getInstallHash();
+ let legacyHash = "F87E39E944FE466E";
+
+ let defaultProfile = makeRandomProfileDir("default");
+ let dedicatedProfile = makeRandomProfileDir("dedicated");
+ let devProfile = makeRandomProfileDir("devedition");
+
+ // Make sure we don't steal the old-style default.
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ {
+ name: "dedicated",
+ path: dedicatedProfile.leafName,
+ },
+ {
+ name: "dev-edition-default",
+ path: devProfile.leafName,
+ },
+ ],
+ installs: {
+ [legacyHash]: {
+ default: dedicatedProfile.leafName,
+ },
+ otherhash: {
+ default: "foobar",
+ },
+ },
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile(
+ [],
+ false,
+ legacyHash
+ );
+ checkStartupReason("default");
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 3,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, `dedicated`, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ dedicatedProfile.leafName,
+ "Should be the expected dedicated profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 3,
+ "Should be three known installs."
+ );
+ Assert.equal(
+ profileData.installs[currentHash].default,
+ dedicatedProfile.leafName,
+ "Should have switched to the new install hash."
+ );
+ Assert.equal(
+ profileData.installs[legacyHash].default,
+ dedicatedProfile.leafName,
+ "Should have kept the old install hash."
+ );
+ Assert.equal(
+ profileData.installs.otherhash.default,
+ "foobar",
+ "Should have kept the default for the other install."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(dedicatedProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, "dedicated");
+});
diff --git a/toolkit/profile/xpcshell/test_ignore_legacy_directory.js b/toolkit/profile/xpcshell/test_ignore_legacy_directory.js
new file mode 100644
index 0000000000..29b1601cc5
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_ignore_legacy_directory.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the case where the user has an default profile set for both the legacy
+ * and new install hash. This should just use the default for the new install
+ * hash.
+ */
+
+add_task(async () => {
+ let currentHash = xreDirProvider.getInstallHash();
+ let legacyHash = "F87E39E944FE466E";
+
+ let defaultProfile = makeRandomProfileDir("default");
+ let dedicatedProfile = makeRandomProfileDir("dedicated");
+ let devProfile = makeRandomProfileDir("devedition");
+
+ // Make sure we don't steal the old-style default.
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ {
+ name: "dedicated",
+ path: dedicatedProfile.leafName,
+ },
+ {
+ name: "dev-edition-default",
+ path: devProfile.leafName,
+ },
+ ],
+ installs: {
+ [legacyHash]: {
+ default: defaultProfile.leafName,
+ },
+ [currentHash]: {
+ default: dedicatedProfile.leafName,
+ },
+ otherhash: {
+ default: "foobar",
+ },
+ },
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile(
+ [],
+ false,
+ legacyHash
+ );
+ checkStartupReason("default");
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 3,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, `dedicated`, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ dedicatedProfile.leafName,
+ "Should be the expected dedicated profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ profile = profileData.profiles[2];
+ Assert.equal(
+ profile.name,
+ "dev-edition-default",
+ "Should have the right name."
+ );
+ Assert.equal(
+ profile.path,
+ devProfile.leafName,
+ "Should not be the original default profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 3,
+ "Should be three known installs."
+ );
+ Assert.equal(
+ profileData.installs[currentHash].default,
+ dedicatedProfile.leafName,
+ "Should have switched to the new install hash."
+ );
+ Assert.equal(
+ profileData.installs[legacyHash].default,
+ defaultProfile.leafName,
+ "Should have ignored old install hash."
+ );
+ Assert.equal(
+ profileData.installs.otherhash.default,
+ "foobar",
+ "Should have kept the default for the other install."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(dedicatedProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, "dedicated");
+});
diff --git a/toolkit/profile/xpcshell/test_invalid_descriptor.js b/toolkit/profile/xpcshell/test_invalid_descriptor.js
new file mode 100644
index 0000000000..87638d301d
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_invalid_descriptor.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * If a user has modified a relative profile path then there may be issues where
+ * the profile default setting doesn't match.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "../data/test",
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ },
+ ],
+ installs: {
+ [hash]: {
+ default: "test",
+ },
+ },
+ };
+
+ writeProfilesIni(profileData);
+
+ let { profile, didCreate } = selectStartupProfile();
+ checkStartupReason("default");
+
+ let service = getProfileService();
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.equal(
+ profile.name,
+ "Profile1",
+ "Should have selected the expected profile"
+ );
+
+ Assert.equal(
+ profile.name,
+ service.defaultProfile.name,
+ "Should have selected the right default."
+ );
+
+ service.flush();
+ checkProfileService();
+});
diff --git a/toolkit/profile/xpcshell/test_legacy_empty.js b/toolkit/profile/xpcshell/test_legacy_empty.js
new file mode 100644
index 0000000000..7348006f8b
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_legacy_empty.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that setting MOZ_LEGACY_PROFILES disables dedicated profiles.
+ */
+
+add_task(async () => {
+ enableLegacyProfiles();
+
+ let service = getProfileService();
+ let { profile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-created-default");
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.equal(
+ profile.name,
+ PROFILE_DEFAULT,
+ "Should have used the normal name."
+ );
+ if (AppConstants.MOZ_DEV_EDITION) {
+ Assert.equal(service.profileCount, 2, "Should be two profiles.");
+ } else {
+ Assert.equal(service.profileCount, 1, "Should be only one profile.");
+ }
+
+ checkProfileService();
+});
diff --git a/toolkit/profile/xpcshell/test_legacy_select.js b/toolkit/profile/xpcshell/test_legacy_select.js
new file mode 100644
index 0000000000..e919169d59
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_legacy_select.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile not previously used by this build
+ * gets selected when configured for legacy profiles.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ // Just pretend this profile was last used by something in the profile dir.
+ let greDir = gProfD.clone();
+ greDir.append("app");
+ writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ enableLegacyProfiles();
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("default");
+
+ let profileData = readProfilesIni();
+ let installsINI = gDataHome.clone();
+ installsINI.append("installs.ini");
+ Assert.ok(
+ !installsINI.exists(),
+ "Installs database should not have been created."
+ );
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
diff --git a/toolkit/profile/xpcshell/test_lock.js b/toolkit/profile/xpcshell/test_lock.js
new file mode 100644
index 0000000000..7283406e00
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_lock.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that when the default application claims the old-style default profile
+ * it locks it to itself.
+ */
+
+add_task(async () => {
+ gIsDefaultApp = true;
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+
+ let hash = xreDirProvider.getInstallHash();
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ defaultProfile.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we're the default app."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
diff --git a/toolkit/profile/xpcshell/test_missing_profilesini.js b/toolkit/profile/xpcshell/test_missing_profilesini.js
new file mode 100644
index 0000000000..74ab7584fa
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_missing_profilesini.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * When profiles.ini is missing there isn't any point in restoring from any
+ * installs.ini, the profiles it refers to are gone anyway.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+
+ let installs = {
+ [hash]: {
+ default: "Path2",
+ },
+ otherhash: {
+ default: "foo",
+ },
+ anotherhash: {
+ default: "bar",
+ },
+ };
+
+ writeInstallsIni({ installs });
+
+ let { profile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-created-default");
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.equal(
+ profile.name,
+ DEDICATED_NAME,
+ "Should have created the right profile"
+ );
+
+ let profilesData = readProfilesIni();
+ Assert.equal(
+ Object.keys(profilesData.installs).length,
+ 1,
+ "Should be only one known install"
+ );
+ Assert.ok(hash in profilesData.installs, "Should be the expected install.");
+ Assert.notEqual(
+ profilesData.installs[hash].default,
+ "Path2",
+ "Didn't import the previous data."
+ );
+ Assert.equal(
+ profilesData.profiles.length,
+ 2,
+ "Should be two profiles (old-style default and dedicated default)."
+ );
+
+ let { databaseVersion, profileCount } = getTelemetryScalars();
+ Assert.equal(
+ databaseVersion,
+ "0",
+ "Database file was not present at startup."
+ );
+ Assert.equal(
+ profileCount,
+ 2,
+ "Should be two profiles (old-style default and dedicated default)."
+ );
+
+ checkProfileService(profilesData);
+});
diff --git a/toolkit/profile/xpcshell/test_new_default.js b/toolkit/profile/xpcshell/test_new_default.js
new file mode 100644
index 0000000000..552433ab95
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_new_default.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile previously used by this build gets
+ * updated to a dedicated profile for this build.
+ */
+
+add_task(async () => {
+ let mydefaultProfile = makeRandomProfileDir("mydefault");
+ let defaultProfile = makeRandomProfileDir("default");
+ let devDefaultProfile = makeRandomProfileDir("devedition");
+
+ writeCompatibilityIni(mydefaultProfile);
+ writeCompatibilityIni(devDefaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "mydefault",
+ path: mydefaultProfile.leafName,
+ default: true,
+ },
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ },
+ {
+ name: "dev-edition-default",
+ path: devDefaultProfile.leafName,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-claimed-default");
+ let { databaseVersion, profileCount } = getTelemetryScalars();
+ Assert.equal(
+ databaseVersion,
+ "1",
+ "Old database file was present at startup."
+ );
+ Assert.equal(profileCount, 3, "Should be three profiles.");
+
+ let hash = xreDirProvider.getInstallHash();
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 3,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original non-default profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(
+ profile.name,
+ "dev-edition-default",
+ "Should have the right name."
+ );
+ Assert.equal(
+ profile.path,
+ devDefaultProfile.leafName,
+ "Should be the original dev default profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ profile = profileData.profiles[2];
+ Assert.equal(profile.name, "mydefault", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ mydefaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ if (AppConstants.MOZ_DEV_EDITION) {
+ Assert.equal(
+ profileData.installs[hash].default,
+ devDefaultProfile.leafName,
+ "Should have marked the original dev default profile as the default for this install."
+ );
+ } else {
+ Assert.equal(
+ profileData.installs[hash].default,
+ mydefaultProfile.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ }
+
+ Assert.ok(
+ !profileData.installs[hash].locked,
+ "Should not be locked as we're not the default app."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ if (AppConstants.MOZ_DEV_EDITION) {
+ Assert.ok(
+ selectedProfile.rootDir.equals(devDefaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, "dev-edition-default");
+ } else {
+ Assert.ok(
+ selectedProfile.rootDir.equals(mydefaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, "mydefault");
+ }
+});
diff --git a/toolkit/profile/xpcshell/test_previous_dedicated.js b/toolkit/profile/xpcshell/test_previous_dedicated.js
new file mode 100644
index 0000000000..6dd80b4028
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_previous_dedicated.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * If install.ini lists a default profile for this build but that profile no
+ * longer exists don't try to steal the old-style default even if it was used
+ * by this build. It means this install has previously used dedicated profiles.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ installs: {
+ [hash]: {
+ default: "foobar",
+ },
+ },
+ });
+
+ testStartsProfileManager();
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ // We keep the data here so we don't steal on the next reboot...
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Still list the broken reference."
+ );
+
+ checkProfileService(profileData);
+});
diff --git a/toolkit/profile/xpcshell/test_profile_reset.js b/toolkit/profile/xpcshell/test_profile_reset.js
new file mode 100644
index 0000000000..7cfb5ed3d1
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_profile_reset.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that from an empty database with profile reset requested a new profile
+ * is still created.
+ */
+
+add_task(async () => {
+ let { profile: selectedProfile, didCreate } = selectStartupProfile([], true);
+ // With no profile we're just create a new profile and skip resetting it.
+ checkStartupReason("firstrun-created-default");
+ checkProfileService();
+
+ let hash = xreDirProvider.getInstallHash();
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 2,
+ "Should have the right number of profiles, ours and the old-style default."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name.");
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should only be one known installs."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ profile.path,
+ "Should have taken the new profile as the default for the current install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we created this profile."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.equal(
+ selectedProfile.name,
+ profile.name,
+ "Should be using the right profile."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_remove.js b/toolkit/profile/xpcshell/test_remove.js
new file mode 100644
index 0000000000..8b5025d612
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_remove.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests adding and removing functions correctly.
+ */
+
+function compareLists(service, knownProfiles) {
+ Assert.equal(
+ service.profileCount,
+ knownProfiles.length,
+ "profileCount should be correct."
+ );
+ let serviceProfiles = Array.from(service.profiles);
+ Assert.equal(
+ serviceProfiles.length,
+ knownProfiles.length,
+ "Enumerator length should be correct."
+ );
+
+ for (let i = 0; i < knownProfiles.length; i++) {
+ // Cannot use strictEqual here, it attempts to print out a string
+ // representation of the profile objects and on some platforms that recurses
+ // infinitely.
+ Assert.ok(
+ serviceProfiles[i] === knownProfiles[i],
+ `Should have the right profile in position ${i}.`
+ );
+ }
+}
+
+function removeProfile(profiles, position) {
+ dump(`Removing profile in position ${position}.`);
+ Assert.greaterOrEqual(position, 0, "Should be removing a valid position.");
+ Assert.less(
+ position,
+ profiles.length,
+ "Should be removing a valid position."
+ );
+
+ let last = profiles.pop();
+
+ if (profiles.length == position) {
+ // We were asked to remove the last profile.
+ last.remove(false);
+ return;
+ }
+
+ profiles[position].remove(false);
+ profiles[position] = last;
+}
+
+add_task(async () => {
+ let service = getProfileService();
+ let profiles = [];
+ compareLists(service, profiles);
+
+ profiles.push(service.createProfile(null, "profile1"));
+ profiles.push(service.createProfile(null, "profile2"));
+ profiles.push(service.createProfile(null, "profile3"));
+ profiles.push(service.createProfile(null, "profile4"));
+ profiles.push(service.createProfile(null, "profile5"));
+ profiles.push(service.createProfile(null, "profile6"));
+ profiles.push(service.createProfile(null, "profile7"));
+ profiles.push(service.createProfile(null, "profile8"));
+ profiles.push(service.createProfile(null, "profile9"));
+ compareLists(service, profiles);
+
+ // Test removing the first profile.
+ removeProfile(profiles, 0);
+ compareLists(service, profiles);
+
+ // And the last profile.
+ removeProfile(profiles, profiles.length - 1);
+ compareLists(service, profiles);
+
+ // Last but one...
+ removeProfile(profiles, profiles.length - 2);
+ compareLists(service, profiles);
+
+ // Second one...
+ removeProfile(profiles, 1);
+ compareLists(service, profiles);
+
+ // Something in the middle.
+ removeProfile(profiles, 2);
+ compareLists(service, profiles);
+
+ let expectedNames = ["profile9", "profile7", "profile5", "profile4"];
+
+ let serviceProfiles = Array.from(service.profiles);
+ for (let i = 0; i < expectedNames.length; i++) {
+ Assert.equal(serviceProfiles[i].name, expectedNames[i]);
+ }
+
+ removeProfile(profiles, 0);
+ removeProfile(profiles, 0);
+ removeProfile(profiles, 0);
+ removeProfile(profiles, 0);
+
+ Assert.equal(Array.from(service.profiles).length, 0, "All profiles gone.");
+ Assert.equal(service.profileCount, 0, "All profiles gone.");
+});
diff --git a/toolkit/profile/xpcshell/test_remove_default.js b/toolkit/profile/xpcshell/test_remove_default.js
new file mode 100644
index 0000000000..f77f2d87d9
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_remove_default.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that calling nsIToolkitProfile.remove on the default profile correctly
+ * removes the profile.
+ */
+
+add_task(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);
+
+ let service = getProfileService();
+ checkProfileService(profilesIni);
+
+ let { profile, didCreate } = selectStartupProfile();
+ Assert.ok(!didCreate, "Should have not created a new profile.");
+ Assert.equal(
+ profile.name,
+ "default",
+ "Should have selected the default profile."
+ );
+ Assert.equal(
+ profile,
+ service.defaultProfile,
+ "Should have selected the default profile."
+ );
+
+ checkProfileService(profilesIni);
+
+ // In an actual run of Firefox we wouldn't be able to delete the profile in
+ // use because it would be locked. But we don't actually lock the profile in
+ // tests.
+ profile.remove(false);
+
+ Assert.ok(!service.defaultProfile, "Should no longer be a default profile.");
+ Assert.equal(
+ profile,
+ service.currentProfile,
+ "Should still be the profile in use."
+ );
+
+ // These are the modifications that should have been made.
+ profilesIni.profiles.pop();
+ profilesIni.installs[hash].default = "";
+
+ // The data isn't flushed to disk so don't check the backup here.
+ checkProfileService(profilesIni, false);
+
+ service.flush();
+
+ // And that should have flushed to disk correctly.
+ checkProfileService();
+
+ // checkProfileService doesn't differentiate between a blank default profile
+ // for the install and a missing install.
+ profilesIni = readProfilesIni();
+ Assert.equal(
+ profilesIni.installs[hash].default,
+ "",
+ "Should be a blank default profile."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js b/toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js
new file mode 100644
index 0000000000..982df276db
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Verify that background tasks don't touch `profiles.ini` for ephemeral profile
+ * tasks.
+ */
+
+let condition = {
+ skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS,
+};
+
+add_task(condition, async () => {
+ writeProfilesIni(BACKGROUNDTASKS_PROFILE_DATA);
+
+ // Pretend that this is a background task. For a task that uses an ephemeral
+ // profile, `profiles.ini` is untouched.
+ const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
+ Ci.nsIBackgroundTasks
+ );
+ bts.overrideBackgroundTaskNameForTesting("ephemeral_profile");
+
+ let { didCreate } = selectStartupProfile();
+ checkStartupReason("backgroundtask-ephemeral");
+
+ Assert.equal(didCreate, true, "Created new ephemeral profile");
+
+ let profileData = readProfilesIni();
+ Assert.deepEqual(BACKGROUNDTASKS_PROFILE_DATA, profileData);
+});
diff --git a/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js
new file mode 100644
index 0000000000..ebed889360
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Verify that background tasks that create non-ephemeral profiles update
+ * `profiles.ini` with a salted profile location.
+ */
+
+let condition = {
+ skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS,
+};
+
+// MOZ_APP_VENDOR is empty on Thunderbird.
+let vendor = AppConstants.MOZ_APP_NAME == "thunderbird" ? "" : "Mozilla";
+
+add_task(condition, async () => {
+ let hash = xreDirProvider.getInstallHash();
+
+ writeProfilesIni(BACKGROUNDTASKS_PROFILE_DATA);
+
+ // Pretend that this is a background task. For a task that does *not* use an
+ // ephemeral profile, i.e., that uses a persistent profile, `profiles.ini` is
+ // updated with a new entry in section `BackgroundTaskProfiles`.
+ const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
+ Ci.nsIBackgroundTasks
+ );
+ // "not_ephemeral_profile" is a special name recognized by the
+ // background task system.
+ bts.overrideBackgroundTaskNameForTesting("not_ephemeral_profile");
+
+ let { didCreate, rootDir } = selectStartupProfile();
+ checkStartupReason("backgroundtask-not-ephemeral");
+
+ Assert.equal(didCreate, true, "Created new non-ephemeral profile");
+
+ let profileData = readProfilesIni();
+
+ // Profile names are lexicographically ordered, and `not_ephemeral_profile`
+ // sorts before `unrelated_task`.
+ Assert.equal(profileData.backgroundTasksProfiles.length, 2);
+ Assert.deepEqual(
+ [profileData.backgroundTasksProfiles[1]],
+ BACKGROUNDTASKS_PROFILE_DATA.backgroundTasksProfiles
+ );
+
+ let saltedPath = profileData.backgroundTasksProfiles[0].path;
+ Assert.ok(
+ saltedPath.endsWith(
+ `.${vendor}BackgroundTask-${hash}-not_ephemeral_profile`
+ ),
+ `${saltedPath} ends with ".${vendor}BackgroundTask-${hash}-not_ephemeral_profile"`
+ );
+ Assert.ok(
+ !saltedPath.startsWith(
+ `.${vendor}BackgroundTask-${hash}-not_ephemeral_profile`
+ ),
+ `${saltedPath} is really salted`
+ );
+ Assert.deepEqual(profileData.backgroundTasksProfiles[0], {
+ name: `${vendor}BackgroundTask-${hash}-not_ephemeral_profile`,
+ path: saltedPath,
+ });
+
+ Assert.ok(
+ rootDir.path.endsWith(saltedPath),
+ `rootDir "${rootDir.path}" ends with salted path "${saltedPath}"`
+ );
+
+ // Really, "UAppData", but this is xpcshell.
+ let backgroundTasksProfilesPath = gDataHome;
+ if (!AppConstants.XP_UNIX || AppConstants.platform === "macosx") {
+ backgroundTasksProfilesPath.append("Background Tasks Profiles");
+ }
+ Assert.ok(
+ rootDir.path.startsWith(backgroundTasksProfilesPath.path),
+ `rootDir "${rootDir.path}" is sibling to user profiles directory ${backgroundTasksProfilesPath}`
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js
new file mode 100644
index 0000000000..6fa3ec5882
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Verify that background tasks that use non-ephemeral profiles re-use existing
+ * salted profile locations from `profiles.ini`.
+ */
+
+let condition = {
+ skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS,
+};
+
+// MOZ_APP_VENDOR is empty on Thunderbird.
+let vendor = AppConstants.MOZ_APP_NAME == "thunderbird" ? "" : "Mozilla";
+
+add_task(condition, async () => {
+ let hash = xreDirProvider.getInstallHash();
+
+ let saltedPath = `saltSALT.${vendor}BackgroundTask-${hash}-not_ephemeral_profile`;
+
+ // See note about ordering below.
+ BACKGROUNDTASKS_PROFILE_DATA.backgroundTasksProfiles.splice(0, 0, {
+ name: `${vendor}BackgroundTask-${hash}-not_ephemeral_profile`,
+ path: saltedPath,
+ });
+
+ writeProfilesIni(BACKGROUNDTASKS_PROFILE_DATA);
+
+ // Pretend that this is a background task. For a task that does *not* use an
+ // ephemeral profile, i.e., that uses a persistent profile, when
+ // `profiles.ini` section `BackgroundTasksProfiles` contains the relevant
+ // entry, that profile path is re-used.
+ const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
+ Ci.nsIBackgroundTasks
+ );
+ // "not_ephemeral_profile" is a special name recognized by the
+ // background task system.
+ bts.overrideBackgroundTaskNameForTesting("not_ephemeral_profile");
+
+ let { didCreate, rootDir } = selectStartupProfile();
+ checkStartupReason("backgroundtask-not-ephemeral");
+
+ Assert.equal(didCreate, false, "Re-used existing non-ephemeral profile");
+
+ let profileData = readProfilesIni();
+
+ // Profile names are lexicographically ordered, and `not_ephemeral_profile`
+ // sorts before `unrelated_task`.
+ Assert.equal(profileData.backgroundTasksProfiles.length, 2);
+ Assert.deepEqual(profileData, BACKGROUNDTASKS_PROFILE_DATA);
+
+ Assert.ok(
+ rootDir.path.endsWith(saltedPath),
+ `rootDir "${rootDir.path}" ends with salted path "${saltedPath}"`
+ );
+
+ // Really, "UAppData", but this is xpcshell.
+ let backgroundTasksProfilesPath = gDataHome;
+ if (!AppConstants.XP_UNIX || AppConstants.platform === "macosx") {
+ backgroundTasksProfilesPath.append("Background Tasks Profiles");
+ }
+ Assert.ok(
+ rootDir.path.startsWith(backgroundTasksProfilesPath.path),
+ `rootDir "${rootDir.path}" is sibling to user profiles directory ${backgroundTasksProfilesPath}`
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_default.js b/toolkit/profile/xpcshell/test_select_default.js
new file mode 100644
index 0000000000..df4b27df7e
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_default.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that from a database of profiles the default profile is selected.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "Path1",
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ installs: {
+ [hash]: {
+ default: "Path2",
+ },
+ },
+ };
+
+ if (AppConstants.MOZ_DEV_EDITION) {
+ profileData.profiles.push(
+ {
+ name: "default",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: PROFILE_DEFAULT,
+ path: "Path4",
+ }
+ );
+ } else {
+ profileData.profiles.push({
+ name: PROFILE_DEFAULT,
+ path: "Path2",
+ default: true,
+ });
+ }
+
+ writeProfilesIni(profileData);
+
+ let { profile, didCreate } = selectStartupProfile();
+ checkStartupReason("default");
+
+ let service = getProfileService();
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.equal(
+ profile,
+ service.defaultProfile,
+ "Should have returned the default profile."
+ );
+ Assert.equal(
+ profile.name,
+ "default",
+ "Should have selected the right profile"
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_environment.js b/toolkit/profile/xpcshell/test_select_environment.js
new file mode 100644
index 0000000000..f7e3138191
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_environment.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the environment variables are used to select a profile.
+ */
+
+add_task(async () => {
+ let dir = makeRandomProfileDir("foo");
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: dir.leafName,
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ Services.env.set("XRE_PROFILE_PATH", dir.path);
+ Services.env.set("XRE_PROFILE_LOCAL_PATH", dir.path);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+ checkStartupReason("restart");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(rootDir.equals(dir), "Should have selected the right root dir.");
+ Assert.ok(localDir.equals(dir), "Should have selected the right local dir.");
+ Assert.ok(!profile, "No named profile matches this.");
+});
diff --git a/toolkit/profile/xpcshell/test_select_environment_named.js b/toolkit/profile/xpcshell/test_select_environment_named.js
new file mode 100644
index 0000000000..348ce0ff0a
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_environment_named.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the environment variables are used to select a profile.
+ */
+
+add_task(async () => {
+ let root = makeRandomProfileDir("foo");
+ let local = gDataHomeLocal.clone();
+ local.append("foo");
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: root.leafName,
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ Services.env.set("XRE_PROFILE_PATH", root.path);
+ Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile([
+ "-P",
+ "Profile3",
+ ]);
+ checkStartupReason("restart");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(rootDir.equals(root), "Should have selected the right root dir.");
+ Assert.ok(
+ localDir.equals(local),
+ "Should have selected the right local dir."
+ );
+ Assert.ok(profile, "A named profile matches this.");
+ Assert.equal(profile.name, "Profile1", "The right profile was matched.");
+
+ let service = getProfileService();
+ Assert.notEqual(
+ service.defaultProfile,
+ profile,
+ "Should not be the default profile."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_missing.js b/toolkit/profile/xpcshell/test_select_missing.js
new file mode 100644
index 0000000000..97da6d76b3
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_missing.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that when choosing an unknown profile the profile manager is shown.
+ */
+
+add_task(async () => {
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "Path1",
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ testStartsProfileManager(["-P", "foo"]);
+});
diff --git a/toolkit/profile/xpcshell/test_select_named.js b/toolkit/profile/xpcshell/test_select_named.js
new file mode 100644
index 0000000000..abecba1f4d
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_named.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that from a database of profiles the correct profile is selected.
+ */
+
+add_task(async () => {
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "Path1",
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+
+ checkProfileService(profileData);
+
+ let { profile, didCreate } = selectStartupProfile(["-P", "Profile1"]);
+ checkStartupReason("argument-p");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.equal(
+ profile.name,
+ "Profile1",
+ "Should have chosen the right profile"
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_noname.js b/toolkit/profile/xpcshell/test_select_noname.js
new file mode 100644
index 0000000000..278b38089c
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_noname.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that when passing the -P command line argument and not passing a
+ * profile name the profile manager is opened.
+ */
+
+add_task(async () => {
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "Path1",
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ testStartsProfileManager(["-P"]);
+});
diff --git a/toolkit/profile/xpcshell/test_select_profile_argument.js b/toolkit/profile/xpcshell/test_select_profile_argument.js
new file mode 100644
index 0000000000..70be506538
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_profile_argument.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that selecting a profile directory with the "profile" argument finds
+ * the matching profile.
+ */
+
+add_task(async () => {
+ let root = makeRandomProfileDir("foo");
+ let local = gDataHomeLocal.clone();
+ local.append("foo");
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: root.leafName,
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile([
+ "-profile",
+ root.path,
+ ]);
+ checkStartupReason("argument-profile");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(rootDir.equals(root), "Should have selected the right root dir.");
+ Assert.ok(
+ localDir.equals(local),
+ "Should have selected the right local dir."
+ );
+ Assert.ok(profile, "A named profile matches this.");
+ Assert.equal(profile.name, "Profile1", "The right profile was matched.");
+
+ let service = getProfileService();
+ Assert.notEqual(
+ service.defaultProfile,
+ profile,
+ "Should not be the default profile."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_select_profile_argument_new.js b/toolkit/profile/xpcshell/test_select_profile_argument_new.js
new file mode 100644
index 0000000000..570abbd19f
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_profile_argument_new.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that selecting a profile directory with the "profile" argument finds
+ * doesn't match the incorrect profile.
+ */
+
+add_task(async () => {
+ let root = makeRandomProfileDir("foo");
+ let local = gDataHomeLocal.clone();
+ local.append("foo");
+ let empty = makeRandomProfileDir("empty");
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: root.leafName,
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile([
+ "-profile",
+ empty.path,
+ ]);
+ checkStartupReason("argument-profile");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(rootDir.equals(empty), "Should have selected the right root dir.");
+ Assert.ok(
+ localDir.equals(empty),
+ "Should have selected the right local dir."
+ );
+ Assert.ok(!profile, "No named profile matches this.");
+});
diff --git a/toolkit/profile/xpcshell/test_select_profilemanager.js b/toolkit/profile/xpcshell/test_select_profilemanager.js
new file mode 100644
index 0000000000..7c8bdd820e
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_select_profilemanager.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that when requested the profile manager is shown.
+ */
+
+add_task(async () => {
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: "Profile1",
+ path: "Path1",
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ default: true,
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ testStartsProfileManager(["-profilemanager"]);
+});
diff --git a/toolkit/profile/xpcshell/test_single_profile_selected.js b/toolkit/profile/xpcshell/test_single_profile_selected.js
new file mode 100644
index 0000000000..1dc1f5c872
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_single_profile_selected.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Previous versions of Firefox automatically used a single profile even if it
+ * wasn't marked as the default. So we should try to upgrade that one if it was
+ * last used by this build. This test checks the case where it was.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ default: false,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-claimed-default");
+
+ let hash = xreDirProvider.getInstallHash();
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ defaultProfile.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ !profileData.installs[hash].locked,
+ "Should not have locked as we're not the default app."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, "default");
+});
diff --git a/toolkit/profile/xpcshell/test_single_profile_unselected.js b/toolkit/profile/xpcshell/test_single_profile_unselected.js
new file mode 100644
index 0000000000..3ad36de387
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_single_profile_unselected.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Previous versions of Firefox automatically used a single profile even if it
+ * wasn't marked as the default. So we should try to upgrade that one if it was
+ * last used by this build. This test checks the case where it wasn't.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ // Just pretend this profile was last used by something in the profile dir.
+ let greDir = gProfD.clone();
+ greDir.append("app");
+ writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ default: false,
+ },
+ ],
+ });
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.ok(!profileData.installs, "Should be no defaults for installs yet.");
+
+ checkProfileService(profileData);
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-skipped-default");
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.ok(
+ !selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, DEDICATED_NAME);
+
+ profileData = readProfilesIni();
+
+ profile = profileData.profiles[0];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should now be marked as the old-style default.");
+
+ checkProfileService(profileData);
+});
diff --git a/toolkit/profile/xpcshell/test_skip_locked_environment.js b/toolkit/profile/xpcshell/test_skip_locked_environment.js
new file mode 100644
index 0000000000..f98f6dd88f
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_skip_locked_environment.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the environment variables are used to select a profile and that
+ * on the first run of a dedicated profile build we don't snatch it if it is
+ * locked by another install.
+ */
+
+add_task(async () => {
+ let root = makeRandomProfileDir("foo");
+ let local = gDataHomeLocal.clone();
+ local.append("foo");
+
+ writeCompatibilityIni(root);
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: root.leafName,
+ default: true,
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ // Another install is using the profile and it is locked.
+ installs: {
+ otherinstall: {
+ default: root.leafName,
+ locked: true,
+ },
+ },
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ Services.env.set("XRE_PROFILE_PATH", root.path);
+ Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+ checkStartupReason("restart-skipped-default");
+
+ // Since there is already a profile with the desired name on dev-edition, a
+ // unique version will be used.
+ let expectedName = AppConstants.MOZ_DEV_EDITION
+ ? `${DEDICATED_NAME}-1`
+ : DEDICATED_NAME;
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.ok(!rootDir.equals(root), "Should have selected the right root dir.");
+ Assert.ok(
+ !localDir.equals(local),
+ "Should have selected the right local dir."
+ );
+ Assert.ok(profile, "A named profile was returned.");
+ Assert.equal(profile.name, expectedName, "The right profile name was used.");
+
+ let service = getProfileService();
+ Assert.equal(
+ service.defaultProfile,
+ profile,
+ "Should be the default profile."
+ );
+ Assert.equal(
+ service.currentProfile,
+ profile,
+ "Should be the current profile."
+ );
+
+ profileData = readProfilesIni();
+
+ Assert.equal(
+ profileData.profiles[0].name,
+ PROFILE_DEFAULT,
+ "Should be the right profile."
+ );
+ Assert.ok(
+ profileData.profiles[0].default,
+ "Should be the old default profile."
+ );
+ Assert.equal(
+ profileData.profiles[0].path,
+ root.leafName,
+ "Should be the correct path."
+ );
+ Assert.equal(
+ profileData.profiles[1].name,
+ expectedName,
+ "Should be the right profile."
+ );
+ Assert.ok(
+ !profileData.profiles[1].default,
+ "Should not be the old default profile."
+ );
+
+ let hash = xreDirProvider.getInstallHash();
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 2,
+ "Should be one known install."
+ );
+ Assert.notEqual(
+ profileData.installs[hash].default,
+ root.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we created the profile for this install."
+ );
+ Assert.equal(
+ profileData.installs.otherinstall.default,
+ root.leafName,
+ "Should have left the other profile as the default for the other install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should still be locked to the other install."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_snap.js b/toolkit/profile/xpcshell/test_snap.js
new file mode 100644
index 0000000000..c29c85089e
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snap.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile not previously used by this build gets
+ * used in a snap environment.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ // Just pretend this profile was last used by something in the profile dir.
+ let greDir = gProfD.clone();
+ greDir.append("app");
+ writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("default");
+
+ let profileData = readProfilesIni();
+ let installsINI = gDataHome.clone();
+ installsINI.append("installs.ini");
+ Assert.ok(
+ !installsINI.exists(),
+ "Installs database should not have been created."
+ );
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
diff --git a/toolkit/profile/xpcshell/test_snap_empty.js b/toolkit/profile/xpcshell/test_snap_empty.js
new file mode 100644
index 0000000000..80b1de9569
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snap_empty.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that from a clean slate snap builds create an appropriate profile.
+ */
+
+add_task(async () => {
+ let service = getProfileService();
+ let { profile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-created-default");
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.equal(
+ profile.name,
+ PROFILE_DEFAULT,
+ "Should have used the normal name."
+ );
+ if (AppConstants.MOZ_DEV_EDITION) {
+ Assert.equal(service.profileCount, 2, "Should be two profiles.");
+ } else {
+ Assert.equal(service.profileCount, 1, "Should be only one profile.");
+ }
+
+ checkProfileService();
+});
diff --git a/toolkit/profile/xpcshell/test_snatch_environment.js b/toolkit/profile/xpcshell/test_snatch_environment.js
new file mode 100644
index 0000000000..2241fb691c
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snatch_environment.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the environment variables are used to select a profile and that
+ * on the first run of a dedicated profile build we snatch it if it was the
+ * default profile.
+ */
+
+add_task(async () => {
+ let root = makeRandomProfileDir("foo");
+ let local = gDataHomeLocal.clone();
+ local.append("foo");
+
+ writeCompatibilityIni(root);
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: root.leafName,
+ default: true,
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ // Another install is using the profile but it isn't locked.
+ installs: {
+ otherinstall: {
+ default: root.leafName,
+ },
+ },
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ Services.env.set("XRE_PROFILE_PATH", root.path);
+ Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+ checkStartupReason("restart-claimed-default");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(rootDir.equals(root), "Should have selected the right root dir.");
+ Assert.ok(
+ localDir.equals(local),
+ "Should have selected the right local dir."
+ );
+ Assert.ok(profile, "A named profile matches this.");
+ Assert.equal(profile.name, PROFILE_DEFAULT, "The right profile was matched.");
+
+ let service = getProfileService();
+ Assert.equal(
+ service.defaultProfile,
+ profile,
+ "Should be the default profile."
+ );
+ Assert.equal(
+ service.currentProfile,
+ profile,
+ "Should be the current profile."
+ );
+
+ profileData = readProfilesIni();
+ Assert.equal(
+ profileData.profiles[0].name,
+ PROFILE_DEFAULT,
+ "Should be the right profile."
+ );
+ Assert.ok(
+ profileData.profiles[0].default,
+ "Should still be the old default profile."
+ );
+
+ let hash = xreDirProvider.getInstallHash();
+ // The info about the other install will have been removed so it goes through first run on next startup.
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ root.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ !profileData.installs[hash].locked,
+ "Should not have locked as we're not the default app."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_snatch_environment_default.js b/toolkit/profile/xpcshell/test_snatch_environment_default.js
new file mode 100644
index 0000000000..d93a0cfc12
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snatch_environment_default.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that the environment variables are used to select a profile and that
+ * on the first run of a dedicated profile build we snatch it if it was the
+ * default profile and lock it when we're the default app.
+ */
+
+add_task(async () => {
+ gIsDefaultApp = true;
+
+ let root = makeRandomProfileDir("foo");
+ let local = gDataHomeLocal.clone();
+ local.append("foo");
+
+ writeCompatibilityIni(root);
+
+ let profileData = {
+ options: {
+ startWithLastProfile: true,
+ },
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: root.leafName,
+ default: true,
+ },
+ {
+ name: "Profile2",
+ path: "Path2",
+ },
+ {
+ name: "Profile3",
+ path: "Path3",
+ },
+ ],
+ // Another install is using the profile but it isn't locked.
+ installs: {
+ otherinstall: {
+ default: root.leafName,
+ },
+ },
+ };
+
+ writeProfilesIni(profileData);
+ checkProfileService(profileData);
+
+ Services.env.set("XRE_PROFILE_PATH", root.path);
+ Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+ let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+ checkStartupReason("restart-claimed-default");
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(rootDir.equals(root), "Should have selected the right root dir.");
+ Assert.ok(
+ localDir.equals(local),
+ "Should have selected the right local dir."
+ );
+ Assert.ok(!!profile, "A named profile matches this.");
+ Assert.equal(profile.name, PROFILE_DEFAULT, "The right profile was matched.");
+
+ let service = getProfileService();
+ Assert.ok(
+ service.defaultProfile === profile,
+ "Should be the default profile."
+ );
+ Assert.ok(
+ service.currentProfile === profile,
+ "Should be the current profile."
+ );
+
+ profileData = readProfilesIni();
+ Assert.equal(
+ profileData.profiles[0].name,
+ PROFILE_DEFAULT,
+ "Should be the right profile."
+ );
+ Assert.ok(
+ profileData.profiles[0].default,
+ "Should still be the old default profile."
+ );
+
+ let hash = xreDirProvider.getInstallHash();
+ // The info about the other install will have been removed so it goes through first run on next startup.
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ root.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we're the default app."
+ );
+});
diff --git a/toolkit/profile/xpcshell/test_startswithlast.js b/toolkit/profile/xpcshell/test_startswithlast.js
new file mode 100644
index 0000000000..1b1fef4415
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_startswithlast.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that if profiles.ini is set to not start with the last profile then
+ * we show the profile manager in preference to assigning the old default.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ options: {
+ startWithLastProfile: false,
+ },
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ testStartsProfileManager();
+});
diff --git a/toolkit/profile/xpcshell/test_steal_inuse.js b/toolkit/profile/xpcshell/test_steal_inuse.js
new file mode 100644
index 0000000000..68263ceae1
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_steal_inuse.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile previously used by this build but
+ * that has already been claimed by a different build gets stolen by this build.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ installs: {
+ otherhash: {
+ default: defaultProfile.leafName,
+ },
+ },
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-claimed-default");
+
+ let hash = xreDirProvider.getInstallHash();
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should only be one known installs."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ defaultProfile.leafName,
+ "Should have taken the original default profile as the default for the current install."
+ );
+ Assert.ok(
+ !profileData.installs[hash].locked,
+ "Should not have locked as we're not the default app."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
diff --git a/toolkit/profile/xpcshell/test_update_selected_dedicated.js b/toolkit/profile/xpcshell/test_update_selected_dedicated.js
new file mode 100644
index 0000000000..5a7c734e90
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_update_selected_dedicated.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile previously used by this build gets
+ * updated to a dedicated profile for this build.
+ */
+
+add_task(async () => {
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-claimed-default");
+
+ let { databaseVersion, profileCount } = getTelemetryScalars();
+ Assert.equal(
+ databaseVersion,
+ "1",
+ "Old database file was present at startup."
+ );
+ Assert.equal(profileCount, 1, "Should be one profile.");
+
+ let hash = xreDirProvider.getInstallHash();
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 1,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be only one known install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ defaultProfile.leafName,
+ "Should have marked the original default profile as the default for this install."
+ );
+ Assert.ok(
+ !profileData.installs[hash].locked,
+ "Should not have locked as we're not the default app."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
+});
diff --git a/toolkit/profile/xpcshell/test_update_unknown_dedicated.js b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js
new file mode 100644
index 0000000000..1fd8c30fbf
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile not previously used by any build
+ * doesn't get updated to a dedicated profile for this build and we don't set
+ * the flag to show the user info about dedicated profiles.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+ let defaultProfile = makeRandomProfileDir("default");
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-created-default");
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 2,
+ "Should have the right number of profiles."
+ );
+
+ // Since there is already a profile with the desired name on dev-edition, a
+ // unique version will be used.
+ let expectedName = AppConstants.MOZ_DEV_EDITION
+ ? `${DEDICATED_NAME}-1`
+ : DEDICATED_NAME;
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, expectedName, "Should have the right name.");
+ Assert.notEqual(
+ profile.path,
+ defaultProfile.leafName,
+ "Should not be the original default profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be a default for installs."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ profile.path,
+ "Should have the right default profile."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we created this profile for this install."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.ok(
+ !selectedProfile.rootDir.equals(defaultProfile),
+ "Should not be using the old directory."
+ );
+ Assert.equal(selectedProfile.name, expectedName);
+});
diff --git a/toolkit/profile/xpcshell/test_update_unselected_dedicated.js b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js
new file mode 100644
index 0000000000..4aa56eaaac
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that an old-style default profile not previously used by this build gets
+ * ignored.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+ let defaultProfile = makeRandomProfileDir("default");
+
+ // Just pretend this profile was last used by something in the profile dir.
+ let greDir = gProfD.clone();
+ greDir.append("app");
+ writeCompatibilityIni(defaultProfile, greDir, greDir);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: PROFILE_DEFAULT,
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ ],
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("firstrun-skipped-default");
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 2,
+ "Should have the right number of profiles."
+ );
+
+ // Since there is already a profile with the desired name on dev-edition, a
+ // unique version will be used.
+ let expectedName = AppConstants.MOZ_DEV_EDITION
+ ? `${DEDICATED_NAME}-1`
+ : DEDICATED_NAME;
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, expectedName, "Should have the right name.");
+ Assert.notEqual(
+ profile.path,
+ defaultProfile.leafName,
+ "Should not be the original default profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 1,
+ "Should be a default for this install."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ profile.path,
+ "Should have marked the new profile as the default for this install."
+ );
+ Assert.ok(
+ profileData.installs[hash].locked,
+ "Should have locked as we created this profile for this install."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(didCreate, "Should have created a new profile.");
+ Assert.ok(
+ !selectedProfile.rootDir.equals(defaultProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, expectedName);
+});
diff --git a/toolkit/profile/xpcshell/test_use_dedicated.js b/toolkit/profile/xpcshell/test_use_dedicated.js
new file mode 100644
index 0000000000..d83b6fc3a7
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_use_dedicated.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that if installs.ini lists a profile we use it as the default.
+ */
+
+add_task(async () => {
+ let hash = xreDirProvider.getInstallHash();
+ let defaultProfile = makeRandomProfileDir("default");
+ let dedicatedProfile = makeRandomProfileDir("dedicated");
+ let devProfile = makeRandomProfileDir("devedition");
+
+ // Make sure we don't steal the old-style default.
+ writeCompatibilityIni(defaultProfile);
+
+ writeProfilesIni({
+ profiles: [
+ {
+ name: "default",
+ path: defaultProfile.leafName,
+ default: true,
+ },
+ {
+ name: "dedicated",
+ path: dedicatedProfile.leafName,
+ },
+ {
+ name: "dev-edition-default",
+ path: devProfile.leafName,
+ },
+ ],
+ installs: {
+ [hash]: {
+ default: dedicatedProfile.leafName,
+ },
+ otherhash: {
+ default: "foobar",
+ },
+ },
+ });
+
+ let { profile: selectedProfile, didCreate } = selectStartupProfile();
+ checkStartupReason("default");
+ let { databaseVersion, profileCount } = getTelemetryScalars();
+ Assert.equal(
+ databaseVersion,
+ "2",
+ "New database file was present at startup."
+ );
+ Assert.equal(profileCount, 3, "Should be three profiles present.");
+
+ let profileData = readProfilesIni();
+
+ Assert.ok(
+ profileData.options.startWithLastProfile,
+ "Should be set to start with the last profile."
+ );
+ Assert.equal(
+ profileData.profiles.length,
+ 3,
+ "Should have the right number of profiles."
+ );
+
+ let profile = profileData.profiles[0];
+ Assert.equal(profile.name, `dedicated`, "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ dedicatedProfile.leafName,
+ "Should be the expected dedicated profile."
+ );
+ Assert.ok(!profile.default, "Should not be marked as the old-style default.");
+
+ profile = profileData.profiles[1];
+ Assert.equal(profile.name, "default", "Should have the right name.");
+ Assert.equal(
+ profile.path,
+ defaultProfile.leafName,
+ "Should be the original default profile."
+ );
+ Assert.ok(profile.default, "Should be marked as the old-style default.");
+
+ Assert.equal(
+ Object.keys(profileData.installs).length,
+ 2,
+ "Should be two known installs."
+ );
+ Assert.equal(
+ profileData.installs[hash].default,
+ dedicatedProfile.leafName,
+ "Should have kept the default for this install."
+ );
+ Assert.equal(
+ profileData.installs.otherhash.default,
+ "foobar",
+ "Should have kept the default for the other install."
+ );
+
+ checkProfileService(profileData);
+
+ Assert.ok(!didCreate, "Should not have created a new profile.");
+ Assert.ok(
+ selectedProfile.rootDir.equals(dedicatedProfile),
+ "Should be using the right directory."
+ );
+ Assert.equal(selectedProfile.name, "dedicated");
+});
diff --git a/toolkit/profile/xpcshell/xpcshell.toml b/toolkit/profile/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..00c00cc6c4
--- /dev/null
+++ b/toolkit/profile/xpcshell/xpcshell.toml
@@ -0,0 +1,97 @@
+[DEFAULT]
+head = "head.js"
+skip-if = ["os == 'android'"]
+
+["test_check_backup.js"]
+
+["test_claim_locked.js"]
+
+["test_clean.js"]
+
+["test_conflict_installs.js"]
+
+["test_conflict_profiles.js"]
+
+["test_create_default.js"]
+
+["test_fix_directory_case.js"]
+
+["test_ignore_legacy_directory.js"]
+
+["test_invalid_descriptor.js"]
+
+["test_legacy_empty.js"]
+
+["test_legacy_select.js"]
+
+["test_lock.js"]
+
+["test_missing_profilesini.js"]
+
+["test_new_default.js"]
+
+["test_previous_dedicated.js"]
+
+["test_profile_reset.js"]
+
+["test_remove.js"]
+
+["test_remove_default.js"]
+
+["test_select_backgroundtasks_ephemeral.js"]
+
+["test_select_backgroundtasks_not_ephemeral_create.js"]
+
+["test_select_backgroundtasks_not_ephemeral_exists.js"]
+
+["test_select_default.js"]
+
+["test_select_environment.js"]
+
+["test_select_environment_named.js"]
+
+["test_select_missing.js"]
+
+["test_select_named.js"]
+
+["test_select_noname.js"]
+
+["test_select_profile_argument.js"]
+
+["test_select_profile_argument_new.js"]
+
+["test_select_profilemanager.js"]
+
+["test_single_profile_selected.js"]
+skip-if = ["devedition"]
+
+["test_single_profile_unselected.js"]
+skip-if = ["devedition"]
+
+["test_skip_locked_environment.js"]
+
+["test_snap.js"]
+snap = true
+run-if = ["os == 'linux'"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_snap_empty.js"]
+snap = true
+run-if = ["os == 'linux'"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_snatch_environment.js"]
+
+["test_snatch_environment_default.js"]
+
+["test_startswithlast.js"]
+
+["test_steal_inuse.js"]
+
+["test_update_selected_dedicated.js"]
+
+["test_update_unknown_dedicated.js"]
+
+["test_update_unselected_dedicated.js"]
+
+["test_use_dedicated.js"]