// 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 "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"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.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"

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() {
  auto* host = ash::GetWindowTreeHostForDisplay(
      display::Screen::Get()->GetDisplayForNewWindows().id());
  DCHECK(host);

  ui::KeyEvent control_press(/*type=*/ui::EventType::kKeyPressed,
                             ui::VKEY_CONTROL,
                             /*code=*/static_cast<ui::DomCode>(0),
                             /*flags=*/0);
  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);
  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;
}

void OnToastClicked() {
  ash::NewWindowDelegate::GetInstance()->OpenUrl(
      GURL(dlp::kDlpLearnMoreUrl),
      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      ash::NewWindowDelegate::Disposition::kNewForegroundTab);
}

}  // 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()->GetHost());
  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;
    }
  }

  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()->GetHost());
  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;
    }
  }

  std::unique_ptr<ui::ClipboardData> warned_clipboard_data;
  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);
  }

  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()->GetHost());

  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::Get()) {  // Clipboard related elements do not
                                  // exist in unittests.
    return;
  }

  // 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));

  SynthesizePaste();

  // Restore the original system clipboard data.
  ui::ClipboardNonBacked::GetForCurrentThread()->WriteClipboardData(
      std::move(current_clipboard_data));

  ignore_clipboard_events_ = false;
}

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();
}

void DlpClipboardNotifier::ShowToast(const std::string& id,
                                     ash::ToastCatalogName catalog_name,
                                     const std::u16string& text) const {
  ash::ToastData toast(id, catalog_name, text);
  toast.button_type = ash::ToastData::ButtonType::kTextButton;
  toast.button_text =
      l10n_util::GetStringUTF16(IDS_POLICY_DLP_CLIPBOARD_BLOCK_TOAST_BUTTON);
  toast.button_callback = base::BindRepeating(&OnToastClicked);
  toast.leading_icon = &ash::kSystemMenuBusinessIcon;
  ash::ToastManager::Get()->Show(std::move(toast));
}

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
