Commit 62c09bbe authored by Kathleen Brade's avatar Kathleen Brade Committed by Richard Pospesel
Browse files

Bug 14631: Improve profile access error messages.

Instead of always reporting that the profile is locked, display specific
messages for "access denied" and "read-only file system".

To allow for localization, get profile-related error strings from Torbutton.
Use app display name ("Tor Browser") in profile-related error alerts.
parent f1676471
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -12,6 +12,11 @@ restartMessageUnlocker=%S is already running, but is not responding. The old %S
restartMessageNoUnlockerMac=A copy of %S is already open. Only one copy of %S can be open at a time.
restartMessageUnlockerMac=A copy of %S is already open. The running copy of %S will quit in order to open this one.

# LOCALIZATION NOTE (profileProblemTitle, profileReadOnly, profileReadOnlyMac, profileAccessDenied):  Messages displayed when the browser profile cannot be accessed or written to. %S is the application name.
profileProblemTitle=%S Profile Problem
profileReadOnly=You cannot run %S from a read-only file system.  Please copy %S to another location before trying to use it.
profileReadOnlyMac=You cannot run %S from a read-only file system.  Please copy %S to your Desktop or Applications folder before trying to use it.
profileAccessDenied=%S does not have permission to access the profile. Please adjust your file system permissions and try again.
# Profile manager
# LOCALIZATION NOTE (profileTooltip): First %S is the profile name, second %S is the path to the profile folder.
profileTooltip=Profile: ‘%S’ — Path: ‘%S’
+55 −2
Original line number Diff line number Diff line
@@ -1251,9 +1251,10 @@ nsToolkitProfileService::SelectStartupProfile(
  }

  bool wasDefault;
  ProfileStatus profileStatus;
  nsresult rv =
      SelectStartupProfile(&argc, argv.get(), aIsResetting, aRootDir, aLocalDir,
                           aProfile, aDidCreate, &wasDefault);
                           aProfile, aDidCreate, &wasDefault, profileStatus);

  // Since we were called outside of the normal startup path complete any
  // startup tasks.
@@ -1286,7 +1287,8 @@ nsToolkitProfileService::SelectStartupProfile(
nsresult nsToolkitProfileService::SelectStartupProfile(
    int* aArgc, char* aArgv[], bool aIsResetting, nsIFile** aRootDir,
    nsIFile** aLocalDir, nsIToolkitProfile** aProfile, bool* aDidCreate,
    bool* aWasDefaultSelection) {
    bool* aWasDefaultSelection, ProfileStatus& aProfileStatus) {
  aProfileStatus = PROFILE_STATUS_OK;
  if (mStartupProfileSelected) {
    return NS_ERROR_ALREADY_INITIALIZED;
  }
@@ -1379,6 +1381,13 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
    rv = XRE_GetFileFromPath(arg, getter_AddRefs(lf));
    NS_ENSURE_SUCCESS(rv, rv);

    aProfileStatus = CheckProfileWriteAccess(lf);
    if (PROFILE_STATUS_OK != aProfileStatus) {
      NS_ADDREF(*aRootDir = lf);
      NS_ADDREF(*aLocalDir = lf);
      return NS_ERROR_FAILURE;
    }

    // Make sure that the profile path exists and it's a directory.
    bool exists;
    rv = lf->Exists(&exists);
@@ -2145,3 +2154,47 @@ nsresult XRE_GetFileFromPath(const char* aPath, nsIFile** aResult) {
#  error Platform-specific logic needed here.
#endif
}

// Check for write permission to the profile directory by trying to create a
// new file (after ensuring that no file with the same name exists).
ProfileStatus nsToolkitProfileService::CheckProfileWriteAccess(
    nsIFile* aProfileDir) {
#if defined(XP_UNIX)
  constexpr auto writeTestFileName = u".parentwritetest"_ns;
#else
  constexpr auto writeTestFileName = u"parent.writetest"_ns;
#endif

  nsCOMPtr<nsIFile> writeTestFile;
  nsresult rv = aProfileDir->Clone(getter_AddRefs(writeTestFile));
  if (NS_SUCCEEDED(rv)) rv = writeTestFile->Append(writeTestFileName);

  if (NS_SUCCEEDED(rv)) {
    bool doesExist = false;
    rv = writeTestFile->Exists(&doesExist);
    if (NS_SUCCEEDED(rv) && doesExist) rv = writeTestFile->Remove(true);
  }

  if (NS_SUCCEEDED(rv)) {
    rv = writeTestFile->Create(nsIFile::NORMAL_FILE_TYPE, 0666);
    (void)writeTestFile->Remove(true);
  }

  ProfileStatus status =
      NS_SUCCEEDED(rv) ? PROFILE_STATUS_OK : PROFILE_STATUS_OTHER_ERROR;
  if (NS_ERROR_FILE_ACCESS_DENIED == rv)
    status = PROFILE_STATUS_ACCESS_DENIED;
  else if (NS_ERROR_FILE_READ_ONLY == rv)
    status = PROFILE_STATUS_READ_ONLY;

  return status;
}

ProfileStatus nsToolkitProfileService::CheckProfileWriteAccess(
    nsIToolkitProfile* aProfile) {
  nsCOMPtr<nsIFile> profileDir;
  nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
  if (NS_FAILED(rv)) return PROFILE_STATUS_OTHER_ERROR;

  return CheckProfileWriteAccess(profileDir);
}
+12 −1
Original line number Diff line number Diff line
@@ -16,6 +16,14 @@
#include "nsProfileLock.h"
#include "nsINIParser.h"

enum ProfileStatus {
  PROFILE_STATUS_OK,
  PROFILE_STATUS_ACCESS_DENIED,
  PROFILE_STATUS_READ_ONLY,
  PROFILE_STATUS_IS_LOCKED,
  PROFILE_STATUS_OTHER_ERROR
};

class nsToolkitProfile final
    : public nsIToolkitProfile,
      public mozilla::LinkedListElement<RefPtr<nsToolkitProfile>> {
@@ -80,10 +88,13 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
  nsresult SelectStartupProfile(int* aArgc, char* aArgv[], bool aIsResetting,
                                nsIFile** aRootDir, nsIFile** aLocalDir,
                                nsIToolkitProfile** aProfile, bool* aDidCreate,
                                bool* aWasDefaultSelection);
                                bool* aWasDefaultSelection,
                                ProfileStatus& aProfileStatus);
  nsresult CreateResetProfile(nsIToolkitProfile** aNewProfile);
  nsresult ApplyResetProfile(nsIToolkitProfile* aOldProfile);
  void CompleteStartup();
  static ProfileStatus CheckProfileWriteAccess(nsIToolkitProfile* aProfile);
  static ProfileStatus CheckProfileWriteAccess(nsIFile* aProfileDir);

 private:
  friend class nsToolkitProfile;
+136 −21
Original line number Diff line number Diff line
@@ -2683,6 +2683,91 @@ nsresult LaunchChild(bool aBlankCommandLine, bool aTryExec) {
  return NS_ERROR_LAUNCHED_CHILD_PROCESS;
}

static nsresult GetOverrideStringBundleForLocale(nsIStringBundleService* aSBS,
                                                 const char* aTorbuttonURI,
                                                 const char* aLocale,
                                                 nsIStringBundle** aResult) {
  NS_ENSURE_ARG(aSBS);
  NS_ENSURE_ARG(aTorbuttonURI);
  NS_ENSURE_ARG(aLocale);
  NS_ENSURE_ARG(aResult);

  const char* kFormatStr =
      "jar:%s!/chrome/torbutton/locale/%s/torbutton.properties";
  nsPrintfCString strBundleURL(kFormatStr, aTorbuttonURI, aLocale);
  nsresult rv = aSBS->CreateBundle(strBundleURL.get(), aResult);
  NS_ENSURE_SUCCESS(rv, rv);

  // To ensure that we have a valid string bundle, try to retrieve a string
  // that we know exists.
  nsAutoString val;
  rv = (*aResult)->GetStringFromName("profileProblemTitle", val);
  if (!NS_SUCCEEDED(rv)) *aResult = nullptr;  // No good.  Discard it.

  return rv;
}

static void GetOverrideStringBundle(nsIStringBundleService* aSBS,
                                    nsIStringBundle** aResult) {
  if (!aSBS || !aResult) return;

  *aResult = nullptr;

  // Build Torbutton file URI string by starting from GREDir.
  RefPtr<nsXREDirProvider> dirProvider = nsXREDirProvider::GetSingleton();
  if (!dirProvider) return;

  nsCOMPtr<nsIFile> greDir = dirProvider->GetGREDir();
  if (!greDir) return;

  // Create file URI, extract as string, and append omni.ja relative path.
  nsCOMPtr<nsIURI> uri;
  nsAutoCString uriString;
  if (NS_FAILED(NS_NewFileURI(getter_AddRefs(uri), greDir)) ||
      NS_FAILED(uri->GetSpec(uriString))) {
    return;
  }

  uriString.Append("omni.ja");

  nsAutoCString userAgentLocale;
  if (!NS_SUCCEEDED(
          Preferences::GetCString("intl.locale.requested", userAgentLocale))) {
    return;
  }

  nsresult rv = GetOverrideStringBundleForLocale(
      aSBS, uriString.get(), userAgentLocale.get(), aResult);
  if (NS_FAILED(rv)) {
    // Try again using base locale, e.g., "en" vs. "en-US".
    int16_t offset = userAgentLocale.FindChar('-', 1);
    if (offset > 0) {
      nsAutoCString shortLocale(Substring(userAgentLocale, 0, offset));
      rv = GetOverrideStringBundleForLocale(aSBS, uriString.get(),
                                            shortLocale.get(), aResult);
    }
  }
}

static nsresult GetFormattedString(nsIStringBundle* aOverrideBundle,
                                   nsIStringBundle* aMainBundle,
                                   const char* aName,
                                   const nsTArray<nsString>& aParams,
                                   nsAString& aResult) {
  NS_ENSURE_ARG(aName);

  nsresult rv = NS_ERROR_FAILURE;
  if (aOverrideBundle) {
    rv = aOverrideBundle->FormatStringFromName(aName, aParams, aResult);
  }

  // If string was not found in override bundle, use main (browser) bundle.
  if (NS_FAILED(rv) && aMainBundle)
    rv = aMainBundle->FormatStringFromName(aName, aParams, aResult);

  return rv;
}

static const char kProfileProperties[] =
    "chrome://mozapps/locale/profile/profileSelection.properties";

@@ -2756,7 +2841,7 @@ static nsresult ProfileMissingDialog(nsINativeAppSupport* aNative) {
    sbs->CreateBundle(kProfileProperties, getter_AddRefs(sb));
    NS_ENSURE_TRUE_LOG(sbs, NS_ERROR_FAILURE);

    NS_ConvertUTF8toUTF16 appName(gAppData->name);
    NS_ConvertUTF8toUTF16 appName(MOZ_APP_DISPLAYNAME);
    AutoTArray<nsString, 2> params = {appName, appName};

    // profileMissing
@@ -2781,8 +2866,9 @@ static nsresult ProfileMissingDialog(nsINativeAppSupport* aNative) {

// If aUnlocker is NULL, it is also OK for the following arguments to be NULL:
//   aProfileDir, aProfileLocalDir, aResult.
static ReturnAbortOnError ProfileLockedDialog(nsIFile* aProfileDir,
static ReturnAbortOnError ProfileErrorDialog(nsIFile* aProfileDir,
                                             nsIFile* aProfileLocalDir,
                                             ProfileStatus aStatus,
                                             nsIProfileUnlocker* aUnlocker,
                                             nsINativeAppSupport* aNative,
                                             nsIProfileLock** aResult) {
@@ -2815,24 +2901,39 @@ static ReturnAbortOnError ProfileLockedDialog(nsIFile* aProfileDir,
    sbs->CreateBundle(kProfileProperties, getter_AddRefs(sb));
    NS_ENSURE_TRUE_LOG(sbs, NS_ERROR_FAILURE);

    NS_ConvertUTF8toUTF16 appName(gAppData->name);
    nsCOMPtr<nsIStringBundle> overrideSB;
    GetOverrideStringBundle(sbs, getter_AddRefs(overrideSB));

    NS_ConvertUTF8toUTF16 appName(MOZ_APP_DISPLAYNAME);
    AutoTArray<nsString, 3> params = {appName, appName, appName};

    nsAutoString killMessage;
#ifndef XP_MACOSX
    rv = sb->FormatStringFromName(
        aUnlocker ? "restartMessageUnlocker" : "restartMessageNoUnlocker2",
        params, killMessage);
    static const char kRestartUnlocker[] = "restartMessageUnlocker";
    static const char kRestartNoUnlocker[] = "restartMessageNoUnlocker2";
    static const char kReadOnly[] = "profileReadOnly";
#else
    rv = sb->FormatStringFromName(
        aUnlocker ? "restartMessageUnlockerMac" : "restartMessageNoUnlockerMac",
        params, killMessage);
#endif
    static const char kRestartUnlocker[] = "restartMessageUnlockerMac";
    static const char kRestartNoUnlocker[] = "restartMessageNoUnlockerMac";
    static const char kReadOnly[] = "profileReadOnlyMac";
#endif
    static const char kAccessDenied[] = "profileAccessDenied";

    const char* errorKey = aUnlocker ? kRestartUnlocker : kRestartNoUnlocker;
    if (PROFILE_STATUS_READ_ONLY == aStatus)
      errorKey = kReadOnly;
    else if (PROFILE_STATUS_ACCESS_DENIED == aStatus)
      errorKey = kAccessDenied;
    rv = GetFormattedString(overrideSB, sb, errorKey, params, killMessage);
    NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);

    const char* titleKey = ((PROFILE_STATUS_READ_ONLY == aStatus) ||
                            (PROFILE_STATUS_ACCESS_DENIED == aStatus))
                               ? "profileProblemTitle"
                               : "restartTitle";
    params.SetLength(1);
    nsAutoString killTitle;
    rv = sb->FormatStringFromName("restartTitle", params, killTitle);
    rv = sb->FormatStringFromName(titleKey, params, killTitle);
    NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);

#ifdef MOZ_BACKGROUNDTASKS
@@ -3018,6 +3119,13 @@ static nsCOMPtr<nsIToolkitProfile> gResetOldProfile;
static nsresult LockProfile(nsINativeAppSupport* aNative, nsIFile* aRootDir,
                            nsIFile* aLocalDir, nsIToolkitProfile* aProfile,
                            nsIProfileLock** aResult) {
  ProfileStatus status =
      (aProfile ? nsToolkitProfileService::CheckProfileWriteAccess(aProfile)
                : nsToolkitProfileService::CheckProfileWriteAccess(aRootDir));
  if (PROFILE_STATUS_OK != status)
    return ProfileErrorDialog(aRootDir, aLocalDir, status, nullptr, aNative,
                              aResult);

  // If you close Firefox and very quickly reopen it, the old Firefox may
  // still be closing down. Rather than immediately showing the
  // "Firefox is running but is not responding" message, we spend a few
@@ -3044,7 +3152,8 @@ static nsresult LockProfile(nsINativeAppSupport* aNative, nsIFile* aRootDir,
  } while (TimeStamp::Now() - start <
           TimeDuration::FromSeconds(kLockRetrySeconds));

  return ProfileLockedDialog(aRootDir, aLocalDir, unlocker, aNative, aResult);
  return ProfileErrorDialog(aRootDir, aLocalDir, PROFILE_STATUS_IS_LOCKED,
                            unlocker, aNative, aResult);
}

// Pick a profile. We need to end up with a profile root dir, local dir and
@@ -3059,7 +3168,8 @@ static nsresult LockProfile(nsINativeAppSupport* aNative, nsIFile* aRootDir,
static nsresult SelectProfile(nsToolkitProfileService* aProfileSvc,
                              nsINativeAppSupport* aNative, nsIFile** aRootDir,
                              nsIFile** aLocalDir, nsIToolkitProfile** aProfile,
                              bool* aWasDefaultSelection) {
                              bool* aWasDefaultSelection,
                              nsIProfileLock** aResult) {
  StartupTimeline::Record(StartupTimeline::SELECT_PROFILE);

  nsresult rv;
@@ -3105,9 +3215,14 @@ static nsresult SelectProfile(nsToolkitProfileService* aProfileSvc,

  // Ask the profile manager to select the profile directories to use.
  bool didCreate = false;
  rv = aProfileSvc->SelectStartupProfile(&gArgc, gArgv, gDoProfileReset,
                                         aRootDir, aLocalDir, aProfile,
                                         &didCreate, aWasDefaultSelection);
  ProfileStatus profileStatus = PROFILE_STATUS_OK;
  rv = aProfileSvc->SelectStartupProfile(
      &gArgc, gArgv, gDoProfileReset, aRootDir, aLocalDir, aProfile, &didCreate,
      aWasDefaultSelection, profileStatus);
  if (PROFILE_STATUS_OK != profileStatus) {
    return ProfileErrorDialog(*aRootDir, *aLocalDir, profileStatus, nullptr,
                              aNative, aResult);
  }

  if (rv == NS_ERROR_SHOW_PROFILE_MANAGER) {
    return ShowProfileManager(aProfileSvc, aNative);
@@ -5004,7 +5119,7 @@ int XREMain::XRE_mainStartup(bool* aExitFlag) {
  nsCOMPtr<nsIToolkitProfile> profile;
  rv = SelectProfile(mProfileSvc, mNativeApp, getter_AddRefs(mProfD),
                     getter_AddRefs(mProfLD), getter_AddRefs(profile),
                     &wasDefaultSelection);
                     &wasDefaultSelection, getter_AddRefs(mProfileLock));
  if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) {
    *aExitFlag = true;
    return 0;