chromium/chrome/browser/ash/app_list/search/essential_search/essential_search_manager.cc

// 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.

#include "chrome/browser/ash/app_list/search/essential_search/essential_search_manager.h"

#include "base/check_is_test.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/essential_search/socs_cookie_fetcher.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/session_manager_types.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_monster.h"
#include "net/cookies/cookie_options.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace app_list {
namespace {
constexpr base::TimeDelta kOneDay = base::Days(1);
const char kCookieName[] = "SOCS";

void LogStatus(SocsCookieFetcher::Status status) {
  base::UmaHistogramEnumeration("Ash.EssentialSearch.Status", status);
}

}  // namespace

const net::BackoffEntry::Policy
    EssentialSearchManager::kFetchSocsCookieRetryBackoffPolicy = {
        0,               // Number of initial errors to ignore.
        10 * 1000,       // Initial request delay in ms.
        2.0,             // Factor by which the waiting time will be multiplied.
        0.1,             // Fuzzing percentage.
        60 * 60 * 1000,  // Maximum request delay in ms.
        -1,              // Never discard the entry.
        true,            // Use initial delay in the first retry.
};

EssentialSearchManager::EssentialSearchManager(Profile* primary_profile)
    : primary_profile_(primary_profile),
      retry_backoff_(&kFetchSocsCookieRetryBackoffPolicy) {
  DCHECK(primary_profile_);
  auto* session_controller = ash::SessionController::Get();
  if (!session_controller) {
    CHECK_IS_TEST();
  } else {
    CHECK(session_controller);
    session_controller->AddObserver(this);
  }

  // Listen to pref changes.
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(primary_profile_->GetPrefs());
  pref_change_registrar_->Add(
      prefs::kEssentialSearchEnabled,
      base::BindRepeating(&EssentialSearchManager::MaybeFetchSocsCookie,
                          weak_ptr_factory_.GetWeakPtr()));

  // Handle the case where EssentialSearchManager is initialized after the
  // session was started.
  if (!session_manager::SessionManager::Get()) {
    CHECK_IS_TEST();
  } else if (session_manager::SessionManager::Get()->IsSessionStarted()) {
    MaybeFetchSocsCookie();
  }
}

EssentialSearchManager::~EssentialSearchManager() {
  auto* session_controller = ash::SessionController::Get();
  if (session_controller) {
    session_controller->RemoveObserver(this);
  }
}

// static
std::unique_ptr<EssentialSearchManager> EssentialSearchManager::Create(
    Profile* primary_profile) {
  return std::make_unique<EssentialSearchManager>(primary_profile);
}

void EssentialSearchManager::OnSessionStateChanged(
    session_manager::SessionState state) {
  // EssentialSearchManager only update SOCS cookie when user sign in/unlock the
  // device.
  if (state != session_manager::SessionState::ACTIVE) {
    return;
  }

  MaybeFetchSocsCookie();
}

void EssentialSearchManager::MaybeFetchSocsCookie() {
  // Check whether the feature flag is enabled.
  if (!chromeos::features::IsEssentialSearchEnabled()) {
    return;
  }

  PrefService* prefs = primary_profile_->GetPrefs();
  if (prefs->GetBoolean(prefs::kEssentialSearchEnabled)) {
    // If the policy is enabled, cancel all active requests then fetch SOCS
    // cookie.
    MaybeDisableSearchSuggest();
    CancelPendingRequests();
    socs_cookie_fetcher_ = std::make_unique<SocsCookieFetcher>(
        primary_profile_->GetURLLoaderFactory(), this);
    socs_cookie_fetcher_->StartFetching();
    return;
  }

  if (prefs->GetBoolean(prefs::kLastEssentialSearchValue)) {
    // If the policy was disabled, remove SOCS cookie from the user's profile.
    RemoveSocsCookie();
    return;
  }
}

void EssentialSearchManager::MaybeDisableSearchSuggest() {
  // Disable search suggest if last kEssentialSearchEnabled value was false
  // (until SOCS is fetched)
  if (!primary_profile_->GetPrefs()->GetBoolean(
          prefs::kLastEssentialSearchValue)) {
    temporary_disable_search_suggest_ = true;
    return;
  }

  // Check user profile for valid SOCS cookie
  primary_profile_->GetDefaultStoragePartition()
      ->GetCookieManagerForBrowserProcess()
      ->GetCookieList(
          GaiaUrls::GetInstance()->secure_google_url(),
          net::CookieOptions::MakeAllInclusive(),
          net::CookiePartitionKeyCollection(),
          base::BindOnce(&EssentialSearchManager::OnCookiesRetrieved,
                         weak_ptr_factory_.GetWeakPtr()));
}

void EssentialSearchManager::OnCookiesRetrieved(
    const net::CookieAccessResultList& list,
    const net::CookieAccessResultList& excluded_list) {
  // Assume disabled until valid SOCS found
  bool should_disable_search_suggest = true;
  for (const auto& cookie_with_access_result : list) {
    if (cookie_with_access_result.cookie.Name() == kCookieName) {
      should_disable_search_suggest = false;
      break;
    }
  }

  temporary_disable_search_suggest_ = should_disable_search_suggest;
}

bool EssentialSearchManager::ShouldDisableSearchSuggest() const {
  // TODO(b/312542928): collect UMA with the return type.
  return temporary_disable_search_suggest_;
}

void EssentialSearchManager::RemoveSocsCookie() {
  // Before removing the cookie, cancel all active request
  CancelPendingRequests();
  socs_cookie_fetcher_.reset();

  network::mojom::CookieDeletionFilterPtr filter =
      network::mojom::CookieDeletionFilter::New();

  filter->cookie_name = kCookieName;
  filter->url = GaiaUrls::GetInstance()->secure_google_url();

  primary_profile_->GetDefaultStoragePartition()
      ->GetCookieManagerForBrowserProcess()
      ->DeleteCookies(std::move(filter),
                      base::BindOnce(&EssentialSearchManager::OnCookieDeleted,
                                     weak_ptr_factory_.GetWeakPtr()));
}

void EssentialSearchManager::OnCookieDeleted(
    uint32_t number_of_cookies_deleted) {
  // Notify the test that OnCookieDeleted has been triggered.
  if (cookie_deletion_closure_for_test_) {
    CHECK_IS_TEST();
    std::move(cookie_deletion_closure_for_test_).Run();
  }

  if (number_of_cookies_deleted != 1) {
    LOG(WARNING) << "Failed to remove SOCS cookie";
  }

  // Update kLastEssentialSearchValue to avoid deleting SOCS cookie again.
  primary_profile_->GetPrefs()->SetBoolean(prefs::kLastEssentialSearchValue,
                                           false);
}

void EssentialSearchManager::OnCookieFetched(const std::string& cookie_header) {
  GURL google_url = GaiaUrls::GetInstance()->secure_google_url();

  std::unique_ptr<net::CanonicalCookie> cc(net::CanonicalCookie::Create(
      google_url, cookie_header, base::Time::Now(),
      std::nullopt /* server_time */, std::nullopt /* cookie_partition_key */,
      net::CookieSourceType::kOther,
      /*status=*/nullptr));

  if (!cc) {
    LOG(ERROR) << "Invalid cookie header";
    OnApiCallFailed(SocsCookieFetcher::Status::kInvalidCookie);
    return;
  }

  const net::CookieOptions options = net::CookieOptions::MakeAllInclusive();

  primary_profile_->GetDefaultStoragePartition()
      ->GetCookieManagerForBrowserProcess()
      ->SetCanonicalCookie(
          *cc, google_url, options,
          base::BindOnce(&EssentialSearchManager::OnCookieAddedToUserProfile,
                         weak_ptr_factory_.GetWeakPtr()));
}

void EssentialSearchManager::OnCookieAddedToUserProfile(
    net::CookieAccessResult result) {
  // Notify the test that OnCookieAddedToUserProfile has been triggered.
  if (cookie_insertion_closure_for_test_) {
    CHECK_IS_TEST();
    std::move(cookie_insertion_closure_for_test_).Run();
  }

  if (!result.status.IsInclude()) {
    // Handle SOCS cookie insertion failure.
    LOG(WARNING) << "Failed to add SOCS cookie to user profile. Retrying.";
    LOG(WARNING) << "Status=" << result.status;
    OnApiCallFailed(SocsCookieFetcher::Status::kCookieInsertionFailure);
    return;
  }

  // Log success of fetching the cookie and adding it to the user profile.
  LogStatus(SocsCookieFetcher::Status::kOk);

  // After the SOCS cookie is added to the user profile, Schedule a SOCS cookie
  // refresh to ensure a valid SOCS cookie is maintained.
  temporary_disable_search_suggest_ = false;
  RefetchAfter(kOneDay);
  retry_backoff_.InformOfRequest(true);
  PrefService* prefs = primary_profile_->GetPrefs();
  if (prefs->GetBoolean(prefs::kEssentialSearchEnabled)) {
    // If the kEssentialSearchEnabled policy is still enabled, mark
    // kLastEssentialSearchValue as enabled.
    prefs->SetBoolean(prefs::kLastEssentialSearchValue, true);
  } else {
    // This would be triggered in case the policy got disabled while storing the
    // cookie.
    RemoveSocsCookie();
  }
}

void EssentialSearchManager::RefetchAfter(base::TimeDelta delay) {
  CancelPendingRequests();

  // Create new request after given delay
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&EssentialSearchManager::MaybeFetchSocsCookie,
                     fetch_requests_weak_factory_.GetWeakPtr()),
      delay);
}

void EssentialSearchManager::CancelPendingRequests() {
  fetch_requests_weak_factory_.InvalidateWeakPtrs();
}

void EssentialSearchManager::OnApiCallFailed(SocsCookieFetcher::Status status) {
  LogStatus(status);
  retry_backoff_.InformOfRequest(false);
  RefetchAfter(retry_backoff_.GetTimeUntilRelease());
}

}  // namespace app_list