diff options
Diffstat (limited to 'toolkit/profile')
22 files changed, 3557 insertions, 0 deletions
diff --git a/toolkit/profile/ProfileUnlockerWin.cpp b/toolkit/profile/ProfileUnlockerWin.cpp new file mode 100644 index 000000000..0e0139188 --- /dev/null +++ b/toolkit/profile/ProfileUnlockerWin.cpp @@ -0,0 +1,278 @@ +/* -*- 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) + , mQueryFullProcessImageName(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; + } + + mQueryFullProcessImageName = + reinterpret_cast<QUERYFULLPROCESSIMAGENAME>(::GetProcAddress( + ::GetModuleHandleW(L"kernel32.dll"), + "QueryFullProcessImageNameW")); + if (!mQueryFullProcessImageName) { + return NS_ERROR_NOT_AVAILABLE; + } + + 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 (!mQueryFullProcessImageName(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 (!mQueryFullProcessImageName(::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 000000000..47c91c913 --- /dev/null +++ b/toolkit/profile/ProfileUnlockerWin.h @@ -0,0 +1,60 @@ +/* -*- 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 "nsProfileStringTypes.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); + typedef BOOL (WINAPI *QUERYFULLPROCESSIMAGENAME)(HANDLE, DWORD, LPWSTR, PDWORD); + +private: + nsModuleHandle mRestartMgrModule; + RMSTARTSESSION mRmStartSession; + RMREGISTERRESOURCES mRmRegisterResources; + RMGETLIST mRmGetList; + RMENDSESSION mRmEndSession; + QUERYFULLPROCESSIMAGENAME mQueryFullProcessImageName; + + 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 000000000..1963f66bc --- /dev/null +++ b/toolkit/profile/content/createProfileWizard.js @@ -0,0 +1,225 @@ +/* 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 = Components.classes; +const I = Components.interfaces; + +Components.utils.import("resource://gre/modules/AppConstants.jsm"); + +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"); + + var dirService = C["@mozilla.org/file/directory_service;1"].getService(I.nsIProperties); + gDefaultProfileParent = dirService.get("DefProfRt", I.nsIFile); + + // Initialize the profile location display. + gProfileDisplay = document.getElementById("profileDisplay").firstChild; + 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.show(); + 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) +{ + var finishButton = document.documentElement.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; + } + + document.documentElement.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) +{ + var profiles = gProfileService.profiles; + while (profiles.hasMoreElements()) { + var profile = profiles.getNext().QueryInterface(I.nsIToolkitProfile); + if (profile.name.toLowerCase() == aName.toLowerCase()) + return true; + } + + return false; +} + +// Called when the first wizard page is shown. +function enableNextButton() +{ + document.documentElement.canAdvance = true; +} + +function onFinish() +{ + 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"); + var promptService = C["@mozilla.org/embedcomp/prompt-service;1"]. + getService(I.nsIPromptService); + promptService.alert(window, profileCreationFailedTitle, + profileCreationFailed + "\n" + e); + + return false; + } + + // window.opener is false if the Create Profile Wizard was opened from the + // command line. + if (window.opener) { + // Add new profile to the list in the Profile Manager. + window.opener.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, false); + } + + // Exit the wizard. + return true; +} diff --git a/toolkit/profile/content/createProfileWizard.xul b/toolkit/profile/content/createProfileWizard.xul new file mode 100644 index 000000000..eab1a9341 --- /dev/null +++ b/toolkit/profile/content/createProfileWizard.xul @@ -0,0 +1,74 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<!DOCTYPE wizard [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % profileDTD SYSTEM "chrome://mozapps/locale/profile/createProfileWizard.dtd"> +%profileDTD; +]> + +<wizard id="createProfileWizard" + title="&newprofile.title;" + 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" + onwizardfinish="return onFinish();" + onload="initWizard();" + style="&window.size;"> + + <stringbundle id="bundle_profileManager" + src="chrome://mozapps/locale/profile/profileSelection.properties"/> + + <script type="application/javascript" src="chrome://mozapps/content/profile/createProfileWizard.js"/> + + <wizardpage id="explanation" onpageshow="enableNextButton();"> + <description>&profileCreationExplanation_1.text;</description> + <description>&profileCreationExplanation_2.text;</description> + <description>&profileCreationExplanation_3.text;</description> + <spacer flex="1"/> +#ifdef XP_MACOSX + <description>&profileCreationExplanation_4Mac.text;</description> +#else +#ifdef XP_UNIX + <description>&profileCreationExplanation_4Gnome.text;</description> +#else + <description>&profileCreationExplanation_4.text;</description> +#endif +#endif + </wizardpage> + + <wizardpage id="createProfile" onpageshow="initSecondWizardPage();"> + <description>&profileCreationIntro.text;</description> + + <label accesskey="&profilePrompt.accesskey;" control="ProfileName">&profilePrompt.label;</label> + <textbox id="profileName" value="&profileDefaultName;" + oninput="updateProfileName(this.value);"/> + + <separator/> + + <description>&profileDirectoryExplanation.text;</description> + + <vbox class="indent" flex="1" style="overflow: auto;"> + <description id="profileDisplay">*</description> + </vbox> + + <hbox> + <button label="&button.choosefolder.label;" oncommand="chooseProfileFolder();" + accesskey="&button.choosefolder.accesskey;"/> + + <button id="useDefault" label="&button.usedefault.label;" + oncommand="setDisplayToDefaultFolder(); updateProfileDisplay();" + accesskey="&button.usedefault.accesskey;" disabled="true"/> + </hbox> + + <separator/> + + <description id="finishText">*</description> + </wizardpage> + +</wizard> diff --git a/toolkit/profile/content/profileSelection.js b/toolkit/profile/content/profileSelection.js new file mode 100644 index 000000000..02b9d6873 --- /dev/null +++ b/toolkit/profile/content/profileSelection.js @@ -0,0 +1,269 @@ +/* -*- 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/. */ + +Components.utils.import("resource://gre/modules/AppConstants.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const C = Components.classes; +const I = Components.interfaces; + +const ToolkitProfileService = "@mozilla.org/toolkit/profile-service;1"; + +var gDialogParams; +var gProfileManagerBundle; +var gBrandBundle; +var gProfileService; + +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.documentElement.centerWindowOnScreen(); + + var profilesElement = document.getElementById("profiles"); + + var profileList = gProfileService.profiles; + while (profileList.hasMoreElements()) { + var profile = profileList.getNext().QueryInterface(I.nsIToolkitProfile); + + var listitem = profilesElement.appendItem(profile.name, ""); + + var tooltiptext = + gProfileManagerBundle.getFormattedString("profileTooltip", [profile.name, profile.rootDir.path]); + listitem.setAttribute("tooltiptext", tooltiptext); + listitem.setAttribute("class", "listitem-iconic"); + listitem.profile = profile; + try { + if (profile === gProfileService.selectedProfile) { + 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); + } +} + +function acceptDialog() +{ + 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); + + return false; + } + + var profileLock; + + try { + profileLock = selectedProfile.profile.lock({ value: null }); + } + catch (e) { + if (!selectedProfile.profile.rootDir.exists()) { + var missingTitle = gProfileManagerBundle.getString("profileMissingTitle"); + var missing = + gProfileManagerBundle.getFormattedString("profileMissing", [appName]); + Services.prompt.alert(window, missingTitle, missing); + return false; + } + + var lockedTitle = gProfileManagerBundle.getString("profileLockedTitle"); + var locked = + gProfileManagerBundle.getFormattedString("profileLocked2", [appName, selectedProfile.profile.name, appName]); + Services.prompt.alert(window, lockedTitle, locked); + + return false; + } + gDialogParams.objects.insertElementAt(profileLock.nsIProfileLock, 0, false); + + gProfileService.selectedProfile = selectedProfile.profile; + gProfileService.defaultProfile = selectedProfile.profile; + updateStartupPrefs(); + + gDialogParams.SetInt(0, 1); + + gDialogParams.SetString(0, selectedProfile.profile.name); + + return true; +} + +function exitDialog() +{ + updateStartupPrefs(); + + return true; +} + +function updateStartupPrefs() +{ + var autoSelectLastProfile = document.getElementById("autoSelectLastProfile"); + gProfileService.startWithLastProfile = autoSelectLastProfile.checked; + + /* Bug 257777 */ + gProfileService.startOffline = document.getElementById("offlineState").checked; +} + +// handle key event on listboxes +function onProfilesKey(aEvent) +{ + switch ( aEvent.keyCode ) + { + case KeyEvent.DOM_VK_BACK_SPACE: + if (AppConstants.platform != "macosx") + break; + case KeyEvent.DOM_VK_DELETE: + ConfirmDelete(); + break; + case KeyEvent.DOM_VK_F2: + RenameProfile(); + break; + } +} + +function onProfilesDblClick(aEvent) +{ + if (aEvent.target.localName == "listitem") + document.documentElement.acceptDialog(); +} + +// invoke the createProfile Wizard +function CreateProfileWizard() +{ + window.openDialog('chrome://mozapps/content/profile/createProfileWizard.xul', + '', 'centerscreen,chrome,modal,titlebar', gProfileService); +} + +/** + * 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.setAttribute("class", "listitem-iconic"); + listitem.profile = aProfile; + + profilesElement.ensureElementIsVisible(listitem); + profilesElement.selectItem(listitem); +} + +// 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; + } + catch (e) { + var alTitle = gProfileManagerBundle.getString("profileNameInvalidTitle"); + var alMsg = gProfileManagerBundle.getFormattedString("profileNameInvalid", [newName]); + Services.prompt.alert(window, alTitle, alMsg); + return false; + } + + selectedItem.label = newName; + var tiptext = gProfileManagerBundle. + getFormattedString("profileTooltip", + [newName, selectedProfile.rootDir.path]); + selectedItem.setAttribute("tooltiptext", tiptext); + + return true; + } + + return false; +} + +function ConfirmDelete() +{ + var deleteButton = document.getElementById("delbutton"); + 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; + } + + selectedProfile.remove(deleteFiles); + profileList.removeChild(selectedItem); + if (profileList.firstChild != undefined) { + profileList.selectItem(profileList.firstChild); + } + + return true; +} diff --git a/toolkit/profile/content/profileSelection.xul b/toolkit/profile/content/profileSelection.xul new file mode 100644 index 000000000..e5dfabb42 --- /dev/null +++ b/toolkit/profile/content/profileSelection.xul @@ -0,0 +1,70 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://mozapps/skin/profile/profileSelection.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % profileDTD SYSTEM "chrome://mozapps/locale/profile/profileSelection.dtd"> +%profileDTD; +]> + +<dialog + id="profileWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="non-resizable" + title="&windowtitle.label;" + orient="vertical" + buttons="accept,cancel" + style="width: 30em;" + onload="startup();" + ondialogaccept="return acceptDialog()" + ondialogcancel="return exitDialog()" + buttonlabelaccept="&start.label;" + buttonlabelcancel="&exit.label;"> + + <stringbundle id="bundle_profileManager" + src="chrome://mozapps/locale/profile/profileSelection.properties"/> + <stringbundle id="bundle_brand" + src="chrome://branding/locale/brand.properties"/> + + <script type="application/javascript" src="chrome://mozapps/content/profile/profileSelection.js"/> + + <description class="label">&pmDescription.label;</description> + + <separator class="thin"/> + + <hbox class="profile-box indent" flex="1"> + + <vbox id="managebuttons"> + <button id="newbutton" label="&newButton.label;" + accesskey="&newButton.accesskey;" oncommand="CreateProfileWizard();"/> + <button id="renbutton" label="&renameButton.label;" + accesskey="&renameButton.accesskey;" oncommand="RenameProfile();"/> + <button id="delbutton" label="&deleteButton.label;" + accesskey="&deleteButton.accesskey;" oncommand="ConfirmDelete();"/> + </vbox> + + <separator flex="1"/> + + <vbox flex="1"> + <listbox id="profiles" rows="5" seltype="single" + ondblclick="onProfilesDblClick(event)" + onkeypress="onProfilesKey(event);"> + </listbox> + + <!-- Bug 257777 --> + <checkbox id="offlineState" label="&offlineState.label;" accesskey="&offlineState.accesskey;"/> + + <checkbox id="autoSelectLastProfile" label="&useSelected.label;" + accesskey="&useSelected.accesskey;"/> + </vbox> + + </hbox> +</dialog> diff --git a/toolkit/profile/gtest/TestProfileLock.cpp b/toolkit/profile/gtest/TestProfileLock.cpp new file mode 100644 index 000000000..ac5117d74 --- /dev/null +++ b/toolkit/profile/gtest/TestProfileLock.cpp @@ -0,0 +1,116 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsProfileLock.h" +#include "nsString.h" + +static void CleanParentLock(const char *tmpdir) +{ + // nsProfileLock doesn't clean up all its files + char permanent_lockfile[] = "/.parentlock"; + + char * parentlock_name; + size_t parentlock_name_size = strlen(tmpdir) + strlen(permanent_lockfile) + 1; + parentlock_name = (char*)malloc(parentlock_name_size); + + strcpy(parentlock_name, tmpdir); + strcat(parentlock_name, permanent_lockfile); + + EXPECT_EQ(0, unlink(parentlock_name)); + EXPECT_EQ(0, rmdir(tmpdir)); + free(parentlock_name); +} + +TEST(ProfileLock, BasicLock) +{ + char templ[] = "/tmp/profilelocktest.XXXXXX"; + char * tmpdir = mkdtemp(templ); + ASSERT_NE(tmpdir, nullptr); + + // This scope exits so the nsProfileLock destructor + // can clean up the files it leaves in tmpdir. + { + nsProfileLock myLock; + 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); + + rv = myLock.Lock(dir, nullptr); + EXPECT_EQ(NS_SUCCEEDED(rv), true); + } + + CleanParentLock(tmpdir); +} + +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); + } + + CleanParentLock(tmpdir); +} diff --git a/toolkit/profile/gtest/moz.build b/toolkit/profile/gtest/moz.build new file mode 100644 index 000000000..51b420e19 --- /dev/null +++ b/toolkit/profile/gtest/moz.build @@ -0,0 +1,16 @@ +# -*- 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 += [ + '..', +] + +if CONFIG['OS_ARCH'] == 'Linux': + UNIFIED_SOURCES += [ + 'TestProfileLock.cpp', + ] + +FINAL_LIBRARY = 'xul-gtest' diff --git a/toolkit/profile/jar.mn b/toolkit/profile/jar.mn new file mode 100644 index 000000000..9b7c22266 --- /dev/null +++ b/toolkit/profile/jar.mn @@ -0,0 +1,9 @@ +# 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.xul (content/createProfileWizard.xul) + content/mozapps/profile/profileSelection.js (content/profileSelection.js) + content/mozapps/profile/profileSelection.xul (content/profileSelection.xul) diff --git a/toolkit/profile/moz.build b/toolkit/profile/moz.build new file mode 100644 index 000000000..b2383a871 --- /dev/null +++ b/toolkit/profile/moz.build @@ -0,0 +1,43 @@ +# -*- 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.ini'] + +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' + ] + +UNIFIED_SOURCES += [ + 'nsToolkitProfileService.cpp' +] + +LOCAL_INCLUDES += [ + '../xre', +] + +FINAL_LIBRARY = 'xul' + +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 000000000..eab1e2c34 --- /dev/null +++ b/toolkit/profile/notifications.txt @@ -0,0 +1,61 @@ +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 + +"shutdown-persist" + The user is logging out and whatever data the observer stores + for the current profile should be released from memory and + saved to disk. + The following topics happen in this context: + profile-change-net-teardown + profile-change-teardown + profile-before-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 000000000..e2351ca9b --- /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 ACString 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 000000000..cd1a71051 --- /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 000000000..8d0c07c51 --- /dev/null +++ b/toolkit/profile/nsIToolkitProfile.idl @@ -0,0 +1,89 @@ +/* -*- 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, 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); + + /** + * 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 000000000..46a5b3cbc --- /dev/null +++ b/toolkit/profile/nsIToolkitProfileService.idl @@ -0,0 +1,108 @@ +/* -*- 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, uuid(1947899b-f369-48fa-89da-f7c37bb1e6bc)] +interface nsIToolkitProfileService : nsISupports +{ + attribute boolean startWithLastProfile; + attribute boolean startOffline; + + readonly attribute nsISimpleEnumerator /*nsIToolkitProfile*/ profiles; + + /** + * The currently selected profile (the one used or about to be used by the + * browser). + */ + attribute nsIToolkitProfile selectedProfile; + + /** + * The default profile (the one used or about to be used by the + * browser if no other profile is specified at runtime). This is the profile + * marked with Default=1 in profiles.ini and is usually the same as + * selectedProfile, except on Developer Edition. + * + * Developer Edition uses a profile named "dev-edition-default" as the + * default profile (which it creates if it doesn't exist), unless a special + * empty file named "ignore-dev-edition-profile" is present next to + * profiles.ini. In that case Developer Edition behaves the same as any + * other build of Firefox. + */ + attribute nsIToolkitProfile defaultProfile; + + /** + * 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); + + /** + * Lock an arbitrary path as a profile. If the path does not exist, it + * will be created and the defaults copied from the application directory. + */ + nsIProfileLock lockProfilePath(in nsIFile aDirectory, + in nsIFile aTempDirectory); + + /** + * Create a new profile. + * + * The profile temporary directory will be chosen based on where the + * profile directory is located. + * + * @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 the default profile for an application. + * + * The profile will be typically in + * {Application Data}/.profilename/{salt}.default or + * {Application Data}/.appname/{salt}.default + * or if aVendorName is provided + * {Application Data}/.vendor/appname/{salt}.default + * + * @note Either aProfileName or aAppName must be non-empty + * + * @param aProfileName + * The name of the profile + * @param aAppName + * The name of the application + * @param aVendorName + * The name of the vendor + * @return The created profile. + */ + nsIToolkitProfile createDefaultProfileForApp(in AUTF8String aProfileName, + in AUTF8String aAppName, + in AUTF8String aVendorName); + + /** + * Returns the number of profiles. + * @return 0, 1, or 2. More than 2 profiles will always return 2. + */ + readonly attribute unsigned long profileCount; + + /** + * Flush the profiles list file. + */ + 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 000000000..08d109224 --- /dev/null +++ b/toolkit/profile/nsProfileLock.cpp @@ -0,0 +1,661 @@ +/* -*- Mode: C++; tab-width: 2; 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 "nsProfileStringTypes.h" +#include "nsProfileLock.h" +#include "nsCOMPtr.h" +#include "nsQueryObject.h" + +#if defined(XP_WIN) +#include "ProfileUnlockerWin.h" +#include "nsAutoPtr.h" +#endif + +#if defined(XP_MACOSX) +#include <Carbon/Carbon.h> +#include <CoreFoundation/CoreFoundation.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 "prprf.h" +#include "prenv.h" +#endif + +#if defined(MOZ_WIDGET_GONK) && !defined(MOZ_CRASHREPORTER) +#include <sys/syscall.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(); +} + + +#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: + NS_NOTREACHED("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); + } + } + +#ifdef MOZ_WIDGET_GONK + switch (signo) { + case SIGQUIT: + case SIGILL: + case SIGABRT: + case SIGSEGV: +#ifndef MOZ_CRASHREPORTER + // Retrigger the signal for those that can generate a core dump + signal(signo, SIG_DFL); + if (info->si_code <= 0) { + if (syscall(__NR_tgkill, getpid(), syscall(__NR_gettid), signo) < 0) { + break; + } + } +#endif + return; + default: + break; + } +#endif + + // Backstop exit call, just in case. + _exit(signo); +} + +nsresult nsProfileLock::LockWithFcntl(nsIFile *aLockFile) +{ + 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) + { + 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); + + 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); + } + + char *signature = + PR_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, 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; + } + + PR_smprintf_free(signature); + signature = nullptr; + + 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; +#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; +} + +nsresult nsProfileLock::Lock(nsIFile* aProfileDir, + nsIProfileUnlocker* *aUnlocker) +{ +#if defined (XP_MACOSX) + NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, ".parentlock"); + NS_NAMED_LITERAL_STRING(OLD_LOCKFILE_NAME, "parent.lock"); +#elif defined (XP_UNIX) + NS_NAMED_LITERAL_STRING(OLD_LOCKFILE_NAME, "lock"); + NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, ".parentlock"); +#else + NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, "parent.lock"); +#endif + + 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; + +#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); + } + + if (NS_SUCCEEDED(rv)) + { + // Check for the old-style lock used by pre-mozilla 1.3 builds. + // Those builds used an earlier check to prevent the application + // from launching if another instance was already running. Because + // of that, we don't need to create an old-style lock as well. + struct LockProcessInfo + { + ProcessSerialNumber psn; + unsigned long launchDate; + }; + + PRFileDesc *fd = nullptr; + int32_t ioBytes; + ProcessInfoRec processInfo; + LockProcessInfo lockProcessInfo; + + rv = lockFile->SetLeafName(OLD_LOCKFILE_NAME); + if (NS_FAILED(rv)) + return rv; + rv = lockFile->OpenNSPRFileDesc(PR_RDONLY, 0, &fd); + if (NS_SUCCEEDED(rv)) + { + ioBytes = PR_Read(fd, &lockProcessInfo, sizeof(LockProcessInfo)); + PR_Close(fd); + + if (ioBytes == sizeof(LockProcessInfo)) + { +#ifdef __LP64__ + processInfo.processAppRef = nullptr; +#else + processInfo.processAppSpec = nullptr; +#endif + processInfo.processName = nullptr; + processInfo.processInfoLength = sizeof(ProcessInfoRec); + if (::GetProcessInformation(&lockProcessInfo.psn, &processInfo) == noErr && + processInfo.processLaunchDate == lockProcessInfo.launchDate) + { + return NS_ERROR_FILE_ACCESS_DENIED; + } + } + else + { + NS_WARNING("Could not read lock file - ignoring lock"); + } + } + rv = NS_OK; // Don't propagate error from OpenNSPRFileDesc. + } +#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); + 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; +} diff --git a/toolkit/profile/nsProfileLock.h b/toolkit/profile/nsProfileLock.h new file mode 100644 index 000000000..e78a3577e --- /dev/null +++ b/toolkit/profile/nsProfileLock.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 2; 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/. */ + +#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); + + /** + * Get the modification time of a replaced profile lock, otherwise 0. + */ + nsresult GetReplacedLockTime(PRTime* aResult); + +private: + bool mHaveLock; + PRTime mReplacedLockTime; + +#if defined (XP_WIN) + HANDLE mLockFileHandle; +#elif defined (XP_UNIX) + + struct RemovePidLockFilesExiting { + RemovePidLockFilesExiting() {} + ~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); + + /** + * @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/nsProfileStringTypes.h b/toolkit/profile/nsProfileStringTypes.h new file mode 100644 index 000000000..fddea519b --- /dev/null +++ b/toolkit/profile/nsProfileStringTypes.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; 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/. */ + +/** + * We support two builds of the directory service provider. + * One, linked into the profile component, uses the internal + * string API. The other can be used by standalone embedding + * clients, and uses embed strings. + * To keep the code clean, we are using typedefs to equate + * embed/internal string types. We are also defining some + * internal macros in terms of the embedding strings API. + * + * When modifying the profile directory service provider, be + * sure to use methods supported by both the internal and + * embed strings APIs. + */ + +#ifndef MOZILLA_INTERNAL_API + +#include "nsEmbedString.h" + +typedef nsCString nsPromiseFlatCString; +typedef nsCString nsAutoCString; + +#define PromiseFlatCString nsCString + +#else +#include "nsString.h" +#include "nsPromiseFlatString.h" +#endif diff --git a/toolkit/profile/nsToolkitProfileService.cpp b/toolkit/profile/nsToolkitProfileService.cpp new file mode 100644 index 000000000..38b3a37f1 --- /dev/null +++ b/toolkit/profile/nsToolkitProfileService.cpp @@ -0,0 +1,1117 @@ +/* -*- Mode: C++; 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 "mozilla/ArrayUtils.h" +#include "mozilla/UniquePtr.h" + +#include <stdio.h> +#include <stdlib.h> +#include <prprf.h> +#include <prtime.h> +#include "nsProfileLock.h" + +#ifdef XP_WIN +#include <windows.h> +#include <shlobj.h> +#endif +#ifdef XP_UNIX +#include <unistd.h> +#endif + +#include "nsIToolkitProfileService.h" +#include "nsIToolkitProfile.h" +#include "nsIFactory.h" +#include "nsIFile.h" +#include "nsISimpleEnumerator.h" + +#ifdef XP_MACOSX +#include <CoreFoundation/CoreFoundation.h> +#include "nsILocalFileMac.h" +#endif + +#include "nsAppDirectoryServiceDefs.h" +#include "nsXULAppAPI.h" + +#include "nsINIParser.h" +#include "nsXREDirProvider.h" +#include "nsAppRunner.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsNativeCharsetUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Sprintf.h" + +using namespace mozilla; + +class nsToolkitProfile final : public nsIToolkitProfile +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSITOOLKITPROFILE + + friend class nsToolkitProfileService; + RefPtr<nsToolkitProfile> mNext; + nsToolkitProfile *mPrev; + +private: + ~nsToolkitProfile() { } + + nsToolkitProfile(const nsACString& aName, + nsIFile* aRootDir, + nsIFile* aLocalDir, + nsToolkitProfile* aPrev, + bool aForExternalApp); + + friend class nsToolkitProfileLock; + + nsCString mName; + nsCOMPtr<nsIFile> mRootDir; + nsCOMPtr<nsIFile> mLocalDir; + nsIProfileLock* mLock; + bool mForExternalApp; +}; + +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() { } + +private: + ~nsToolkitProfileLock(); + + RefPtr<nsToolkitProfile> mProfile; + nsCOMPtr<nsIFile> mDirectory; + nsCOMPtr<nsIFile> mLocalDirectory; + + nsProfileLock mLock; +}; + +class nsToolkitProfileFactory final : public nsIFactory +{ + ~nsToolkitProfileFactory() {} +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIFACTORY +}; + +class nsToolkitProfileService final : public nsIToolkitProfileService +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSITOOLKITPROFILESERVICE + +private: + friend class nsToolkitProfile; + friend class nsToolkitProfileFactory; + friend nsresult NS_NewToolkitProfileService(nsIToolkitProfileService**); + + nsToolkitProfileService() : + mDirty(false), + mStartWithLast(true), + mStartOffline(false) + { + gService = this; + } + ~nsToolkitProfileService() + { + gService = nullptr; + } + + nsresult Init(); + + nsresult CreateTimesInternal(nsIFile *profileDir); + + nsresult CreateProfileInternal(nsIFile* aRootDir, + const nsACString& aName, + const nsACString* aProfileName, + const nsACString* aAppName, + const nsACString* aVendorName, + bool aForExternalApp, + nsIToolkitProfile** aResult); + + RefPtr<nsToolkitProfile> mFirst; + nsCOMPtr<nsIToolkitProfile> mChosen; + nsCOMPtr<nsIToolkitProfile> mDefault; + nsCOMPtr<nsIFile> mAppData; + nsCOMPtr<nsIFile> mTempData; + nsCOMPtr<nsIFile> mListFile; + bool mDirty; + bool mStartWithLast; + bool mStartOffline; + + static nsToolkitProfileService *gService; + + class ProfileEnumerator final : public nsISimpleEnumerator + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISIMPLEENUMERATOR + + explicit ProfileEnumerator(nsToolkitProfile *first) + { mCurrent = first; } + private: + ~ProfileEnumerator() { } + RefPtr<nsToolkitProfile> mCurrent; + }; +}; + +nsToolkitProfile::nsToolkitProfile(const nsACString& aName, + nsIFile* aRootDir, + nsIFile* aLocalDir, + nsToolkitProfile* aPrev, + bool aForExternalApp) : + mPrev(aPrev), + mName(aName), + mRootDir(aRootDir), + mLocalDir(aLocalDir), + mLock(nullptr), + mForExternalApp(aForExternalApp) +{ + NS_ASSERTION(aRootDir, "No file!"); + + if (!aForExternalApp) { + if (aPrev) { + aPrev->mNext = this; + } else { + nsToolkitProfileService::gService->mFirst = this; + } + } +} + +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?"); + NS_ENSURE_TRUE(!mForExternalApp, NS_ERROR_NOT_IMPLEMENTED); + + mName = aName; + nsToolkitProfileService::gService->mDirty = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfile::Remove(bool removeFiles) +{ + NS_ASSERTION(nsToolkitProfileService::gService, + "Whoa, my service is gone."); + + NS_ENSURE_TRUE(!mForExternalApp, NS_ERROR_NOT_IMPLEMENTED); + + if (mLock) + return NS_ERROR_FILE_IS_LOCKED; + + if (!mPrev && !mNext && nsToolkitProfileService::gService->mFirst != this) + return NS_ERROR_NOT_INITIALIZED; + + if (removeFiles) { + bool equals; + nsresult rv = mRootDir->Equals(mLocalDir, &equals); + if (NS_FAILED(rv)) + return rv; + + // The root dir might contain the temp dir, so remove + // the temp dir first. + if (!equals) + mLocalDir->Remove(true); + + mRootDir->Remove(true); + } + + if (mPrev) + mPrev->mNext = mNext; + else + nsToolkitProfileService::gService->mFirst = mNext; + + if (mNext) + mNext->mPrev = mPrev; + + mPrev = nullptr; + mNext = nullptr; + + if (nsToolkitProfileService::gService->mChosen == this) + nsToolkitProfileService::gService->mChosen = nullptr; + + nsToolkitProfileService::gService->mDirty = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfile::Lock(nsIProfileUnlocker* *aUnlocker, nsIProfileLock* *aResult) +{ + if (mLock) { + NS_ADDREF(*aResult = mLock); + return NS_OK; + } + + RefPtr<nsToolkitProfileLock> lock = new nsToolkitProfileLock(); + if (!lock) return NS_ERROR_OUT_OF_MEMORY; + + 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; + } + + 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) + +nsresult +nsToolkitProfileService::Init() +{ + NS_ASSERTION(gDirServiceProvider, "No dirserviceprovider!"); + nsresult rv; + + rv = gDirServiceProvider->GetUserAppDataDirectory(getter_AddRefs(mAppData)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = gDirServiceProvider->GetUserLocalDataDirectory(getter_AddRefs(mTempData)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mAppData->Clone(getter_AddRefs(mListFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mListFile->AppendNative(NS_LITERAL_CSTRING("profiles.ini")); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = mListFile->IsFile(&exists); + if (NS_FAILED(rv) || !exists) { + return NS_OK; + } + + int64_t size; + rv = mListFile->GetFileSize(&size); + if (NS_FAILED(rv) || !size) { + return NS_OK; + } + + nsINIParser parser; + rv = parser.Init(mListFile); + // Init does not fail on parsing errors, only on OOM/really unexpected + // conditions. + if (NS_FAILED(rv)) + return rv; + + nsAutoCString buffer; + rv = parser.GetString("General", "StartWithLastProfile", buffer); + if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("0")) + mStartWithLast = false; + + nsToolkitProfile* currentProfile = nullptr; + +#ifdef MOZ_DEV_EDITION + nsCOMPtr<nsIFile> ignoreSeparateProfile; + rv = mAppData->Clone(getter_AddRefs(ignoreSeparateProfile)); + if (NS_FAILED(rv)) + return rv; + + rv = ignoreSeparateProfile->AppendNative(NS_LITERAL_CSTRING("ignore-dev-edition-profile")); + if (NS_FAILED(rv)) + return rv; + + bool shouldIgnoreSeparateProfile; + rv = ignoreSeparateProfile->Exists(&shouldIgnoreSeparateProfile); + if (NS_FAILED(rv)) + return rv; +#endif + + unsigned int c = 0; + bool foundAuroraDefault = false; + for (c = 0; true; ++c) { + nsAutoCString profileID("Profile"); + profileID.AppendInt(c); + + rv = parser.GetString(profileID.get(), "IsRelative", buffer); + if (NS_FAILED(rv)) break; + + bool isRelative = buffer.EqualsLiteral("1"); + + nsAutoCString filePath; + + rv = parser.GetString(profileID.get(), "Path", filePath); + if (NS_FAILED(rv)) { + NS_ERROR("Malformed profiles.ini: Path= not found"); + continue; + } + + nsAutoCString name; + + rv = parser.GetString(profileID.get(), "Name", name); + if (NS_FAILED(rv)) { + NS_ERROR("Malformed profiles.ini: Name= not found"); + continue; + } + + nsCOMPtr<nsIFile> rootDir; + rv = NS_NewNativeLocalFile(EmptyCString(), 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(EmptyCString(), true, + getter_AddRefs(localDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = localDir->SetRelativeDescriptor(mTempData, filePath); + } else { + localDir = rootDir; + } + + currentProfile = new nsToolkitProfile(name, + rootDir, localDir, + currentProfile, false); + NS_ENSURE_TRUE(currentProfile, NS_ERROR_OUT_OF_MEMORY); + + rv = parser.GetString(profileID.get(), "Default", buffer); + if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("1") && !foundAuroraDefault) { + mChosen = currentProfile; + this->SetDefaultProfile(currentProfile); + } +#ifdef MOZ_DEV_EDITION + // Use the dev-edition-default profile if this is an Aurora build and + // ignore-dev-edition-profile is not present. + if (name.EqualsLiteral("dev-edition-default") && !shouldIgnoreSeparateProfile) { + mChosen = currentProfile; + foundAuroraDefault = true; + } +#endif + } + +#ifdef MOZ_DEV_EDITION + if (!foundAuroraDefault && !shouldIgnoreSeparateProfile) { + // If a single profile exists, it may not be already marked as default. + // Do it now to avoid problems when we create the dev-edition-default profile. + if (!mChosen && mFirst && !mFirst->mNext) + this->SetDefaultProfile(mFirst); + + // Create a default profile for aurora, if none was found. + nsCOMPtr<nsIToolkitProfile> profile; + rv = CreateProfile(nullptr, + NS_LITERAL_CSTRING("dev-edition-default"), + getter_AddRefs(profile)); + if (NS_FAILED(rv)) return rv; + mChosen = profile; + rv = Flush(); + if (NS_FAILED(rv)) return rv; + } +#endif + + if (!mChosen && mFirst && !mFirst->mNext) // only one profile + mChosen = mFirst; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::SetStartWithLastProfile(bool aValue) +{ + if (mStartWithLast != aValue) { + mStartWithLast = aValue; + mDirty = true; + } + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetStartWithLastProfile(bool *aResult) +{ + *aResult = mStartWithLast; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetStartOffline(bool *aResult) +{ + *aResult = mStartOffline; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::SetStartOffline(bool aValue) +{ + mStartOffline = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetProfiles(nsISimpleEnumerator* *aResult) +{ + *aResult = new ProfileEnumerator(this->mFirst); + if (!*aResult) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(*aResult); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsToolkitProfileService::ProfileEnumerator, + nsISimpleEnumerator) + +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->mNext; + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetSelectedProfile(nsIToolkitProfile* *aResult) +{ + if (!mChosen && mFirst && !mFirst->mNext) // only one profile + mChosen = mFirst; + + if (!mChosen) return NS_ERROR_FAILURE; + + NS_ADDREF(*aResult = mChosen); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::SetSelectedProfile(nsIToolkitProfile* aProfile) +{ + if (mChosen != aProfile) { + mChosen = aProfile; + mDirty = true; + } + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile* *aResult) +{ + if (!mDefault) return NS_ERROR_FAILURE; + + NS_ADDREF(*aResult = mDefault); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile) +{ + if (mDefault != aProfile) { + mDefault = aProfile; + mDirty = true; + } + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetProfileByName(const nsACString& aName, + nsIToolkitProfile* *aResult) +{ + nsToolkitProfile* curP = mFirst; + while (curP) { + if (curP->mName.Equals(aName)) { + NS_ADDREF(*aResult = curP); + return NS_OK; + } + curP = curP->mNext; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsToolkitProfileService::LockProfilePath(nsIFile* aDirectory, + nsIFile* aLocalDirectory, + nsIProfileLock* *aResult) +{ + return NS_LockProfilePath(aDirectory, aLocalDirectory, nullptr, aResult); +} + +nsresult +NS_LockProfilePath(nsIFile* aPath, nsIFile* aTempPath, + nsIProfileUnlocker* *aUnlocker, nsIProfileLock* *aResult) +{ + RefPtr<nsToolkitProfileLock> lock = new nsToolkitProfileLock(); + if (!lock) return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = lock->Init(aPath, aTempPath, aUnlocker); + if (NS_FAILED(rv)) return rv; + + lock.forget(aResult); + return NS_OK; +} + +static const char kTable[] = + { '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' }; + +static void SaltProfileName(nsACString& aName) +{ + double fpTime = double(PR_Now()); + + // use 1e-6, granularity of PR_Now() on the mac is seconds + srand((unsigned int)(fpTime * 1e-6 + 0.5)); + + char salt[9]; + + int i; + for (i = 0; i < 8; ++i) + salt[i] = kTable[rand() % ArrayLength(kTable)]; + + salt[8] = '.'; + + aName.Insert(salt, 0, 9); +} + +NS_IMETHODIMP +nsToolkitProfileService::CreateDefaultProfileForApp(const nsACString& aProfileName, + const nsACString& aAppName, + const nsACString& aVendorName, + nsIToolkitProfile** aResult) +{ + NS_ENSURE_STATE(!aProfileName.IsEmpty() || !aAppName.IsEmpty()); + nsCOMPtr<nsIFile> appData; + nsresult rv = + gDirServiceProvider->GetUserDataDirectory(getter_AddRefs(appData), + false, + &aProfileName, + &aAppName, + &aVendorName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> profilesini; + appData->Clone(getter_AddRefs(profilesini)); + rv = profilesini->AppendNative(NS_LITERAL_CSTRING("profiles.ini")); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists = false; + profilesini->Exists(&exists); + NS_ENSURE_FALSE(exists, NS_ERROR_ALREADY_INITIALIZED); + + rv = CreateProfileInternal(nullptr, + NS_LITERAL_CSTRING("default"), + &aProfileName, &aAppName, &aVendorName, + true, aResult); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(*aResult); + + nsCOMPtr<nsIFile> rootDir; + (*aResult)->GetRootDir(getter_AddRefs(rootDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString profileDir; + rv = rootDir->GetRelativeDescriptor(appData, profileDir); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString ini; + ini.SetCapacity(512); + ini.AppendLiteral("[General]\n"); + ini.AppendLiteral("StartWithLastProfile=1\n\n"); + + ini.AppendLiteral("[Profile0]\n"); + ini.AppendLiteral("Name=default\n"); + ini.AppendLiteral("IsRelative=1\n"); + ini.AppendLiteral("Path="); + ini.Append(profileDir); + ini.Append('\n'); + ini.AppendLiteral("Default=1\n\n"); + + FILE* writeFile; + rv = profilesini->OpenANSIFileDesc("w", &writeFile); + NS_ENSURE_SUCCESS(rv, rv); + + if (fwrite(ini.get(), sizeof(char), ini.Length(), writeFile) != + ini.Length()) { + rv = NS_ERROR_UNEXPECTED; + } + fclose(writeFile); + return rv; +} + +NS_IMETHODIMP +nsToolkitProfileService::CreateProfile(nsIFile* aRootDir, + const nsACString& aName, + nsIToolkitProfile** aResult) +{ + return CreateProfileInternal(aRootDir, aName, + nullptr, nullptr, nullptr, false, aResult); +} + +nsresult +nsToolkitProfileService::CreateProfileInternal(nsIFile* aRootDir, + const nsACString& aName, + const nsACString* aProfileName, + const nsACString* aAppName, + const nsACString* aVendorName, + bool aForExternalApp, + nsIToolkitProfile** aResult) +{ + nsresult rv = NS_ERROR_FAILURE; + + if (!aForExternalApp) { + rv = GetProfileByName(aName, aResult); + if (NS_SUCCEEDED(rv)) { + return rv; + } + } + + nsCOMPtr<nsIFile> rootDir (aRootDir); + + nsAutoCString dirName; + if (!rootDir) { + rv = gDirServiceProvider->GetUserProfilesRootDir(getter_AddRefs(rootDir), + aProfileName, aAppName, + aVendorName); + 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(EmptyCString(), 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); + + nsToolkitProfile* last = aForExternalApp ? nullptr : mFirst.get(); + if (last) { + while (last->mNext) + last = last->mNext; + } + + nsCOMPtr<nsIToolkitProfile> profile = + new nsToolkitProfile(aName, rootDir, localDir, last, aForExternalApp); + if (!profile) return NS_ERROR_OUT_OF_MEMORY; + + profile.forget(aResult); + return NS_OK; +} + +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(NS_LITERAL_CSTRING("times.json")); + 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}\n", msec); + PR_Close(writeFile); + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::GetProfileCount(uint32_t *aResult) +{ + if (!mFirst) + *aResult = 0; + else if (! mFirst->mNext) + *aResult = 1; + else + *aResult = 2; + + return NS_OK; +} + +NS_IMETHODIMP +nsToolkitProfileService::Flush() +{ + // Errors during writing might cause unhappy semi-written files. + // To avoid this, write the entire thing to a buffer, then write + // that buffer to disk. + + nsresult rv; + uint32_t pCount = 0; + nsToolkitProfile *cur; + + for (cur = mFirst; cur != nullptr; cur = cur->mNext) + ++pCount; + + uint32_t length; + const int bufsize = 100+MAXPATHLEN*pCount; + auto buffer = MakeUnique<char[]>(bufsize); + + char *pos = buffer.get(); + char *end = pos + bufsize; + + pos += snprintf(pos, end - pos, + "[General]\n" + "StartWithLastProfile=%s\n\n", + mStartWithLast ? "1" : "0"); + + nsAutoCString path; + cur = mFirst; + pCount = 0; + + while (cur) { + // if the profile dir is relative to appdir... + bool isRelative; + rv = mAppData->Contains(cur->mRootDir, &isRelative); + if (NS_SUCCEEDED(rv) && isRelative) { + // we use a relative descriptor + rv = cur->mRootDir->GetRelativeDescriptor(mAppData, path); + } else { + // otherwise, a persistent descriptor + rv = cur->mRootDir->GetPersistentDescriptor(path); + NS_ENSURE_SUCCESS(rv, rv); + } + + pos += snprintf(pos, end - pos, + "[Profile%u]\n" + "Name=%s\n" + "IsRelative=%s\n" + "Path=%s\n", + pCount, cur->mName.get(), + isRelative ? "1" : "0", path.get()); + + nsCOMPtr<nsIToolkitProfile> profile; + rv = this->GetDefaultProfile(getter_AddRefs(profile)); + if (NS_SUCCEEDED(rv) && profile == cur) { + pos += snprintf(pos, end - pos, "Default=1\n"); + } + + pos += snprintf(pos, end - pos, "\n"); + + cur = cur->mNext; + ++pCount; + } + + FILE* writeFile; + rv = mListFile->OpenANSIFileDesc("w", &writeFile); + NS_ENSURE_SUCCESS(rv, rv); + + length = pos - buffer.get(); + + if (fwrite(buffer.get(), sizeof(char), length, writeFile) != length) { + fclose(writeFile); + return NS_ERROR_UNEXPECTED; + } + + fclose(writeFile); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsToolkitProfileFactory, nsIFactory) + +NS_IMETHODIMP +nsToolkitProfileFactory::CreateInstance(nsISupports* aOuter, const nsID& aIID, + void** aResult) +{ + if (aOuter) + return NS_ERROR_NO_AGGREGATION; + + nsCOMPtr<nsIToolkitProfileService> profileService = + nsToolkitProfileService::gService; + if (!profileService) { + nsresult rv = NS_NewToolkitProfileService(getter_AddRefs(profileService)); + if (NS_FAILED(rv)) + return rv; + } + return profileService->QueryInterface(aIID, aResult); +} + +NS_IMETHODIMP +nsToolkitProfileFactory::LockFactory(bool aVal) +{ + return NS_OK; +} + +nsresult +NS_NewToolkitProfileFactory(nsIFactory* *aResult) +{ + *aResult = new nsToolkitProfileFactory(); + if (!*aResult) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(*aResult); + return NS_OK; +} + +nsresult +NS_NewToolkitProfileService(nsIToolkitProfileService* *aResult) +{ + nsToolkitProfileService* profileService = new nsToolkitProfileService(); + if (!profileService) + return NS_ERROR_OUT_OF_MEMORY; + nsresult rv = profileService->Init(); + if (NS_FAILED(rv)) { + NS_ERROR("nsToolkitProfileService::Init failed!"); + delete profileService; + return rv; + } + + NS_ADDREF(*aResult = profileService); + return NS_OK; +} + +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(EmptyCString(), 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/test/.eslintrc.js b/toolkit/profile/test/.eslintrc.js new file mode 100644 index 000000000..4e6d4bcf0 --- /dev/null +++ b/toolkit/profile/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/profile/test/chrome.ini b/toolkit/profile/test/chrome.ini new file mode 100644 index 000000000..e21c1022e --- /dev/null +++ b/toolkit/profile/test/chrome.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_create_profile.xul] diff --git a/toolkit/profile/test/test_create_profile.xul b/toolkit/profile/test/test_create_profile.xul new file mode 100644 index 000000000..040b1256b --- /dev/null +++ b/toolkit/profile/test/test_create_profile.xul @@ -0,0 +1,134 @@ +<?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 type="application/javascript" + 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 Cc = Components.classes; + const Ci = Components.interfaces; + + const ASCIIName = "myprofile"; + const UnicodeName = "\u09A0\u09BE\u0995\u09C1\u09B0"; // A Bengali name + + var gDirService; + var gIOService; + var gProfileService; + + var gDefaultLocalProfileParent; + + gDirService = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + gIOService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + 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(Components.interfaces.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); + + // 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); +} + + ]]> + </script> +</window> |