/* -*- 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 #include #include #include #ifdef XP_WIN # include # include # include "mozilla/PolicyChecks.h" #endif #ifdef XP_UNIX # include #endif #include "nsToolkitProfileService.h" #include "CmdLineAndEnvUtils.h" #include "nsIFile.h" #ifdef XP_MACOSX # include # 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>* array = static_cast>*>(aClosure); array->AppendElement(MakeUnique(aString, aValue)); return true; } /** * Returns an array of the strings inside a section of an ini file. */ nsTArray> GetSectionStrings(nsINIParser* aParser, const char* aSection) { nsTArray> result; aParser->GetStrings(aSection, &GetStrings, &result); return result; } void RemoveProfileRecursion(const nsCOMPtr& aDirectoryOrFile, bool aIsIgnoreRoot, bool aIsIgnoreLockfile, nsTArray>& 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 dirEnum; NS_ENSURE_SUCCESS_VOID( aDirectoryOrFile->GetDirectoryEntries(getter_AddRefs(dirEnum))); bool more = false; while (NS_SUCCEEDED(dirEnum->HasMoreElements(&more)) && more) { nsCOMPtr item; dirEnum->GetNext(getter_AddRefs(item)); nsCOMPtr 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 rootDir; aProfile->GetRootDir(getter_AddRefs(rootDir)); nsCOMPtr 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 lock; NS_ENSURE_SUCCESS_VOID( NS_LockProfilePath(rootDir, localDir, nullptr, getter_AddRefs(lock))); nsCOMPtr 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> 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>{})) { 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 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 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 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 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), 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); if (mMaybeLockProfile) { nsCOMPtr 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 profileDir; nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir)); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr 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 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 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 installs = GetKnownInstalls(); // Cache the installs that use the profile. nsTArray 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 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 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(aClosure); nsTArray> 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", buffer); if (NS_FAILED(rv)) { // This is a profiles.ini written by an older version. We must restore // any install data from the backup. 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 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 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 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 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(mNormalDefault.get()); mProfileDB.DeleteString(profile->mSection.get(), "Default"); } mNormalDefault = aProfile; if (mNormalDefault) { nsToolkitProfile* profile = static_cast(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 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& 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(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[]>(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 lf = GetFileFromEnv("XRE_PROFILE_PATH"); if (lf) { nsCOMPtr 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 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 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 profile; if (delim) { nsCOMPtr 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 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 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 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 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 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> 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 profile = mNormalDefault; if (mUseDevEditionProfile) { profile = mDevEditionDefault; } if (profile) { nsCOMPtr rootDir; profile->GetRootDir(getter_AddRefs(rootDir)); nsCOMPtr 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 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 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 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 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 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 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 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 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 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 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* installs; }; static bool FindInstalls(const char* aSection, void* aClosure) { FindInstallsClosure* closure = static_cast(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 nsToolkitProfileService::GetKnownInstalls() { nsTArray result; FindInstallsClosure closure = {&mProfileDB, &result}; mProfileDB.GetSections(&FindInstalls, &closure); return result; } nsresult nsToolkitProfileService::CreateTimesInternal(nsIFile* aProfileDir) { nsresult rv = NS_ERROR_FAILURE; nsCOMPtr 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 installs = GetKnownInstalls(); if (!installs.IsEmpty()) { nsCString data; nsCString buffer; for (uint32_t i = 0; i < installs.Length(); i++) { nsTArray> 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 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 lf; nsresult rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(lf)); if (NS_SUCCEEDED(rv)) { nsCOMPtr 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 }