blob: 25789691c34e972a436f0a7494b354e96d19111d [file] [log] [blame]
// Copyright 2020 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/safe_browsing/user_interaction_observer.h"
#include <string>
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "components/safe_browsing/core/features.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
namespace safe_browsing {
// If true, a delayed warning will be shown when the user clicks on the page.
// If false, the warning won't be shown, but a metric will be recorded on the
// first click.
const base::FeatureParam<bool> kEnableMouseClicks{&kDelayedWarnings, "mouse",
/*default_value=*/false};
const char kDelayedWarningsHistogram[] = "SafeBrowsing.DelayedWarnings.Event";
namespace {
const char kWebContentsUserDataKey[] =
"web_contents_safe_browsing_user_interaction_observer";
void RecordUMA(DelayedWarningEvent event) {
base::UmaHistogramEnumeration(kDelayedWarningsHistogram, event);
}
} // namespace
SafeBrowsingUserInteractionObserver::SafeBrowsingUserInteractionObserver(
content::WebContents* web_contents,
const security_interstitials::UnsafeResource& resource,
bool is_main_frame,
scoped_refptr<SafeBrowsingUIManager> ui_manager)
: content::WebContentsObserver(web_contents),
web_contents_(web_contents),
resource_(resource),
ui_manager_(ui_manager) {
DCHECK(base::FeatureList::IsEnabled(kDelayedWarnings));
key_press_callback_ =
base::BindRepeating(&SafeBrowsingUserInteractionObserver::HandleKeyPress,
base::Unretained(this));
mouse_event_callback_ = base::BindRepeating(
&SafeBrowsingUserInteractionObserver::HandleMouseEvent,
base::Unretained(this));
// Pass a callback to the render widget host instead of implementing
// WebContentsObserver::DidGetUserInteraction(). The reason for this is that
// render widget host handles keyboard events earlier and the callback can
// indicate that it wants the key press to be ignored.
// (DidGetUserInteraction() can only observe and not cancel the event.)
web_contents->GetRenderViewHost()->GetWidget()->AddKeyPressEventCallback(
key_press_callback_);
web_contents->GetRenderViewHost()->GetWidget()->AddMouseEventCallback(
mouse_event_callback_);
// Observe permission bubble events.
permissions::PermissionRequestManager* permission_request_manager =
permissions::PermissionRequestManager::FromWebContents(web_contents);
if (permission_request_manager) {
permission_request_manager->AddObserver(this);
}
RecordUMA(DelayedWarningEvent::kPageLoaded);
}
SafeBrowsingUserInteractionObserver::~SafeBrowsingUserInteractionObserver() {
permissions::PermissionRequestManager* permission_request_manager =
permissions::PermissionRequestManager::FromWebContents(web_contents());
if (permission_request_manager) {
permission_request_manager->RemoveObserver(this);
}
web_contents_->GetRenderViewHost()->GetWidget()->RemoveKeyPressEventCallback(
key_press_callback_);
web_contents_->GetRenderViewHost()->GetWidget()->RemoveMouseEventCallback(
mouse_event_callback_);
if (!interstitial_shown_) {
RecordUMA(DelayedWarningEvent::kWarningNotShown);
}
}
// static
void SafeBrowsingUserInteractionObserver::CreateForWebContents(
content::WebContents* web_contents,
const security_interstitials::UnsafeResource& resource,
bool is_main_frame,
scoped_refptr<SafeBrowsingUIManager> ui_manager) {
// This method is called for all unsafe resources on |web_contents|. Only
// create an observer if there isn't one.
// TODO(crbug.com/1057157): The observer should observe all unsafe resources
// instead of the first one only.
if (FromWebContents(web_contents)) {
return;
}
auto observer = std::make_unique<SafeBrowsingUserInteractionObserver>(
web_contents, resource, is_main_frame, ui_manager);
web_contents->SetUserData(kWebContentsUserDataKey, std::move(observer));
}
// static
SafeBrowsingUserInteractionObserver*
SafeBrowsingUserInteractionObserver::FromWebContents(
content::WebContents* web_contents) {
return static_cast<SafeBrowsingUserInteractionObserver*>(
web_contents->GetUserData(kWebContentsUserDataKey));
}
void SafeBrowsingUserInteractionObserver::RenderViewHostChanged(
content::RenderViewHost* old_host,
content::RenderViewHost* new_host) {
old_host->GetWidget()->RemoveKeyPressEventCallback(key_press_callback_);
new_host->GetWidget()->AddKeyPressEventCallback(key_press_callback_);
old_host->GetWidget()->RemoveMouseEventCallback(mouse_event_callback_);
new_host->GetWidget()->AddMouseEventCallback(mouse_event_callback_);
}
void SafeBrowsingUserInteractionObserver::WebContentsDestroyed() {
CleanUp();
Detach();
}
void SafeBrowsingUserInteractionObserver::DidFinishNavigation(
content::NavigationHandle* handle) {
// Remove the observer on a top frame navigation to another page. The user is
// now on another page so we don't need to wait for an interaction.
if (!handle->IsInMainFrame() || handle->IsSameDocument()) {
return;
}
// If this is the first navigation we are seeing, it must be the
// navigation that caused this observer to be created.
// As an example, if the user navigates to http://test.site, the order of
// events are:
// 1. SafeBrowsingUrlCheckerImpl detects that the URL should be blocked with
// an interstitial.
// 2. It delays the interstitial and creates an instance of this class.
// 3. DidFinishNavigation() of this class is called.
//
// This means that the first time we are here, we should ignore this event
// because it's not an interesting navigation. We only want to handle the
// navigations that follow.
if (!initial_navigation_finished_) {
initial_navigation_finished_ = true;
return;
}
// If a download happens when an instance of this observer is attached to
// the WebContents, DelayedNavigationThrottle cancels the download. As a
// result, the page should remain unchanged on downloads. Record a metric and
// ignore this cancelled navigation.
if (handle->IsDownload()) {
RecordUMA(DelayedWarningEvent::kDownloadCancelled);
return;
}
Detach();
// DO NOT add code past this point. |this| is destroyed.
}
void SafeBrowsingUserInteractionObserver::Detach() {
web_contents()->RemoveUserData(kWebContentsUserDataKey);
}
void SafeBrowsingUserInteractionObserver::DidToggleFullscreenModeForTab(
bool entered_fullscreen,
bool will_cause_resize) {
// This class is only instantiated upon a navigation. If a page is in
// fullscreen mode, any navigation away from it should exit fullscreen. This
// means that this class is never instantiated while the current web contents
// is in fullscreen mode, so |entered_fullscreen| should never be false when
// this method is called for the first time. However, we don't know if it's
// guaranteed for a page to not be in fullscreen upon navigation, so we just
// ignore this event if the page exited fullscreen.
if (!entered_fullscreen) {
return;
}
// IMPORTANT: Store the web contents pointer in a temporary because |this| is
// deleted after ShowInterstitial().
content::WebContents* contents = web_contents();
ShowInterstitial(DelayedWarningEvent::kWarningShownOnFullscreenAttempt);
// Exit fullscreen only after navigating to the interstitial. We don't want to
// interfere with an ongoing fullscreen request.
contents->ExitFullscreen(will_cause_resize);
// DO NOT add code past this point. |this| is destroyed.
}
void SafeBrowsingUserInteractionObserver::OnBubbleAdded() {
// The page requested a permission that triggered a permission prompt. Deny
// and show the interstitial.
permissions::PermissionRequestManager* permission_request_manager =
permissions::PermissionRequestManager::FromWebContents(web_contents());
if (!permission_request_manager) {
return;
}
permission_request_manager->Deny();
ShowInterstitial(DelayedWarningEvent::kWarningShownOnPermissionRequest);
// DO NOT add code past this point. |this| is destroyed.
}
void SafeBrowsingUserInteractionObserver::OnJavaScriptDialog() {
ShowInterstitial(DelayedWarningEvent::kWarningShownOnJavaScriptDialog);
// DO NOT add code past this point. |this| is destroyed.
}
void SafeBrowsingUserInteractionObserver::OnPasswordSaveOrAutofillDenied() {
if (password_save_or_autofill_denied_metric_recorded_) {
return;
}
password_save_or_autofill_denied_metric_recorded_ = true;
RecordUMA(DelayedWarningEvent::kPasswordSaveOrAutofillDenied);
}
void SafeBrowsingUserInteractionObserver::OnDesktopCaptureRequest() {
ShowInterstitial(DelayedWarningEvent::kWarningShownOnDesktopCaptureRequest);
// DO NOT add code past this point. |this| is destroyed.
}
bool SafeBrowsingUserInteractionObserver::HandleKeyPress(
const content::NativeWebKeyboardEvent& event) {
ShowInterstitial(DelayedWarningEvent::kWarningShownOnKeypress);
// DO NOT add code past this point. |this| is destroyed.
return true;
}
bool SafeBrowsingUserInteractionObserver::HandleMouseEvent(
const blink::WebMouseEvent& event) {
if (event.GetType() != blink::WebInputEvent::Type::kMouseDown) {
return false;
}
// If warning isn't enabled for mouse clicks, still record the first time when
// the user clicks.
if (!kEnableMouseClicks.Get()) {
if (!mouse_click_with_no_warning_recorded_) {
RecordUMA(DelayedWarningEvent::kWarningNotTriggeredOnMouseClick);
mouse_click_with_no_warning_recorded_ = true;
}
return false;
}
ShowInterstitial(DelayedWarningEvent::kWarningShownOnMouseClick);
// DO NOT add code past this point. |this| is destroyed.
return true;
}
void SafeBrowsingUserInteractionObserver::ShowInterstitial(
DelayedWarningEvent event) {
// Show the interstitial.
DCHECK(!interstitial_shown_);
interstitial_shown_ = true;
CleanUp();
RecordUMA(event);
SafeBrowsingUIManager::StartDisplayingBlockingPage(ui_manager_, resource_);
Detach();
// DO NOT add code past this point. |this| is destroyed.
}
void SafeBrowsingUserInteractionObserver::CleanUp() {
web_contents_->GetRenderViewHost()->GetWidget()->RemoveKeyPressEventCallback(
key_press_callback_);
web_contents_->GetRenderViewHost()->GetWidget()->RemoveMouseEventCallback(
mouse_event_callback_);
}
} // namespace safe_browsing