// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/policy/dlp/dlp_clipboard_notifier.h"
#include <memory>
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "chrome/browser/chromeos/policy/dlp/clipboard_bubble.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_clipboard_bubble_constants.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_policy_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_monitor.h"
#include "ui/base/clipboard/clipboard_non_backed.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/screen.h"
#include "ui/events/ozone/events_ozone.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/clipboard_history_controller.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/scoped_clipboard_history_pause.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/resources/vector_icons/vector_icons.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/chromeos/policy/dlp/dlp_browser_helper_lacros.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
namespace policy {
namespace {
ui::DataTransferEndpoint CloneEndpoint(
base::optional_ref<const ui::DataTransferEndpoint> data_endpoint) {
if (!data_endpoint.has_value()) {
return ui::DataTransferEndpoint(ui::EndpointType::kDefault);
}
return ui::DataTransferEndpoint(*data_endpoint);
}
void SynthesizePaste() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
auto* host = ash::GetWindowTreeHostForDisplay(
display::Screen::GetScreen()->GetDisplayForNewWindows().id());
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
auto* host = dlp::GetActiveWindowTreeHost();
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
DCHECK(host);
ui::KeyEvent control_press(/*type=*/ui::EventType::kKeyPressed,
ui::VKEY_CONTROL,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/0);
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Set a property as if this is a key event not consumed by IME.
// Ozone/wayland IME relies on this flag to work properly.
ui::SetKeyboardImeFlags(&control_press, ui::kPropertyKeyboardImeIgnoredFlag);
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
host->DeliverEventToSink(&control_press);
ui::KeyEvent v_press(/*type=*/ui::EventType::kKeyPressed, ui::VKEY_V,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/ui::EF_CONTROL_DOWN);
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Set a property as if this is a key event not consumed by IME.
// Ozone/wayland IME relies on this flag to work properly.
ui::SetKeyboardImeFlags(&v_press, ui::kPropertyKeyboardImeIgnoredFlag);
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
host->DeliverEventToSink(&v_press);
ui::KeyEvent v_release(/*type=*/ui::EventType::kKeyReleased, ui::VKEY_V,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/ui::EF_CONTROL_DOWN);
host->DeliverEventToSink(&v_release);
ui::KeyEvent control_release(/*type=*/ui::EventType::kKeyReleased,
ui::VKEY_CONTROL,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/0);
host->DeliverEventToSink(&control_release);
}
bool HasEndpoint(const std::vector<ui::DataTransferEndpoint>& saved_endpoints,
base::optional_ref<const ui::DataTransferEndpoint> endpoint) {
const ui::EndpointType endpoint_type =
endpoint.has_value() ? endpoint->type() : ui::EndpointType::kDefault;
for (const auto& ept : saved_endpoints) {
if (ept.type() == endpoint_type) {
if (endpoint_type != ui::EndpointType::kUrl)
return true;
else if (ept.IsSameURLWith(*endpoint))
return true;
}
}
return false;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void OnToastClicked() {
ash::NewWindowDelegate::GetPrimary()->OpenUrl(
GURL(dlp::kDlpLearnMoreUrl),
ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
ash::NewWindowDelegate::Disposition::kNewForegroundTab);
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
} // namespace
DlpClipboardNotifier::DlpClipboardNotifier() {
ui::ClipboardMonitor::GetInstance()->AddObserver(this);
}
DlpClipboardNotifier::~DlpClipboardNotifier() {
ui::ClipboardMonitor::GetInstance()->RemoveObserver(this);
}
void DlpClipboardNotifier::NotifyBlockedAction(
base::optional_ref<const ui::DataTransferEndpoint> data_src,
base::optional_ref<const ui::DataTransferEndpoint> data_dst) {
DCHECK(data_src.has_value());
DCHECK(data_src->GetURL());
const std::u16string host_name =
base::UTF8ToUTF16(data_src->GetURL()->host());
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (data_dst.has_value()) {
if (data_dst->type() == ui::EndpointType::kCrostini) {
ShowToast(kClipboardBlockCrostiniToastId,
ash::ToastCatalogName::kClipboardBlockedAction,
l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_BLOCKED_ON_COPY_VM, host_name,
l10n_util::GetStringUTF16(IDS_CROSTINI_LINUX)));
return;
}
if (data_dst->type() == ui::EndpointType::kPluginVm) {
ShowToast(kClipboardBlockPluginVmToastId,
ash::ToastCatalogName::kClipboardBlockedAction,
l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_BLOCKED_ON_COPY_VM, host_name,
l10n_util::GetStringUTF16(IDS_PLUGIN_VM_APP_NAME)));
return;
}
if (data_dst->type() == ui::EndpointType::kArc) {
ShowToast(kClipboardBlockArcToastId,
ash::ToastCatalogName::kClipboardBlockedAction,
l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_BLOCKED_ON_COPY_VM, host_name,
l10n_util::GetStringUTF16(IDS_POLICY_DLP_ANDROID_APPS)));
return;
}
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
ShowBlockBubble(l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_BLOCKED_ON_PASTE, host_name));
}
void DlpClipboardNotifier::WarnOnPaste(
base::optional_ref<const ui::DataTransferEndpoint> data_src,
base::optional_ref<const ui::DataTransferEndpoint> data_dst,
base::OnceCallback<void()> reporting_cb) {
DCHECK(data_src.has_value());
DCHECK(data_src->GetURL());
CloseWidget(widget_.get(), views::Widget::ClosedReason::kUnspecified);
const std::u16string host_name =
base::UTF8ToUTF16(data_src->GetURL()->host());
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (data_dst.has_value()) {
if (data_dst->type() == ui::EndpointType::kCrostini) {
ShowToast(kClipboardWarnCrostiniToastId,
ash::ToastCatalogName::kClipboardWarnOnPaste,
l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_WARN_ON_COPY_VM,
l10n_util::GetStringUTF16(IDS_CROSTINI_LINUX)));
return;
}
if (data_dst->type() == ui::EndpointType::kPluginVm) {
ShowToast(kClipboardWarnPluginVmToastId,
ash::ToastCatalogName::kClipboardWarnOnPaste,
l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_WARN_ON_COPY_VM,
l10n_util::GetStringUTF16(IDS_PLUGIN_VM_APP_NAME)));
return;
}
if (data_dst->type() == ui::EndpointType::kArc) {
ShowToast(kClipboardWarnArcToastId,
ash::ToastCatalogName::kClipboardWarnOnPaste,
l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_WARN_ON_COPY_VM,
l10n_util::GetStringUTF16(IDS_POLICY_DLP_ANDROID_APPS)));
return;
}
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
std::unique_ptr<ui::ClipboardData> warned_clipboard_data;
#if BUILDFLAG(IS_CHROMEOS_ASH)
ui::DataTransferEndpoint dte(ui::EndpointType::kClipboardHistory);
auto* data_ptr =
ui::ClipboardNonBacked::GetForCurrentThread()->GetClipboardData(&dte);
if (data_ptr) { // dlp_clipboard_notifier_unittests do not set the clipboard
// before calling WarnOnPaste.
warned_clipboard_data = std::make_unique<ui::ClipboardData>(*data_ptr);
}
#endif
auto proceed_cb =
base::BindOnce(&DlpClipboardNotifier::ProceedPressed,
base::Unretained(this), std::move(warned_clipboard_data),
CloneEndpoint(data_dst), std::move(reporting_cb));
auto cancel_cb =
base::BindOnce(&DlpClipboardNotifier::CancelWarningPressed,
base::Unretained(this), CloneEndpoint(data_dst));
ShowWarningBubble(l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_WARN_ON_PASTE, host_name),
std::move(proceed_cb), std::move(cancel_cb));
}
void DlpClipboardNotifier::WarnOnBlinkPaste(
base::optional_ref<const ui::DataTransferEndpoint> data_src,
base::optional_ref<const ui::DataTransferEndpoint> data_dst,
content::WebContents* web_contents,
base::OnceCallback<void(bool)> paste_cb) {
DCHECK(data_src.has_value());
DCHECK(data_src->GetURL());
CloseWidget(widget_.get(), views::Widget::ClosedReason::kUnspecified);
const std::u16string host_name =
base::UTF8ToUTF16(data_src->GetURL()->host());
auto proceed_cb =
base::BindOnce(&DlpClipboardNotifier::BlinkProceedPressed,
base::Unretained(this), CloneEndpoint(data_dst));
auto cancel_cb =
base::BindOnce(&DlpClipboardNotifier::CancelWarningPressed,
base::Unretained(this), CloneEndpoint(data_dst));
ShowWarningBubble(l10n_util::GetStringFUTF16(
IDS_POLICY_DLP_CLIPBOARD_WARN_ON_PASTE, host_name),
std::move(proceed_cb), std::move(cancel_cb));
SetPasteCallback(std::move(paste_cb));
Observe(web_contents);
}
bool DlpClipboardNotifier::DidUserApproveDst(
base::optional_ref<const ui::DataTransferEndpoint> data_dst) {
return HasEndpoint(approved_dsts_, data_dst);
}
bool DlpClipboardNotifier::DidUserCancelDst(
base::optional_ref<const ui::DataTransferEndpoint> data_dst) {
return HasEndpoint(cancelled_dsts_, data_dst);
}
void DlpClipboardNotifier::ProceedPressed(
std::unique_ptr<ui::ClipboardData> data,
const ui::DataTransferEndpoint& data_dst,
base::OnceCallback<void()> reporting_cb,
views::Widget* widget) {
CloseWidget(widget, views::Widget::ClosedReason::kAcceptButtonClicked);
approved_dsts_.push_back(data_dst);
std::move(reporting_cb).Run();
if (!display::Screen::GetScreen()) { // Clipboard related elements do not
// exist in unittests.
return;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Temporarily ignore clipboard events because we are going to replace the
// system clipboard and this would otherwise trigger a call to
// `OnClipboardDataChanged` that resets the user warn selection.
ignore_clipboard_events_ = true;
// Pause clipboard history since we are temporarily replacing the system
// clipboard data with a non user-initiated action.
auto scoped_clipboard_history_pause =
ash::ClipboardHistoryController::Get()->CreateScopedPause();
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
CHECK(clipboard);
std::unique_ptr<ui::ClipboardData> current_clipboard_data =
clipboard->WriteClipboardData(std::move(data));
#endif
SynthesizePaste();
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Restore the original system clipboard data.
ui::ClipboardNonBacked::GetForCurrentThread()->WriteClipboardData(
std::move(current_clipboard_data));
ignore_clipboard_events_ = false;
#endif
}
void DlpClipboardNotifier::BlinkProceedPressed(
const ui::DataTransferEndpoint& data_dst,
views::Widget* widget) {
approved_dsts_.push_back(data_dst);
RunPasteCallback();
CloseWidget(widget, views::Widget::ClosedReason::kAcceptButtonClicked);
}
void DlpClipboardNotifier::CancelWarningPressed(
const ui::DataTransferEndpoint& data_dst,
views::Widget* widget) {
cancelled_dsts_.push_back(data_dst);
CloseWidget(widget, views::Widget::ClosedReason::kCancelButtonClicked);
}
void DlpClipboardNotifier::ResetUserWarnSelection() {
approved_dsts_.clear();
cancelled_dsts_.clear();
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void DlpClipboardNotifier::ShowToast(const std::string& id,
ash::ToastCatalogName catalog_name,
const std::u16string& text) const {
ash::ToastData toast(
id, catalog_name, text, ash::ToastData::kDefaultToastDuration,
/*visible_on_lock_screen=*/false,
/*has_dismiss_button=*/true,
/*custom_dismiss_text=*/
l10n_util::GetStringUTF16(IDS_POLICY_DLP_CLIPBOARD_BLOCK_TOAST_BUTTON),
/*dismiss_callback=*/base::BindRepeating(&OnToastClicked),
/*leading_icon=*/ash::kSystemMenuBusinessIcon);
ash::ToastManager::Get()->Show(std::move(toast));
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
void DlpClipboardNotifier::OnClipboardDataChanged() {
if (ignore_clipboard_events_) {
return;
}
ResetUserWarnSelection();
}
void DlpClipboardNotifier::OnWidgetDestroying(views::Widget* widget) {
Observe(nullptr);
DlpDataTransferNotifier::OnWidgetDestroying(widget);
}
void DlpClipboardNotifier::WebContentsDestroyed() {
CloseWidget(widget_.get(), views::Widget::ClosedReason::kUnspecified);
}
} // namespace policy