diff options
Diffstat (limited to 'toolkit/profile')
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"] |