blob: 21fd736054ac7b23f6fb519db209d7f04d160eb1 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/previews/content/previews_decider_impl.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/location.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/clock.h"
#include "components/blacklist/opt_out_blacklist/opt_out_store.h"
#include "components/optimization_guide/optimization_guide_features.h"
#include "components/previews/content/previews_ui_service.h"
#include "components/previews/content/previews_user_data.h"
#include "components/previews/core/previews_experiments.h"
#include "components/previews/core/previews_features.h"
#include "components/previews/core/previews_switches.h"
#include "content/public/browser/navigation_handle.h"
#include "net/nqe/network_quality_estimator.h"
namespace previews {
namespace {
void LogPreviewsEligibilityReason(PreviewsEligibilityReason status,
PreviewsType type) {
DCHECK_LT(status, PreviewsEligibilityReason::LAST);
UMA_HISTOGRAM_ENUMERATION("Previews.EligibilityReason", status,
PreviewsEligibilityReason::LAST);
int32_t max_limit = static_cast<int32_t>(PreviewsEligibilityReason::LAST);
base::LinearHistogram::FactoryGet(
base::StringPrintf("Previews.EligibilityReason.%s",
GetStringNameForType(type).c_str()),
1, max_limit, max_limit + 1,
base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(static_cast<int>(status));
}
bool ShouldCheckOptimizationHints(PreviewsType type) {
switch (type) {
// These types may have server optimization hints.
case PreviewsType::NOSCRIPT:
case PreviewsType::RESOURCE_LOADING_HINTS:
case PreviewsType::LITE_PAGE_REDIRECT:
case PreviewsType::DEFER_ALL_SCRIPT:
return true;
// These types do not have server optimization hints.
case PreviewsType::OFFLINE:
case PreviewsType::LITE_PAGE:
return false;
case PreviewsType::NONE:
case PreviewsType::UNSPECIFIED:
case PreviewsType::DEPRECATED_AMP_REDIRECTION:
case PreviewsType::DEPRECATED_LOFI:
case PreviewsType::LAST:
break;
}
NOTREACHED();
return false;
}
// Returns true if ECT should be checked for |type| only at the commit time. If
// true is returned, then ECT need not be checked at the navigation time.
bool CheckECTOnlyAtCommitTime(PreviewsType type) {
switch (type) {
case PreviewsType::NOSCRIPT:
case PreviewsType::RESOURCE_LOADING_HINTS:
case PreviewsType::DEFER_ALL_SCRIPT:
return true;
case PreviewsType::LITE_PAGE_REDIRECT:
case PreviewsType::OFFLINE:
case PreviewsType::LITE_PAGE:
return false;
case PreviewsType::NONE:
case PreviewsType::UNSPECIFIED:
case PreviewsType::DEPRECATED_AMP_REDIRECTION:
case PreviewsType::DEPRECATED_LOFI:
case PreviewsType::LAST:
break;
}
NOTREACHED();
return false;
}
// We don't care if the ECT is unknown if the slow page threshold is set to 4G
// (i.e.: all pages).
bool ShouldCheckForUnknownECT(net::EffectiveConnectionType ect) {
if (!base::FeatureList::IsEnabled(features::kSlowPageTriggering))
return true;
return ect != net::EFFECTIVE_CONNECTION_TYPE_LAST - 1;
}
} // namespace
PreviewsDeciderImpl::PreviewsDeciderImpl(base::Clock* clock)
: blacklist_ignored_(switches::ShouldIgnorePreviewsBlacklist()),
clock_(clock),
page_id_(1u) {}
PreviewsDeciderImpl::~PreviewsDeciderImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void PreviewsDeciderImpl::Initialize(
PreviewsUIService* previews_ui_service,
std::unique_ptr<blacklist::OptOutStore> previews_opt_out_store,
std::unique_ptr<PreviewsOptimizationGuide> previews_opt_guide,
const PreviewsIsEnabledCallback& is_enabled_callback,
blacklist::BlacklistData::AllowedTypesAndVersions allowed_previews) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!is_enabled_callback.is_null());
is_enabled_callback_ = is_enabled_callback;
previews_ui_service_ = previews_ui_service;
previews_opt_guide_ = std::move(previews_opt_guide);
previews_black_list_ = std::make_unique<PreviewsBlackList>(
std::move(previews_opt_out_store), clock_, this,
std::move(allowed_previews));
}
void PreviewsDeciderImpl::OnNewBlacklistedHost(const std::string& host,
base::Time time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
previews_ui_service_->OnNewBlacklistedHost(host, time);
}
void PreviewsDeciderImpl::OnUserBlacklistedStatusChange(bool blacklisted) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
previews_ui_service_->OnUserBlacklistedStatusChange(blacklisted);
}
void PreviewsDeciderImpl::OnBlacklistCleared(base::Time time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
previews_ui_service_->OnBlacklistCleared(time);
}
void PreviewsDeciderImpl::SetPreviewsBlacklistForTesting(
std::unique_ptr<PreviewsBlackList> previews_back_list) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
previews_black_list_ = std::move(previews_back_list);
}
void PreviewsDeciderImpl::LogPreviewNavigation(const GURL& url,
bool opt_out,
PreviewsType type,
base::Time time,
uint64_t page_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
previews_ui_service_->LogPreviewNavigation(url, type, opt_out, time, page_id);
}
void PreviewsDeciderImpl::LogPreviewDecisionMade(
PreviewsEligibilityReason reason,
const GURL& url,
base::Time time,
PreviewsType type,
std::vector<PreviewsEligibilityReason>&& passed_reasons,
PreviewsUserData* user_data) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LogPreviewsEligibilityReason(reason, type);
user_data->SetEligibilityReasonForPreview(type, reason);
previews_ui_service_->LogPreviewDecisionMade(
reason, url, time, type, std::move(passed_reasons), user_data->page_id());
}
void PreviewsDeciderImpl::AddPreviewNavigation(const GURL& url,
bool opt_out,
PreviewsType type,
uint64_t page_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::Time time =
previews_black_list_->AddPreviewNavigation(url, opt_out, type);
LogPreviewNavigation(url, opt_out, type, time, page_id);
}
void PreviewsDeciderImpl::ClearBlackList(base::Time begin_time,
base::Time end_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
previews_black_list_->ClearBlackList(begin_time, end_time);
// Removes all fetched hints known to the owned optimization guide.
if (previews_opt_guide_)
previews_opt_guide_->ClearFetchedHints();
}
void PreviewsDeciderImpl::SetIgnorePreviewsBlacklistDecision(bool ignored) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
blacklist_ignored_ = ignored;
previews_ui_service_->OnIgnoreBlacklistDecisionStatusChanged(
blacklist_ignored_);
}
bool PreviewsDeciderImpl::ShouldAllowPreviewAtNavigationStart(
PreviewsUserData* previews_data,
content::NavigationHandle* navigation_handle,
bool is_reload,
PreviewsType type) const {
const GURL url = navigation_handle->GetURL();
if (!ShouldConsiderPreview(type, url, previews_data)) {
// Don't capture metrics since preview is either disabled or url is local.
return false;
}
bool is_drp_server_preview = (type == PreviewsType::LITE_PAGE);
std::vector<PreviewsEligibilityReason> passed_reasons;
PreviewsEligibilityReason eligibility =
DeterminePreviewEligibility(previews_data, navigation_handle, is_reload,
type, is_drp_server_preview, &passed_reasons);
LogPreviewDecisionMade(eligibility, url, clock_->Now(), type,
std::move(passed_reasons), previews_data);
return eligibility == PreviewsEligibilityReason::ALLOWED;
}
bool PreviewsDeciderImpl::ShouldConsiderPreview(
PreviewsType type,
const GURL& url,
PreviewsUserData* previews_data) const {
return previews::params::ArePreviewsAllowed() &&
is_enabled_callback_.Run(type) && url.has_host() &&
url.SchemeIsHTTPOrHTTPS() && previews_data;
}
PreviewsEligibilityReason PreviewsDeciderImpl::DeterminePreviewEligibility(
PreviewsUserData* previews_data,
content::NavigationHandle* navigation_handle,
bool is_reload,
PreviewsType type,
bool is_drp_server_preview,
std::vector<PreviewsEligibilityReason>* passed_reasons) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(previews::params::ArePreviewsAllowed());
const GURL url = navigation_handle->GetURL();
DCHECK(url.has_host());
DCHECK(previews_data);
// Capture the effective connection type at this time of determining
// eligibility so that it will be available at commit time.
previews_data->set_navigation_ect(effective_connection_type_);
// Do not allow previews on any authenticated pages.
if (url.has_username() || url.has_password())
return PreviewsEligibilityReason::URL_HAS_BASIC_AUTH;
passed_reasons->push_back(PreviewsEligibilityReason::URL_HAS_BASIC_AUTH);
// Do not allow previews for URL suffixes which are excluded. In practice,
// this is used to exclude navigations that look like media resources like
// navigating to http://chromium.org/video.mp4.
if (params::ShouldExcludeMediaSuffix(url))
return PreviewsEligibilityReason::EXCLUDED_BY_MEDIA_SUFFIX;
passed_reasons->push_back(
PreviewsEligibilityReason::EXCLUDED_BY_MEDIA_SUFFIX);
// Skip blacklist checks if the blacklist is ignored.
if (!blacklist_ignored_) {
if (!previews_black_list_)
return PreviewsEligibilityReason::BLACKLIST_UNAVAILABLE;
passed_reasons->push_back(PreviewsEligibilityReason::BLACKLIST_UNAVAILABLE);
// Trigger the USER_RECENTLY_OPTED_OUT rule when a reload on a preview has
// occurred recently. No need to push_back the eligibility reason as it will
// be added in IsLoadedAndAllowed as the first check.
if (recent_preview_reload_time_ &&
recent_preview_reload_time_.value() + params::SingleOptOutDuration() >
clock_->Now()) {
return PreviewsEligibilityReason::USER_RECENTLY_OPTED_OUT;
}
// The blacklist will disallow certain hosts for periods of time based on
// user's opting out of the preview.
PreviewsEligibilityReason status = previews_black_list_->IsLoadedAndAllowed(
url, type,
is_drp_server_preview &&
ignore_long_term_blacklist_for_server_previews_,
passed_reasons);
if (status != PreviewsEligibilityReason::ALLOWED) {
if (type == PreviewsType::LITE_PAGE) {
previews_data->set_black_listed_for_lite_page(true);
}
return status;
}
}
// Check the network quality for client previews that don't have optimization
// hints. This defers checking ECT for server previews because the server will
// perform its own ECT check and for previews with hints because the hints may
// specify variable ECT thresholds for slow page hints.
if (!is_drp_server_preview && !CheckECTOnlyAtCommitTime(type)) {
if (effective_connection_type_ == net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN) {
return PreviewsEligibilityReason::NETWORK_QUALITY_UNAVAILABLE;
}
passed_reasons->push_back(
PreviewsEligibilityReason::NETWORK_QUALITY_UNAVAILABLE);
// Network quality estimator may sometimes return effective connection type
// as offline when the Android APIs incorrectly return device connectivity
// as null. See https://crbug.com/838969. So, we do not trigger previews
// when |observed_effective_connection_type| is
// net::EFFECTIVE_CONNECTION_TYPE_OFFLINE.
if (effective_connection_type_ == net::EFFECTIVE_CONNECTION_TYPE_OFFLINE) {
return PreviewsEligibilityReason::DEVICE_OFFLINE;
}
passed_reasons->push_back(PreviewsEligibilityReason::DEVICE_OFFLINE);
// If the optimization type does not require optimization hints, determine
// the ECT network triggering condition here.
if (!ShouldCheckOptimizationHints(type)) {
if (effective_connection_type_ >
previews::params::GetECTThresholdForPreview(type)) {
return PreviewsEligibilityReason::NETWORK_NOT_SLOW;
}
passed_reasons->push_back(PreviewsEligibilityReason::NETWORK_NOT_SLOW);
}
if (effective_connection_type_ > params::GetSessionMaxECTThreshold()) {
return PreviewsEligibilityReason::NETWORK_NOT_SLOW_FOR_SESSION;
}
passed_reasons->push_back(
PreviewsEligibilityReason::NETWORK_NOT_SLOW_FOR_SESSION);
}
if (is_reload) {
return PreviewsEligibilityReason::RELOAD_DISALLOWED;
}
passed_reasons->push_back(PreviewsEligibilityReason::RELOAD_DISALLOWED);
// Check optimization hints, if provided.
if (ShouldCheckOptimizationHints(type)) {
if (optimization_guide::features::IsOptimizationHintsEnabled()) {
// Optimization hints are configured, so determine if those hints
// allow the optimization type (as of start-of-navigation time anyway).
return ShouldAllowPreviewPerOptimizationHints(
previews_data, navigation_handle, type, passed_reasons);
} else if (type == PreviewsType::RESOURCE_LOADING_HINTS ||
type == PreviewsType::NOSCRIPT ||
type == PreviewsType::DEFER_ALL_SCRIPT) {
return PreviewsEligibilityReason::OPTIMIZATION_HINTS_NOT_AVAILABLE;
}
}
return PreviewsEligibilityReason::ALLOWED;
}
bool PreviewsDeciderImpl::LoadPageHints(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return previews_opt_guide_->MaybeLoadOptimizationHints(navigation_handle,
base::DoNothing());
}
bool PreviewsDeciderImpl::GetResourceLoadingHints(
const GURL& url,
std::vector<std::string>* out_resource_patterns_to_block) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!previews_opt_guide_)
return false;
return previews_opt_guide_->GetResourceLoadingHints(
url, out_resource_patterns_to_block);
}
void PreviewsDeciderImpl::LogHintCacheMatch(const GURL& url,
bool is_committed) const {
if (!previews_opt_guide_)
return;
previews_opt_guide_->LogHintCacheMatch(url, is_committed);
}
bool PreviewsDeciderImpl::ShouldCommitPreview(
PreviewsUserData* previews_data,
content::NavigationHandle* navigation_handle,
PreviewsType type) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(PreviewsType::NOSCRIPT == type ||
PreviewsType::RESOURCE_LOADING_HINTS == type ||
PreviewsType::DEFER_ALL_SCRIPT == type);
const GURL committed_url = navigation_handle->GetURL();
if (previews_black_list_ && !blacklist_ignored_) {
std::vector<PreviewsEligibilityReason> passed_reasons;
// The blacklist will disallow certain hosts for periods of time based on
// user's opting out of the preview.
PreviewsEligibilityReason status = previews_black_list_->IsLoadedAndAllowed(
committed_url, type, false, &passed_reasons);
if (status != PreviewsEligibilityReason::ALLOWED) {
LogPreviewDecisionMade(status, committed_url, clock_->Now(), type,
std::move(passed_reasons), previews_data);
return false;
}
}
// Re-check server optimization hints (if provided) on this commit-time URL.
if (ShouldCheckOptimizationHints(type) &&
optimization_guide::features::IsOptimizationHintsEnabled()) {
std::vector<PreviewsEligibilityReason> passed_reasons;
PreviewsEligibilityReason status = ShouldCommitPreviewPerOptimizationHints(
previews_data, navigation_handle, type, &passed_reasons);
if (status != PreviewsEligibilityReason::ALLOWED) {
LogPreviewDecisionMade(status, committed_url, clock_->Now(), type,
std::move(passed_reasons), previews_data);
return false;
}
}
return true;
}
PreviewsEligibilityReason
PreviewsDeciderImpl::ShouldAllowPreviewPerOptimizationHints(
PreviewsUserData* previews_data,
content::NavigationHandle* navigation_handle,
PreviewsType type,
std::vector<PreviewsEligibilityReason>* passed_reasons) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(type == PreviewsType::LITE_PAGE_REDIRECT ||
type == PreviewsType::NOSCRIPT ||
type == PreviewsType::RESOURCE_LOADING_HINTS ||
type == PreviewsType::DEFER_ALL_SCRIPT);
// For LitePageRedirect, ensure it is not blacklisted for this request, and
// hints have been fully loaded.
//
// We allow all other Optimization Hint previews in the hopes that the missing
// state will load in before commit.
if (type == PreviewsType::LITE_PAGE_REDIRECT) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kIgnoreLitePageRedirectOptimizationBlacklist)) {
// Make sure to also check the ECT threshold for the Preview if we are
// bypassing the optimization guide.
if (effective_connection_type_ >
params::GetECTThresholdForPreview(type)) {
return PreviewsEligibilityReason::NETWORK_NOT_SLOW;
}
return PreviewsEligibilityReason::ALLOWED;
}
if (!previews_opt_guide_ || !previews_opt_guide_->IsReady())
return PreviewsEligibilityReason::OPTIMIZATION_HINTS_NOT_AVAILABLE;
passed_reasons->push_back(
PreviewsEligibilityReason::OPTIMIZATION_HINTS_NOT_AVAILABLE);
if (!previews_opt_guide_->CanApplyPreview(previews_data, navigation_handle,
type)) {
return PreviewsEligibilityReason::NOT_ALLOWED_BY_OPTIMIZATION_GUIDE;
}
passed_reasons->push_back(
PreviewsEligibilityReason::NOT_ALLOWED_BY_OPTIMIZATION_GUIDE);
}
return PreviewsEligibilityReason::ALLOWED;
}
PreviewsEligibilityReason
PreviewsDeciderImpl::ShouldCommitPreviewPerOptimizationHints(
PreviewsUserData* previews_data,
content::NavigationHandle* navigation_handle,
PreviewsType type,
std::vector<PreviewsEligibilityReason>* passed_reasons) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(type == PreviewsType::NOSCRIPT ||
type == PreviewsType::RESOURCE_LOADING_HINTS ||
type == PreviewsType::DEFER_ALL_SCRIPT);
// If kEnableDeferAllScriptWithoutOptimizationHints switch is provided, then
// DEFER_ALL_SCRIPT is triggered on all pages irrespective of hints provided
// by optimization hints guide.
if (type == PreviewsType::DEFER_ALL_SCRIPT &&
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableDeferAllScriptWithoutOptimizationHints)) {
return PreviewsEligibilityReason::ALLOWED;
}
if (!previews_opt_guide_ || !previews_opt_guide_->IsReady())
return PreviewsEligibilityReason::OPTIMIZATION_HINTS_NOT_AVAILABLE;
passed_reasons->push_back(
PreviewsEligibilityReason::OPTIMIZATION_HINTS_NOT_AVAILABLE);
// Check if request URL is whitelisted by the optimization guide.
if (!previews_opt_guide_->CanApplyPreview(previews_data, navigation_handle,
type)) {
return PreviewsEligibilityReason::NOT_ALLOWED_BY_OPTIMIZATION_GUIDE;
}
passed_reasons->push_back(
PreviewsEligibilityReason::NOT_ALLOWED_BY_OPTIMIZATION_GUIDE);
// The url is whitelisted, now check some additional cases of the effective
// network condition.
// Note: the network quality estimator may sometimes return effective
// connection type as offline when the Android APIs incorrectly return device
// connectivity as null. See https://crbug.com/838969. So, we do not trigger
// previews when |ect| is net::EFFECTIVE_CONNECTION_TYPE_OFFLINE.
net::EffectiveConnectionType ect = previews_data->navigation_ect();
if (CheckECTOnlyAtCommitTime(type) &&
ect == net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN) {
// Update the |ect| to the current value.
ect = effective_connection_type_;
}
if (ShouldCheckForUnknownECT(params::GetSessionMaxECTThreshold()) &&
ect == net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN) {
return PreviewsEligibilityReason::NETWORK_QUALITY_UNAVAILABLE;
}
passed_reasons->push_back(
PreviewsEligibilityReason::NETWORK_QUALITY_UNAVAILABLE);
if (ect == net::EFFECTIVE_CONNECTION_TYPE_OFFLINE) {
return PreviewsEligibilityReason::DEVICE_OFFLINE;
}
passed_reasons->push_back(PreviewsEligibilityReason::DEVICE_OFFLINE);
if (ect > params::GetSessionMaxECTThreshold()) {
return PreviewsEligibilityReason::NETWORK_NOT_SLOW_FOR_SESSION;
}
passed_reasons->push_back(
PreviewsEligibilityReason::NETWORK_NOT_SLOW_FOR_SESSION);
return PreviewsEligibilityReason::ALLOWED;
}
uint64_t PreviewsDeciderImpl::GeneratePageId() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return ++page_id_;
}
void PreviewsDeciderImpl::SetIgnoreLongTermBlackListForServerPreviews(
bool ignore_long_term_blacklist_for_server_previews) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ignore_long_term_blacklist_for_server_previews_ =
ignore_long_term_blacklist_for_server_previews;
}
void PreviewsDeciderImpl::SetEffectiveConnectionType(
net::EffectiveConnectionType effective_connection_type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
effective_connection_type_ = effective_connection_type;
}
void PreviewsDeciderImpl::AddPreviewReload() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
recent_preview_reload_time_ = clock_->Now();
}
} // namespace previews