blob: 15e7ddd5567ed73a8cbbc6bbab8a4c3d7624a48c [file] [log] [blame]
// Copyright 2018 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 "chrome/browser/previews/previews_lite_page_decider.h"
#include <vector>
#include "base/callback.h"
#include "base/command_line.h"
#include "base/memory/ptr_util.h"
#include "base/rand_util.h"
#include "base/time/default_tick_clock.h"
#include "build/build_config.h"
#include "chrome/browser/data_reduction_proxy/data_reduction_proxy_chrome_settings.h"
#include "chrome/browser/data_reduction_proxy/data_reduction_proxy_chrome_settings_factory.h"
#include "chrome/browser/previews/previews_lite_page_infobar_delegate.h"
#include "chrome/browser/previews/previews_lite_page_navigation_throttle.h"
#include "chrome/browser/previews/previews_service.h"
#include "chrome/browser/previews/previews_service_factory.h"
#include "chrome/browser/previews/previews_ui_tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_metrics.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_service.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_params.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_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/browser_context.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_user_data.h"
#include "net/base/net_errors.h"
namespace {
const char kUserNeedsNotification[] =
"previews.litepage.user-needs-notification";
const char kHostBlacklist[] = "previews.litepage.host-blacklist";
const size_t kMaxBlacklistEntries = 30;
// Cleans up the given host blacklist by removing all stale (expiry has passed)
// entries. If after removing all stale entries, the blacklist is still over
// capacity, then remove the entry with the closest expiration.
void RemoveStaleEntries(base::DictionaryValue* dict) {
std::vector<std::string> keys_to_delete;
base::Time min_value = base::Time::Max();
std::string min_key;
for (const auto& iter : dict->DictItems()) {
base::Time value = base::Time::FromDoubleT(iter.second.GetDouble());
// Delete all stale entries.
if (value <= base::Time::Now()) {
keys_to_delete.push_back(iter.first);
continue;
}
// Record the closest expiration in case we need it later on.
if (value < min_value) {
min_value = value;
min_key = iter.first;
}
}
// Remove all expired entries.
for (const std::string& key : keys_to_delete)
dict->RemoveKey(key);
// Remove the closest expiration if needed.
if (dict->DictSize() > kMaxBlacklistEntries)
dict->RemoveKey(min_key);
DCHECK_GE(kMaxBlacklistEntries, dict->DictSize());
}
} // namespace
// This WebContentsObserver watches the rest of the current navigation shows a
// notification to the user that this preview now exists and will be used on
// future eligible page loads. This is only done if the navigations finishes on
// the same URL as the one when it began. After finishing the navigation, |this|
// will be removed as an observer.
class UserNotificationWebContentsObserver
: public content::WebContentsObserver,
public content::WebContentsUserData<UserNotificationWebContentsObserver> {
public:
void SetUIShownCallback(base::OnceClosure callback) {
ui_shown_callback_ = std::move(callback);
}
private:
friend class content::WebContentsUserData<
UserNotificationWebContentsObserver>;
explicit UserNotificationWebContentsObserver(
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
void DestroySelf() {
content::WebContents* old_web_contents = web_contents();
Observe(nullptr);
old_web_contents->RemoveUserData(UserDataKey());
// DO NOT add code past this point. |this| is destroyed.
}
void DidRedirectNavigation(
content::NavigationHandle* navigation_handle) override {
DestroySelf();
// DO NOT add code past this point. |this| is destroyed.
}
void DidFinishNavigation(content::NavigationHandle* handle) override {
if (ui_shown_callback_ && handle->GetNetErrorCode() == net::OK) {
PreviewsLitePageInfoBarDelegate::Create(web_contents());
std::move(ui_shown_callback_).Run();
}
DestroySelf();
// DO NOT add code past this point. |this| is destroyed.
}
base::OnceClosure ui_shown_callback_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(UserNotificationWebContentsObserver)
PreviewsLitePageDecider::PreviewsLitePageDecider(
content::BrowserContext* browser_context)
: clock_(base::DefaultTickClock::GetInstance()),
page_id_(base::RandUint64()),
drp_settings_(nullptr),
pref_service_(nullptr),
host_bypass_blacklist_(std::make_unique<base::DictionaryValue>()) {
if (!browser_context)
return;
DataReductionProxyChromeSettings* drp_settings =
DataReductionProxyChromeSettingsFactory::GetForBrowserContext(
browser_context);
if (!drp_settings)
return;
DCHECK(!browser_context->IsOffTheRecord());
pref_service_ = Profile::FromBrowserContext(browser_context)->GetPrefs();
host_bypass_blacklist_ =
pref_service_->GetDictionary(kHostBlacklist)->CreateDeepCopy();
// Note: This switch has no effect if |drp_settings| was null since
// |host_bypass_blacklist_| would be empty anyways.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
previews::switches::kClearLitePageRedirectLocalBlacklist)) {
host_bypass_blacklist_->Clear();
pref_service_->Set(kHostBlacklist, *host_bypass_blacklist_);
}
// Add |this| as an observer to DRP, but if DRP is already initialized, check
// the prefs now.
drp_settings_ = drp_settings;
drp_settings_->AddDataReductionProxySettingsObserver(this);
if (drp_settings_->Config()) {
OnSettingsInitialized();
OnProxyRequestHeadersChanged(drp_settings->GetProxyRequestHeaders());
}
}
PreviewsLitePageDecider::~PreviewsLitePageDecider() = default;
// static
void PreviewsLitePageDecider::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(kUserNeedsNotification, true);
registry->RegisterDictionaryPref(kHostBlacklist,
std::make_unique<base::DictionaryValue>());
}
// static
std::unique_ptr<content::NavigationThrottle>
PreviewsLitePageDecider::MaybeCreateThrottleFor(
content::NavigationHandle* handle) {
DCHECK(handle);
DCHECK(handle->GetWebContents());
DCHECK(handle->GetWebContents()->GetBrowserContext());
if (base::FeatureList::IsEnabled(
previews::features::kHTTPSServerPreviewsUsingURLLoader)) {
return nullptr;
}
content::BrowserContext* browser_context =
handle->GetWebContents()->GetBrowserContext();
PreviewsService* previews_service = PreviewsServiceFactory::GetForProfile(
Profile::FromBrowserContext(browser_context));
if (!previews_service)
return nullptr;
DCHECK(!browser_context->IsOffTheRecord());
PreviewsUITabHelper* tab_helper =
PreviewsUITabHelper::FromWebContents(handle->GetWebContents());
if (!tab_helper)
return nullptr;
previews::PreviewsUserData* previews_data =
tab_helper->GetPreviewsUserData(handle);
if (!previews_data)
return nullptr;
// If this navigation is reloading on a lite page, always create a navigation
// throttle. In this event, the navigation throttle will always cancel and
// restart the navigation to a non-preview page. This is important for the
// experiment that disables previews on reloads since it won't enable the
// previews state.
bool reload_load_original =
previews::ExtractOriginalURLFromLitePageRedirectURL(handle->GetURL(),
nullptr) &&
handle->GetReloadType() != content::ReloadType::NONE;
if (previews_data->allowed_previews_state() &
content::LITE_PAGE_REDIRECT_ON ||
reload_load_original) {
return std::make_unique<PreviewsLitePageNavigationThrottle>(
handle, previews_service->previews_lite_page_decider());
}
return nullptr;
}
void PreviewsLitePageDecider::OnProxyRequestHeadersChanged(
const net::HttpRequestHeaders& headers) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This is done so that successive page ids cannot be used to track users
// across sessions. These sessions are contained in the chrome-proxy header.
page_id_ = base::RandUint64();
}
void PreviewsLitePageDecider::OnSettingsInitialized() {
// The notification only needs to be shown if the user has never seen it
// before, and is an existing Data Saver user.
if (!pref_service_->GetBoolean(kUserNeedsNotification)) {
need_to_show_notification_ = false;
} else if (drp_settings_->IsDataReductionProxyEnabled()) {
need_to_show_notification_ = true;
} else {
need_to_show_notification_ = false;
pref_service_->SetBoolean(kUserNeedsNotification, false);
}
}
void PreviewsLitePageDecider::Shutdown() {
if (drp_settings_)
drp_settings_->RemoveDataReductionProxySettingsObserver(this);
}
void PreviewsLitePageDecider::SetClockForTesting(const base::TickClock* clock) {
clock_ = clock;
}
void PreviewsLitePageDecider::SetDRPSettingsForTesting(
data_reduction_proxy::DataReductionProxySettings* drp_settings) {
drp_settings_ = drp_settings;
drp_settings_->AddDataReductionProxySettingsObserver(this);
}
void PreviewsLitePageDecider::ClearBlacklist() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
host_bypass_blacklist_->Clear();
if (pref_service_)
pref_service_->Set(kHostBlacklist, *host_bypass_blacklist_);
}
void PreviewsLitePageDecider::ClearStateForTesting() {
single_bypass_.clear();
host_bypass_blacklist_->Clear();
}
void PreviewsLitePageDecider::SetUserHasSeenUINotification() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(pref_service_);
need_to_show_notification_ = false;
pref_service_->SetBoolean(kUserNeedsNotification, false);
}
void PreviewsLitePageDecider::SetServerUnavailableFor(
base::TimeDelta retry_after) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeTicks retry_at = clock_->NowTicks() + retry_after;
if (!retry_at_.has_value() || retry_at > retry_at_)
retry_at_ = retry_at;
}
bool PreviewsLitePageDecider::IsServerUnavailable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!retry_at_.has_value())
return false;
bool server_loadshedding = retry_at_ > clock_->NowTicks();
if (!server_loadshedding)
retry_at_.reset();
return server_loadshedding;
}
void PreviewsLitePageDecider::AddSingleBypass(std::string url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Garbage collect any old entries while looking for the one for |url|.
auto entry = single_bypass_.end();
for (auto iter = single_bypass_.begin(); iter != single_bypass_.end();
/* no increment */) {
if (iter->second < clock_->NowTicks()) {
iter = single_bypass_.erase(iter);
continue;
}
if (iter->first == url)
entry = iter;
++iter;
}
// Update the entry for |url|.
const base::TimeTicks ttl =
clock_->NowTicks() + base::TimeDelta::FromMinutes(5);
if (entry == single_bypass_.end()) {
single_bypass_.emplace(url, ttl);
return;
}
entry->second = ttl;
}
bool PreviewsLitePageDecider::CheckSingleBypass(std::string url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto entry = single_bypass_.find(url);
if (entry == single_bypass_.end())
return false;
return entry->second >= clock_->NowTicks();
}
uint64_t PreviewsLitePageDecider::GeneratePageID() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return ++page_id_;
}
void PreviewsLitePageDecider::ReportDataSavings(int64_t network_bytes,
int64_t original_bytes,
const std::string& host) {
if (!drp_settings_ || !drp_settings_->data_reduction_proxy_service())
return;
drp_settings_->data_reduction_proxy_service()->UpdateDataUseForHost(
network_bytes, original_bytes, host);
drp_settings_->data_reduction_proxy_service()->UpdateContentLengths(
network_bytes, original_bytes, true /* data_reduction_proxy_enabled */,
data_reduction_proxy::DataReductionProxyRequestType::
VIA_DATA_REDUCTION_PROXY,
"text/html", true /* is_user_traffic */,
data_use_measurement::DataUseUserData::DataUseContentType::
MAIN_FRAME_HTML,
0);
}
bool PreviewsLitePageDecider::NeedsToNotifyUser() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
previews::switches::kDoNotRequireLitePageRedirectInfoBar)) {
return false;
}
return need_to_show_notification_;
}
void PreviewsLitePageDecider::NotifyUser(content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(need_to_show_notification_);
DCHECK(!UserNotificationWebContentsObserver::FromWebContents(web_contents));
UserNotificationWebContentsObserver::CreateForWebContents(web_contents);
UserNotificationWebContentsObserver* observer =
UserNotificationWebContentsObserver::FromWebContents(web_contents);
// base::Unretained is safe here because |this| outlives |web_contents|.
observer->SetUIShownCallback(
base::BindOnce(&PreviewsLitePageDecider::SetUserHasSeenUINotification,
base::Unretained(this)));
}
void PreviewsLitePageDecider::BlacklistBypassedHost(const std::string& host,
base::TimeDelta duration) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If there is an existing entry, intentionally update it.
host_bypass_blacklist_->SetKey(
host, base::Value((base::Time::Now() + duration).ToDoubleT()));
RemoveStaleEntries(host_bypass_blacklist_.get());
if (pref_service_)
pref_service_->Set(kHostBlacklist, *host_bypass_blacklist_);
}
bool PreviewsLitePageDecider::HostBlacklistedFromBypass(
const std::string& host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::Value* value = host_bypass_blacklist_->FindKey(host);
if (!value)
return false;
DCHECK(value->is_double());
base::Time expiry = base::Time::FromDoubleT(value->GetDouble());
return expiry > base::Time::Now();
}