blob: 77b83d027df4fb077cda3b81d40af9818ca3e9fa [file] [log] [blame]
// Copyright 2019 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/reputation/reputation_web_contents_observer.h"
#include <string>
#include <utility>
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/reputation/reputation_service.h"
#include "chrome/browser/reputation/safety_tip_ui.h"
#include "components/security_state/core/features.h"
#include "components/security_state/core/security_state.h"
#include "components/ukm/content/source_url_recorder.h"
#include "content/public/browser/navigation_entry.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
namespace {
void RecordHeuristicsUKMData(ReputationCheckResult result,
ukm::SourceId navigation_source_id,
SafetyTipInteraction action) {
// If we didn't trigger any heuristics at all, we don't want to record UKM
// data.
if (!result.triggered_heuristics.triggered_any()) {
return;
}
ukm::builders::Security_SafetyTip(navigation_source_id)
.SetSafetyTipStatus(static_cast<int64_t>(result.safety_tip_status))
.SetSafetyTipInteraction(static_cast<int64_t>(action))
.SetTriggeredKeywordsHeuristics(
result.triggered_heuristics.keywords_heuristic_triggered)
.SetTriggeredLookalikeHeuristics(
result.triggered_heuristics.lookalike_heuristic_triggered)
.SetTriggeredServerSideBlocklist(
result.triggered_heuristics.blocklist_heuristic_triggered)
.SetUserPreviouslyIgnored(
result.safety_tip_status ==
security_state::SafetyTipStatus::kBadReputationIgnored ||
result.safety_tip_status ==
security_state::SafetyTipStatus::kLookalikeIgnored)
.Record(ukm::UkmRecorder::Get());
}
void OnSafetyTipClosed(ReputationCheckResult result,
base::Time start_time,
ukm::SourceId navigation_source_id,
SafetyTipInteraction action) {
std::string action_suffix;
bool warning_dismissed = false;
switch (action) {
case SafetyTipInteraction::kNoAction:
action_suffix = "NoAction";
break;
case SafetyTipInteraction::kLeaveSite:
action_suffix = "LeaveSite";
break;
case SafetyTipInteraction::kDismiss:
NOTREACHED();
// Do nothing because the dismissal action passed to this method should
// be the more specific version (esc, close, or ignore).
break;
case SafetyTipInteraction::kDismissWithEsc:
action_suffix = "DismissWithEsc";
warning_dismissed = true;
break;
case SafetyTipInteraction::kDismissWithClose:
action_suffix = "DismissWithClose";
warning_dismissed = true;
break;
case SafetyTipInteraction::kDismissWithIgnore:
action_suffix = "DismissWithIgnore";
warning_dismissed = true;
break;
case SafetyTipInteraction::kLearnMore:
action_suffix = "LearnMore";
break;
case SafetyTipInteraction::kNotShown:
NOTREACHED();
// Do nothing because the OnSafetyTipClosed should never be called if the
// safety tip is not shown.
break;
}
if (warning_dismissed) {
base::UmaHistogramCustomTimes(
security_state::GetSafetyTipHistogramName(
std::string("Security.SafetyTips.OpenTime.Dismiss"),
result.safety_tip_status),
base::Time::Now() - start_time, base::TimeDelta::FromMilliseconds(1),
base::TimeDelta::FromHours(1), 100);
}
base::UmaHistogramCustomTimes(
security_state::GetSafetyTipHistogramName(
std::string("Security.SafetyTips.OpenTime.") + action_suffix,
result.safety_tip_status),
base::Time::Now() - start_time, base::TimeDelta::FromMilliseconds(1),
base::TimeDelta::FromHours(1), 100);
RecordHeuristicsUKMData(result, navigation_source_id, action);
}
} // namespace
ReputationWebContentsObserver::~ReputationWebContentsObserver() = default;
void ReputationWebContentsObserver::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument() ||
!navigation_handle->HasCommitted() || navigation_handle->IsErrorPage()) {
MaybeCallReputationCheckCallback();
return;
}
last_navigation_safety_tip_info_ = {security_state::SafetyTipStatus::kUnknown,
GURL()};
last_safety_tip_navigation_entry_id_ = 0;
MaybeShowSafetyTip(
ukm::ConvertToSourceId(navigation_handle->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID),
/*record_ukm_if_tip_not_shown=*/true);
}
void ReputationWebContentsObserver::OnVisibilityChanged(
content::Visibility visibility) {
MaybeShowSafetyTip(ukm::GetSourceIdForWebContentsDocument(web_contents()),
/*record_ukm_if_tip_not_shown=*/false);
}
security_state::SafetyTipInfo
ReputationWebContentsObserver::GetSafetyTipInfoForVisibleNavigation() const {
content::NavigationEntry* entry =
web_contents()->GetController().GetVisibleEntry();
if (!entry)
return {security_state::SafetyTipStatus::kUnknown, GURL()};
return last_safety_tip_navigation_entry_id_ == entry->GetUniqueID()
? last_navigation_safety_tip_info_
: security_state::SafetyTipInfo(
{security_state::SafetyTipStatus::kUnknown, GURL()});
}
void ReputationWebContentsObserver::RegisterReputationCheckCallbackForTesting(
base::OnceClosure callback) {
reputation_check_callback_for_testing_ = std::move(callback);
}
ReputationWebContentsObserver::ReputationWebContentsObserver(
content::WebContents* web_contents)
: WebContentsObserver(web_contents),
profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())),
weak_factory_(this) {
last_navigation_safety_tip_info_ = {security_state::SafetyTipStatus::kUnknown,
GURL()};
}
void ReputationWebContentsObserver::MaybeShowSafetyTip(
ukm::SourceId navigation_source_id,
bool record_ukm_if_tip_not_shown) {
if (web_contents()->GetMainFrame()->GetVisibilityState() !=
content::PageVisibilityState::kVisible) {
return;
}
const GURL& url = web_contents()->GetLastCommittedURL();
if (!url.SchemeIsHTTPOrHTTPS()) {
return;
}
ReputationService* service = ReputationService::Get(profile_);
service->GetReputationStatus(
url, base::BindRepeating(
&ReputationWebContentsObserver::HandleReputationCheckResult,
weak_factory_.GetWeakPtr(), navigation_source_id,
record_ukm_if_tip_not_shown));
}
void ReputationWebContentsObserver::HandleReputationCheckResult(
ukm::SourceId navigation_source_id,
bool record_ukm_if_tip_not_shown,
ReputationCheckResult result) {
UMA_HISTOGRAM_ENUMERATION("Security.SafetyTips.SafetyTipShown",
result.safety_tip_status);
// Set this field independent of whether the feature to show the UI is
// enabled/disabled. Metrics code uses this field and we want to record
// metrics regardless of the feature being enabled/disabled.
last_navigation_safety_tip_info_ = {result.safety_tip_status,
result.suggested_url};
// A navigation entry should always exist because reputation checks are only
// triggered when a committed navigation finishes.
last_safety_tip_navigation_entry_id_ =
web_contents()->GetController().GetLastCommittedEntry()->GetUniqueID();
// Since we downgrade indicator when a safety tip is triggered, update the
// visible security state if we have a non-kNone status. This has to happen
// after last_safety_tip_navigation_entry_id_ is updated.
if (result.safety_tip_status != security_state::SafetyTipStatus::kNone) {
web_contents()->DidChangeVisibleSecurityState();
}
if (result.safety_tip_status == security_state::SafetyTipStatus::kNone ||
result.safety_tip_status ==
security_state::SafetyTipStatus::kBadKeyword) {
FinalizeReputationCheckWhenTipNotShown(record_ukm_if_tip_not_shown, result,
navigation_source_id);
return;
}
if (result.safety_tip_status ==
security_state::SafetyTipStatus::kLookalikeIgnored ||
result.safety_tip_status ==
security_state::SafetyTipStatus::kBadReputationIgnored) {
UMA_HISTOGRAM_ENUMERATION("Security.SafetyTips.SafetyTipIgnoredPageLoad",
result.safety_tip_status);
FinalizeReputationCheckWhenTipNotShown(record_ukm_if_tip_not_shown, result,
navigation_source_id);
return;
}
if (!base::FeatureList::IsEnabled(security_state::features::kSafetyTipUI)) {
// When the feature isn't enabled, we 'ignore' the UI after the first visit
// to make it easier to disambiguate the control groups' first visit from
// subsequent navigations to the flagged page in metrics. Since the user
// never sees the UI, this is a no-op from their perspective.
if (result.safety_tip_status ==
security_state::SafetyTipStatus::kLookalike ||
result.safety_tip_status ==
security_state::SafetyTipStatus::kBadReputation) {
ReputationService::Get(profile_)->OnUIDisabledFirstVisit(result.url);
}
FinalizeReputationCheckWhenTipNotShown(record_ukm_if_tip_not_shown, result,
navigation_source_id);
return;
}
ShowSafetyTipDialog(web_contents(), result.safety_tip_status, result.url,
result.suggested_url,
base::BindOnce(OnSafetyTipClosed, result,
base::Time::Now(), navigation_source_id));
MaybeCallReputationCheckCallback();
}
void ReputationWebContentsObserver::MaybeCallReputationCheckCallback() {
if (reputation_check_callback_for_testing_.is_null())
return;
std::move(reputation_check_callback_for_testing_).Run();
}
void ReputationWebContentsObserver::FinalizeReputationCheckWhenTipNotShown(
bool record_ukm,
ReputationCheckResult result,
ukm::SourceId navigation_source_id) {
if (record_ukm) {
RecordHeuristicsUKMData(result, navigation_source_id,
SafetyTipInteraction::kNotShown);
}
MaybeCallReputationCheckCallback();
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(ReputationWebContentsObserver)