chromium/ios/chrome/browser/passwords/model/password_checkup_utils.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/passwords/model/password_checkup_utils.h"

#import "base/ranges/algorithm.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/time/time.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/l10n/time_format.h"

using password_manager::CredentialUIEntry;

namespace {

// Amount of time after which the timestamp is shown instead of "just now".
constexpr base::TimeDelta kJustCheckedTimeThreshold = base::Minutes(1);

}  // anonymous namespace

namespace password_manager {

bool operator==(const InsecurePasswordCounts& lhs,
                const InsecurePasswordCounts& rhs) {
  std::tuple lhs_tuple = std::tie(lhs.compromised_count, lhs.dismissed_count,
                                  lhs.reused_count, lhs.weak_count);
  std::tuple rhs_tuple = std::tie(rhs.compromised_count, rhs.dismissed_count,
                                  rhs.reused_count, rhs.weak_count);
  return lhs_tuple == rhs_tuple;
}

bool IsCredentialUnmutedCompromised(const CredentialUIEntry& credential) {
  return IsCompromised(credential) && !credential.IsMuted();
}

WarningType GetWarningOfHighestPriority(
    const std::vector<CredentialUIEntry>& insecure_credentials) {
  bool has_reused_passwords = false;
  bool has_weak_passwords = false;
  bool has_muted_warnings = false;

  for (const auto& credential : insecure_credentials) {
    if (credential.IsMuted()) {
      has_muted_warnings = true;
    } else if (IsCompromised(credential)) {
      return WarningType::kCompromisedPasswordsWarning;
    }

    // A reused password warning is of higher priority than a weak password
    // warning. So, if the credential is reused, there is no need to verify if
    // it is also weak.
    if (credential.IsReused()) {
      has_reused_passwords = true;
    } else if (credential.IsWeak()) {
      has_weak_passwords = true;
    }
  }

  if (has_reused_passwords) {
    return WarningType::kReusedPasswordsWarning;
  } else if (has_weak_passwords) {
    return WarningType::kWeakPasswordsWarning;
  } else if (has_muted_warnings) {
    return WarningType::kDismissedWarningsWarning;
  }

  return WarningType::kNoInsecurePasswordsWarning;
}

InsecurePasswordCounts CountInsecurePasswordsPerInsecureType(
    const std::vector<password_manager::CredentialUIEntry>&
        insecure_credentials) {
  InsecurePasswordCounts counts{};
  std::map<std::u16string, int> reused_passwords;
  for (const auto& credential : insecure_credentials) {
    // If a compromised credential is muted, we don't want to take it into
    // account in the compromised count.
    if (credential.IsMuted()) {
      counts.dismissed_count++;
    } else if (IsCompromised(credential)) {
      counts.compromised_count++;
    }
    if (credential.IsReused()) {
      counts.reused_count++;
    }
    if (credential.IsWeak()) {
      counts.weak_count++;
    }
  }

  return counts;
}

int GetPasswordCountForWarningType(
    WarningType warningType,
    const std::vector<password_manager::CredentialUIEntry>&
        insecure_credentials) {
  InsecurePasswordCounts counts =
      CountInsecurePasswordsPerInsecureType(insecure_credentials);
  switch (warningType) {
    case WarningType::kCompromisedPasswordsWarning:
      return counts.compromised_count;
    case WarningType::kReusedPasswordsWarning:
      return counts.reused_count;
    case WarningType::kWeakPasswordsWarning:
      return counts.weak_count;
    case WarningType::kDismissedWarningsWarning:
      return counts.dismissed_count;
    case WarningType::kNoInsecurePasswordsWarning:
      return 0;
  }
}

NSString* FormatElapsedTimeSinceLastCheck(
    std::optional<base::Time> last_completed_check,
    bool use_title_case) {
  if (!last_completed_check.has_value()) {
    // The title case format is only used in the Password Checkup Homepage as of
    // now and it is currently not possible to reach this page if no check has
    // yet been completed. There is therefore no need for now to have a title
    // case version of "Check never run"
    return l10n_util::GetNSString(IDS_IOS_CHECK_NEVER_RUN);
  }

  base::TimeDelta elapsed_time =
      base::Time::Now() - last_completed_check.value();

  // If check finished in less than `kJustCheckedTimeThreshold` show "Checked
  // just now" instead of timestamp.
  if (elapsed_time < kJustCheckedTimeThreshold) {
    return l10n_util::GetNSString(
        use_title_case ? IDS_IOS_CHECK_FINISHED_JUST_NOW_TITLE_CASE
                       : IDS_IOS_CHECK_FINISHED_JUST_NOW);
  }

  std::u16string timestamp = ui::TimeFormat::SimpleWithMonthAndYear(
      use_title_case ? ui::TimeFormat::FORMAT_TITLE_CASE_ELAPSED
                     : ui::TimeFormat::FORMAT_ELAPSED,
      ui::TimeFormat::LENGTH_LONG, elapsed_time, true);

  return l10n_util::GetNSStringF(IDS_IOS_PASSWORD_CHECKUP_LAST_COMPLETED_CHECK,
                                 timestamp);
}

std::vector<CredentialUIEntry> GetPasswordsForWarningType(
    WarningType warning_type,
    const std::vector<CredentialUIEntry>& insecure_credentials) {
  std::vector<CredentialUIEntry> filtered_credentials;

  switch (warning_type) {
    case WarningType::kCompromisedPasswordsWarning:
      base::ranges::copy_if(insecure_credentials,
                            std::back_inserter(filtered_credentials),
                            IsCredentialUnmutedCompromised);
      break;
    case WarningType::kWeakPasswordsWarning:
      base::ranges::copy_if(insecure_credentials,
                            std::back_inserter(filtered_credentials),
                            std::mem_fn(&CredentialUIEntry::IsWeak));
      break;
    case WarningType::kReusedPasswordsWarning:
      base::ranges::copy_if(insecure_credentials,
                            std::back_inserter(filtered_credentials),
                            std::mem_fn(&CredentialUIEntry::IsReused));
      break;
    case WarningType::kDismissedWarningsWarning:
      base::ranges::copy_if(insecure_credentials,
                            std::back_inserter(filtered_credentials),
                            std::mem_fn(&CredentialUIEntry::IsMuted));
      break;
    case WarningType::kNoInsecurePasswordsWarning:
      NOTREACHED();
  }

  return filtered_credentials;
}

}  // namespace password_manager