| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/affiliations/core/browser/affiliation_backend.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/barrier_closure.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/clock.h" |
| #include "base/time/tick_clock.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/affiliations/core/browser/affiliation_database.h" |
| #include "components/affiliations/core/browser/affiliation_fetch_throttler.h" |
| #include "components/affiliations/core/browser/affiliation_fetcher_factory_impl.h" |
| #include "components/affiliations/core/browser/affiliation_fetcher_interface.h" |
| #include "components/affiliations/core/browser/affiliation_utils.h" |
| #include "components/affiliations/core/browser/facet_manager.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| namespace affiliations { |
| |
| BASE_FEATURE(kFetchChangePasswordUrl, |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS) |
| // Change-password urls aren't utilized in any way on mobile. No |
| // need to fetch them. |
| base::FEATURE_DISABLED_BY_DEFAULT); |
| #else |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| #endif |
| |
| AffiliationBackend::AffiliationBackend( |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner, |
| base::Clock* time_source, |
| const base::TickClock* time_tick_source) |
| : task_runner_(task_runner), |
| clock_(time_source), |
| tick_clock_(time_tick_source), |
| construction_time_(clock_->Now()) { |
| DCHECK_LT(base::Time(), clock_->Now()); |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| AffiliationBackend::~AffiliationBackend() = default; |
| |
| void AffiliationBackend::Initialize( |
| std::unique_ptr<network::PendingSharedURLLoaderFactory> |
| pending_url_loader_factory, |
| network::NetworkConnectionTracker* network_connection_tracker, |
| const base::FilePath& db_path) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::Initialize"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!throttler_); |
| throttler_ = std::make_unique<AffiliationFetchThrottler>( |
| this, task_runner_, network_connection_tracker, tick_clock_); |
| |
| // TODO(engedy): Currently, when Init() returns false, it always poisons the |
| // DB, so subsequent operations will silently fail. Consider either fully |
| // committing to this approach and making Init() a void, or handling the |
| // return value here. See: https://crbug.com/478831. |
| cache_ = std::make_unique<AffiliationDatabase>(); |
| cache_->Init(db_path); |
| DCHECK(pending_url_loader_factory); |
| DCHECK(!url_loader_factory_); |
| url_loader_factory_ = network::SharedURLLoaderFactory::Create( |
| std::move(pending_url_loader_factory)); |
| fetcher_manager_ = std::make_unique<AffiliationFetcherManager>( |
| std::move(url_loader_factory_)); |
| } |
| |
| void AffiliationBackend::GetAffiliationsAndBranding( |
| const FacetURI& facet_uri, |
| AffiliationService::ResultCallback callback, |
| const scoped_refptr<base::TaskRunner>& callback_task_runner) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::GetAffiliationsAndBranding"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| FacetManager* facet_manager = GetOrCreateFacetManager(facet_uri); |
| DCHECK(facet_manager); |
| facet_manager->GetAffiliationsAndBranding(std::move(callback), |
| callback_task_runner); |
| |
| if (facet_manager->CanBeDiscarded()) |
| facet_managers_.erase(facet_uri); |
| } |
| |
| void AffiliationBackend::Prefetch(const FacetURI& facet_uri, |
| const base::Time& keep_fresh_until) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::Prefetch"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| FacetManager* facet_manager = GetOrCreateFacetManager(facet_uri); |
| DCHECK(facet_manager); |
| facet_manager->Prefetch(keep_fresh_until); |
| |
| if (facet_manager->CanBeDiscarded()) |
| facet_managers_.erase(facet_uri); |
| } |
| |
| void AffiliationBackend::CancelPrefetch(const FacetURI& facet_uri, |
| const base::Time& keep_fresh_until) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::CancelPrefetch"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto facet_manager_it = facet_managers_.find(facet_uri); |
| if (facet_manager_it == facet_managers_.end()) |
| return; |
| facet_manager_it->second->CancelPrefetch(keep_fresh_until); |
| |
| if (facet_manager_it->second->CanBeDiscarded()) { |
| facet_managers_.erase(facet_uri); |
| TrimCacheForFacetURI(facet_uri); |
| } |
| } |
| |
| void AffiliationBackend::KeepPrefetchForFacets( |
| std::vector<FacetURI> facet_uris) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::KeepPrefetchForFacets"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Firstly, check which facets are missing from the |facet_managers_| and |
| // schedule Prefetch() for them. |
| for (const auto& facet : facet_uris) { |
| if (facet_managers_.find(facet) == facet_managers_.end()) { |
| Prefetch(facet, base::Time::Max()); |
| } |
| } |
| |
| // Now remove facets which which aren't retained anymore. |
| auto retained_facets = base::MakeFlatSet<std::string>( |
| facet_uris, {}, &FacetURI::potentially_invalid_spec); |
| |
| std::vector<FacetURI> facets_to_remove; |
| for (const auto& facet_manager_pair : facet_managers_) { |
| if (retained_facets.contains( |
| facet_manager_pair.first.potentially_invalid_spec())) { |
| continue; |
| } |
| |
| facet_manager_pair.second->CancelPrefetch(base::Time::Max()); |
| |
| if (facet_manager_pair.second->CanBeDiscarded()) |
| facets_to_remove.push_back(facet_manager_pair.first); |
| } |
| |
| for (const auto& facet : facets_to_remove) { |
| facet_managers_.erase(facet); |
| TrimCacheForFacetURI(facet); |
| } |
| } |
| |
| void AffiliationBackend::TrimCacheForFacetURI(const FacetURI& facet_uri) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::TrimCacheForFacetURI"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| AffiliatedFacetsWithUpdateTime affiliation; |
| if (cache_->GetAffiliationsAndBrandingForFacetURI(facet_uri, &affiliation)) |
| DiscardCachedDataIfNoLongerNeeded(affiliation.facets); |
| } |
| |
| void AffiliationBackend::TrimUnusedCache(std::vector<FacetURI> facet_uris) { |
| TRACE_EVENT0("passwords", "AffiliationBackend::TrimUnusedCache"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| cache_->RemoveMissingFacetURI(facet_uris); |
| } |
| |
| std::vector<GroupedFacets> AffiliationBackend::GetGroupingInfo( |
| std::vector<FacetURI> facet_uris) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::unordered_set<std::string> facets_in_response; |
| std::vector<GroupedFacets> result; |
| |
| for (FacetURI& facet : facet_uris) { |
| // If facet is already in response, nothing else to do. |
| if (facets_in_response.contains(facet.potentially_invalid_spec())) { |
| continue; |
| } |
| result.push_back(cache_->GetGroup(facet)); |
| for (const auto& response : result.back().facets) { |
| facets_in_response.insert(response.uri.potentially_invalid_spec()); |
| } |
| } |
| return result; |
| } |
| |
| std::vector<std::string> AffiliationBackend::GetPSLExtensions() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return cache_->GetPSLExtensions(); |
| } |
| |
| void AffiliationBackend::UpdateAffiliationsAndBranding( |
| const std::vector<FacetURI>& facets, |
| base::OnceClosure consumer_closure) { |
| TRACE_EVENT0("passwords", |
| "AffiliationBackend::UpdateAffiliationsAndBranding"); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If there is no internet connection we can't do anything. Fail immediately. |
| if (!throttler_->HasInternetConnection()) { |
| std::move(consumer_closure).Run(); |
| return; |
| } |
| |
| std::vector<FacetURI> valid_facets; |
| std::copy_if(facets.begin(), facets.end(), std::back_inserter(valid_facets), |
| [](const FacetURI& facet) { return facet.is_valid(); }); |
| // Return early if there are no facets to request. |
| if (valid_facets.empty()) { |
| std::move(consumer_closure).Run(); |
| return; |
| } |
| auto result_callback = |
| base::BindOnce( |
| [](base::WeakPtr<AffiliationBackend> weak_self, |
| AffiliationFetcherInterface::FetchResult fetch_result) { |
| if (weak_self && fetch_result.IsSuccessful()) { |
| weak_self->ProcessSuccessfulFetch( |
| std::move(fetch_result.data.value())); |
| } |
| }, |
| weak_ptr_factory_.GetWeakPtr()) |
| .Then(std::move(consumer_closure)); |
| fetcher_manager_->Fetch(valid_facets, |
| {.branding_info = true, .psl_extension_list = true}, |
| std::move(result_callback)); |
| } |
| |
| // static |
| void AffiliationBackend::DeleteCache(const base::FilePath& db_path) { |
| AffiliationDatabase::Delete(db_path); |
| } |
| |
| FacetManager* AffiliationBackend::GetOrCreateFacetManager( |
| const FacetURI& facet_uri) { |
| std::unique_ptr<FacetManager>& facet_manager = facet_managers_[facet_uri]; |
| if (!facet_manager) { |
| facet_manager = std::make_unique<FacetManager>(facet_uri, this, clock_); |
| } |
| return facet_manager.get(); |
| } |
| |
| void AffiliationBackend::DiscardCachedDataIfNoLongerNeeded( |
| const AffiliatedFacets& affiliated_facets) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Discard the equivalence class if there is no facet in the class whose |
| // FacetManager claims that it needs to keep the data. |
| for (const auto& facet : affiliated_facets) { |
| auto facet_manager_it = facet_managers_.find(facet.uri); |
| if (facet_manager_it != facet_managers_.end() && |
| !facet_manager_it->second->CanCachedDataBeDiscarded()) { |
| return; |
| } |
| } |
| |
| CHECK(!affiliated_facets.empty()); |
| cache_->DeleteAffiliationsAndBrandingForFacetURI(affiliated_facets[0].uri); |
| } |
| |
| void AffiliationBackend::OnSendNotification(const FacetURI& facet_uri) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto facet_manager_it = facet_managers_.find(facet_uri); |
| if (facet_manager_it == facet_managers_.end()) |
| return; |
| facet_manager_it->second->NotifyAtRequestedTime(); |
| |
| if (facet_manager_it->second->CanBeDiscarded()) |
| facet_managers_.erase(facet_uri); |
| } |
| |
| bool AffiliationBackend::ReadAffiliationsAndBrandingFromDatabase( |
| const FacetURI& facet_uri, |
| AffiliatedFacetsWithUpdateTime* affiliations) { |
| return cache_->GetAffiliationsAndBrandingForFacetURI(facet_uri, affiliations); |
| } |
| |
| void AffiliationBackend::SignalNeedNetworkRequest() { |
| throttler_->SignalNetworkRequestNeeded(); |
| } |
| |
| void AffiliationBackend::RequestNotificationAtTime(const FacetURI& facet_uri, |
| base::Time time) { |
| // TODO(engedy): Avoid spamming the task runner; only ever schedule the first |
| // callback. crbug.com/437865. |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AffiliationBackend::OnSendNotification, |
| weak_ptr_factory_.GetWeakPtr(), facet_uri), |
| time - clock_->Now()); |
| } |
| |
| void AffiliationBackend::ProcessSuccessfulFetch( |
| AffiliationFetcherInterface::ParsedFetchResponse result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!result.psl_extensions.empty()) { |
| cache_->UpdatePslExtensions(result.psl_extensions); |
| } |
| |
| auto psl_extensions = base::MakeFlatSet<std::string>(result.psl_extensions); |
| result.groupings = MergeRelatedGroups(psl_extensions, result.groupings); |
| std::map<std::string, const GroupedFacets*> map_facet_to_group; |
| for (const GroupedFacets& grouped_facets : result.groupings) { |
| for (const Facet& facet : grouped_facets.facets) { |
| map_facet_to_group[facet.uri.canonical_spec()] = &grouped_facets; |
| } |
| } |
| |
| for (const AffiliatedFacets& affiliated_facets : result.affiliations) { |
| AffiliatedFacetsWithUpdateTime affiliation; |
| affiliation.facets = affiliated_facets; |
| affiliation.last_update_time = clock_->Now(); |
| std::vector<AffiliatedFacetsWithUpdateTime> obsoleted_affiliations; |
| GroupedFacets group; |
| if (map_facet_to_group.count(affiliated_facets[0].uri.canonical_spec())) { |
| // Affiliations are subset of group. So |map_facet_to_group| must hold a |
| // vector to the whole group. |
| group = *map_facet_to_group[affiliated_facets[0].uri.canonical_spec()]; |
| } |
| cache_->StoreAndRemoveConflicting(affiliation, group, |
| &obsoleted_affiliations); |
| |
| // Cached data in contradiction with newly stored data automatically gets |
| // removed from the DB, and will be stored into |obsoleted_affiliations|. |
| // TODO(engedy): Currently, handling this is entirely meaningless unless in |
| // the edge case when the user has credentials for two Android applications |
| // which are now being de-associated. But even in that case, nothing will |
| // explode and the only symptom will be that credentials for the Android |
| // application that is not being fetched right now, if any, will not be |
| // filled into affiliated applications until the next fetch. Still, this |
| // should be implemented at some point by letting facet managers know if |
| // data. See: https://crbug.com/478832. |
| |
| for (const auto& facet : affiliated_facets) { |
| auto facet_manager_it = facet_managers_.find(facet.uri); |
| if (facet_manager_it == facet_managers_.end()) |
| continue; |
| FacetManager* facet_manager = facet_manager_it->second.get(); |
| facet_manager->UpdateLastFetchTime(affiliation.last_update_time); |
| if (facet_manager->CanBeDiscarded()) |
| facet_managers_.erase(facet.uri); |
| } |
| } |
| |
| // A subsequent fetch may be needed if any additional |
| // GetAffiliationsAndBranding() requests came in while the current fetch was |
| // in flight. |
| for (const auto& facet_manager_pair : facet_managers_) { |
| if (facet_manager_pair.second->DoesRequireFetch()) { |
| throttler_->SignalNetworkRequestNeeded(); |
| return; |
| } |
| } |
| } |
| |
| void AffiliationBackend::RetryRequestIfNeeded() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Trigger a retry if a fetch is still needed. |
| for (const auto& facet_manager_pair : facet_managers_) { |
| if (facet_manager_pair.second->DoesRequireFetch()) { |
| throttler_->SignalNetworkRequestNeeded(); |
| return; |
| } |
| } |
| } |
| |
| void AffiliationBackend::OnFetchFinished( |
| AffiliationFetcherInterface::FetchResult fetch_result) { |
| throttler_->InformOfNetworkRequestComplete(fetch_result.IsSuccessful(), |
| fetch_result.http_status_code); |
| if (!fetch_result.IsSuccessful()) { |
| RetryRequestIfNeeded(); |
| return; |
| } |
| ProcessSuccessfulFetch(std::move(fetch_result.data.value())); |
| } |
| |
| bool AffiliationBackend::OnCanSendNetworkRequest() { |
| std::vector<FacetURI> requested_facet_uris; |
| for (const auto& facet_manager_pair : facet_managers_) { |
| if (facet_manager_pair.second->DoesRequireFetch()) |
| requested_facet_uris.push_back(facet_manager_pair.first); |
| } |
| |
| // In case a request is no longer needed, return false to indicate this. |
| if (requested_facet_uris.empty()) |
| return false; |
| |
| if (!fetcher_manager_->IsFetchPossible()) { |
| return false; |
| } |
| |
| // TODO(crbug.com/40858918): There is no need to request psl extension every |
| // time, find a better way of caching it. |
| ReportStatistics(requested_facet_uris.size()); |
| fetcher_manager_->Fetch(requested_facet_uris, |
| {.branding_info = true, |
| .change_password_info = base::FeatureList::IsEnabled( |
| kFetchChangePasswordUrl), |
| .psl_extension_list = true}, |
| base::BindOnce(&AffiliationBackend::OnFetchFinished, |
| weak_ptr_factory_.GetWeakPtr())); |
| return true; |
| } |
| |
| void AffiliationBackend::ReportStatistics(size_t requested_facet_uri_count) { |
| UMA_HISTOGRAM_COUNTS_100("PasswordManager.AffiliationBackend.FetchSize", |
| requested_facet_uri_count); |
| |
| if (last_request_time_.is_null()) { |
| base::TimeDelta delay = clock_->Now() - construction_time_; |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "PasswordManager.AffiliationBackend.FirstFetchDelay", delay, |
| base::Seconds(1), base::Days(3), 50); |
| } else { |
| base::TimeDelta delay = clock_->Now() - last_request_time_; |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "PasswordManager.AffiliationBackend.SubsequentFetchDelay", delay, |
| base::Seconds(1), base::Days(3), 50); |
| } |
| last_request_time_ = clock_->Now(); |
| } |
| |
| } // namespace affiliations |