blob: 9ce8fad1504f0d852bad349c0346689944079a3f [file] [log] [blame]
// Copyright (c) 2017 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/safe_browsing/triggers/trigger_manager.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/base_ui_manager.h"
#include "components/safe_browsing/browser/threat_details.h"
#include "components/safe_browsing/common/safe_browsing_prefs.h"
#include "components/safe_browsing/features.h"
#include "components/security_interstitials/content/unsafe_resource.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "net/url_request/url_request_context_getter.h"
DEFINE_WEB_CONTENTS_USER_DATA_KEY(
safe_browsing::TriggerManagerWebContentsHelper);
namespace safe_browsing {
namespace {
bool TriggerNeedsScout(const TriggerType trigger_type) {
switch (trigger_type) {
case TriggerType::SECURITY_INTERSTITIAL:
// Security interstitials only need legacy SBER opt-in.
return false;
case TriggerType::AD_SAMPLE:
// Ad samples need Scout-level opt-in.
return true;
case TriggerType::GAIA_PASSWORD_REUSE:
// Gaia password reuses only need legacy SBER opt-in.
return false;
}
// By default, require Scout so we are more restrictive on data collection.
return true;
}
bool TriggerNeedsOptInForCollection(const TriggerType trigger_type) {
switch (trigger_type) {
case TriggerType::SECURITY_INTERSTITIAL:
// For security interstitials, users can change the opt-in while the
// trigger runs, so collection can begin without opt-in.
return false;
case TriggerType::AD_SAMPLE:
// Ad samples happen in the background so the user must already be opted
// in before the trigger is allowed to run.
return true;
case TriggerType::GAIA_PASSWORD_REUSE:
// For Gaia password reuses, it is unlikely for users to change opt-in
// while the trigger runs, so we require opt-in for collection to avoid
// overheads.
return true;
}
// By default, require opt-in for all triggers.
return true;
}
bool CanSendReport(const SBErrorOptions& error_display_options,
const TriggerType trigger_type) {
// If the |kAdSamplerCollectButDontSendFeature| feature is enabled then we
// will overlook other checks to force the report to be created (which is safe
// because we ensure it will be discarded downstream).
// TODO(crbug.com/776893): Remote the feature and this logic.
if (trigger_type == TriggerType::AD_SAMPLE &&
base::FeatureList::IsEnabled(kAdSamplerCollectButDontSendFeature)) {
return true;
}
// Some triggers require that users are eligible for elevated Scout data
// collection in order to run.
bool scout_check_ok = !TriggerNeedsScout(trigger_type) ||
error_display_options.is_scout_reporting_enabled;
// Reports are only sent for non-incoginito users who are allowed to modify
// the Extended Reporting setting and have opted-in to Extended Reporting.
return !error_display_options.is_off_the_record &&
error_display_options.is_extended_reporting_opt_in_allowed &&
error_display_options.is_extended_reporting_enabled && scout_check_ok;
}
} // namespace
DataCollectorsContainer::DataCollectorsContainer() {}
DataCollectorsContainer::~DataCollectorsContainer() {}
TriggerManager::TriggerManager(BaseUIManager* ui_manager)
: ui_manager_(ui_manager),
trigger_throttler_(new TriggerThrottler()),
weak_factory_(this) {}
TriggerManager::~TriggerManager() {}
void TriggerManager::set_trigger_throttler(TriggerThrottler* throttler) {
trigger_throttler_.reset(throttler);
}
// static
SBErrorOptions TriggerManager::GetSBErrorDisplayOptions(
const PrefService& pref_service,
const content::WebContents& web_contents) {
return SBErrorOptions(/*is_main_frame_load_blocked=*/false,
IsExtendedReportingOptInAllowed(pref_service),
web_contents.GetBrowserContext()->IsOffTheRecord(),
IsExtendedReportingEnabled(pref_service),
IsScout(pref_service),
/*is_proceed_anyway_disabled=*/false,
/*should_open_links_in_new_tab=*/false,
/*show_back_to_safety_button=*/true,
/*help_center_article_link=*/std::string());
}
bool TriggerManager::CanStartDataCollection(
const SBErrorOptions& error_display_options,
const TriggerType trigger_type) {
// If the |kAdSamplerCollectButDontSendFeature| feature is enabled then we
// will overlook other checks to force the report to be created (which is safe
// because we ensure it will be discarded downstream).
// TODO(crbug.com/776893): Remote the feature and this logic.
if (trigger_type == TriggerType::AD_SAMPLE &&
base::FeatureList::IsEnabled(kAdSamplerCollectButDontSendFeature)) {
return true;
}
// Some triggers require that the user be opted-in to extended reporting in
// order to run, while others can run without opt-in (eg: because users are
// prompted for opt-in as part of the trigger).
bool optin_required_check_ok =
!TriggerNeedsOptInForCollection(trigger_type) ||
error_display_options.is_extended_reporting_enabled;
// Some triggers require that users are eligible for elevated Scout data
// collection in order to run.
bool scout_check_ok = !TriggerNeedsScout(trigger_type) ||
error_display_options.is_scout_reporting_enabled;
// We start data collection as long as user is not incognito and is able to
// change the Extended Reporting opt-in, and the |trigger_type| has available
// quota. For some triggers we also require Scout or extended reporting opt-in
// in order to start data collection.
return !error_display_options.is_off_the_record &&
error_display_options.is_extended_reporting_opt_in_allowed &&
optin_required_check_ok && scout_check_ok &&
trigger_throttler_->TriggerCanFire(trigger_type);
}
bool TriggerManager::StartCollectingThreatDetails(
const TriggerType trigger_type,
content::WebContents* web_contents,
const security_interstitials::UnsafeResource& resource,
net::URLRequestContextGetter* request_context_getter,
history::HistoryService* history_service,
const SBErrorOptions& error_display_options) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!CanStartDataCollection(error_display_options, trigger_type))
return false;
// Ensure we're not already collecting ThreatDetails on this tab. Create an
// entry in the map for this |web_contents| if it's not there already.
DataCollectorsContainer* collectors = &data_collectors_map_[web_contents];
if (collectors->threat_details != nullptr)
return false;
bool should_trim_threat_details = trigger_type == TriggerType::AD_SAMPLE;
collectors->threat_details =
scoped_refptr<ThreatDetails>(ThreatDetails::NewThreatDetails(
ui_manager_, web_contents, resource, request_context_getter,
history_service, should_trim_threat_details,
base::Bind(&TriggerManager::ThreatDetailsDone,
weak_factory_.GetWeakPtr())));
return true;
}
bool TriggerManager::FinishCollectingThreatDetails(
const TriggerType trigger_type,
content::WebContents* web_contents,
const base::TimeDelta& delay,
bool did_proceed,
int num_visits,
const SBErrorOptions& error_display_options) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Make sure there's a ThreatDetails collector running on this tab.
if (!base::ContainsKey(data_collectors_map_, web_contents))
return false;
DataCollectorsContainer* collectors = &data_collectors_map_[web_contents];
if (collectors->threat_details == nullptr)
return false;
// Determine whether a report should be sent.
bool should_send_report = CanSendReport(error_display_options, trigger_type);
if (should_send_report) {
// Find the data collector and tell it to finish collecting data. We expect
// it to notify us when it's finished so we can clean up references to it.
content::BrowserThread::PostDelayedTask(
content::BrowserThread::IO, FROM_HERE,
base::BindOnce(&ThreatDetails::FinishCollection,
collectors->threat_details, did_proceed, num_visits),
delay);
// Record that this trigger fired and collected data.
trigger_throttler_->TriggerFired(trigger_type);
} else {
// We aren't telling ThreatDetails to finish the report so we should clean
// up our map ourselves.
ThreatDetailsDone(web_contents);
}
return should_send_report;
}
void TriggerManager::ThreatDetailsDone(content::WebContents* web_contents) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Clean up the ThreatDetailsdata collector on the specified tab.
if (!base::ContainsKey(data_collectors_map_, web_contents))
return;
DataCollectorsContainer* collectors = &data_collectors_map_[web_contents];
collectors->threat_details = nullptr;
}
void TriggerManager::WebContentsDestroyed(content::WebContents* web_contents) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!base::ContainsKey(data_collectors_map_, web_contents))
return;
data_collectors_map_.erase(web_contents);
}
TriggerManagerWebContentsHelper::TriggerManagerWebContentsHelper(
content::WebContents* web_contents,
TriggerManager* trigger_manager)
: content::WebContentsObserver(web_contents),
trigger_manager_(trigger_manager) {}
TriggerManagerWebContentsHelper::~TriggerManagerWebContentsHelper() {}
void TriggerManagerWebContentsHelper::CreateForWebContents(
content::WebContents* web_contents,
TriggerManager* trigger_manager) {
DCHECK(web_contents);
if (!FromWebContents(web_contents)) {
web_contents->SetUserData(
UserDataKey(), base::WrapUnique(new TriggerManagerWebContentsHelper(
web_contents, trigger_manager)));
}
}
void TriggerManagerWebContentsHelper::WebContentsDestroyed() {
trigger_manager_->WebContentsDestroyed(web_contents());
}
} // namespace safe_browsing